1. 程式人生 > >連連看--詳解及實現

連連看--詳解及實現

看似簡單的遊戲,實現起來也並不是那麼輕鬆。在消除的演算法上面卡殼了整整一天(腦袋笨),然後就是遊戲的各種狀態控制也十分繁瑣。因此想通過此來給大家提供我對於解決這些問題的思路。
雖然使用C#寫的,但是其設計思路及核心的消除演算法可借鑑並由其他語言輕鬆實現。解釋也儘量詳細,希望能幫到大家。
程式碼中解釋也十分詳細,嫌文章太長可直接看程式碼。

資源在此:連連看–C#實現
注意!由於設計缺陷(圖示太大,窗體尺寸太大),此程式(資源)只能執行在1080P螢幕上,且放縮比例為100%(比例太高看不到RESET和START按鈕)。
對於螢幕為1366x768的朋友,可用VS2015開啟,通過設計視窗進行檢視。
對此文章或資源有任何的問題,請提出,我會盡量做出改進。

遊戲主介面介紹

遊戲主介面

① scoreLabel ② recordLabel ③ resetBtn ④ startBtn

介面中共有100個PictureBox,即pictureBox0~99,其中外圈的pictureBox不用於放圖片,而是為了連線(畫線)時方便。即連線效果是通過對應位置的pictureBox顯示不同圖片來實現的。例如點選左側第二列兩個間隔開的相同圖片(如兩個齒輪)時,需要通過左側第一列對應位置圖片改變來實現(通過顯示左右線 ,上下線 | ,以及四種拐彎線 來模擬連線)。

點類Point和圖類Graph

  • 點類,每個pictureBox對應於一個點。如第一個不可見的pictureBox對應(0,0),第一張圖片對應(1,1)。
  • 圖類,有成員變數graph,是一個二維10*10的陣列,每個元素對應於圖中的點。通過這些元素的值,來操縱對應位置的pictureBox,實現圖片載入、消除、畫線等。
//點類
public class Point {
    int x;
    int y;
    bool valid;  //有效點
    /*
     * 通過valid來判定此點是否屬於圖類。假如點為(-1,-1),則通過此點呼叫某些函式會導致訪問越界
     * 因此需要用valid來斷定,讓函式在應對無效點時不會得出錯誤結果
     */
    //建構函式,通過座標點(x,y)
    public Point(int
x, int y) { valid = true; //先預設點為有效點 if(x < 0 || x > 9 || y < 0 || y > 9) valid = false; //點無效 else { this.x = x; this.y = y; } } //複製建構函式 public Point(Point p) { this.x = p.x; this.y = p.y; //不必根據p的valid來設定此valid,每次獲取valid都需要通過判斷 } //x屬性的get和set public int X { get { return x; } set { x = value; } } //y屬性的get和set public int Y { get { return y; } set { y = value; } } //valid屬性的get和set public bool Valid { get { if(x >= 0 && x <= 9 && y >= 0 && y <= 9) //根據x和y的值,設定valid。放在此處更新以避免通過直接設定valid而導致錯誤 valid = true; else valid = false; return valid; } //無set屬性,避免誤用。因為get中會自動判斷valid的值,因此valid的值一定為正確的 } //通過索引設定點 public void setPoint(int index) { x = index / 10; y = index % 10; } //獲取點的資訊----Test public string getPointInfo() { return "X: " + x.ToString() + " Y: " + y.ToString() + " Valid: " + valid.ToString(); } };

——

//圖類Graph
public class Graph {
    int[,] graph;
    //用於描述10*10pictureBox中圖片的型別
    //0:無圖    1:圖片1    2:圖片2    3:圖片3    ...
    public Graph() {
        graph = new int[10, 10];  //10*10
        //重置圖陣列
        for(int i = 0; i < 10; i++)
            for(int j = 0; j < 10; j++)
                graph[i, j] = 0;
    }
    //獲取圖的資訊----Test
    public string getInfo() {
        string s = "";
        for(int i = 0; i < 10; i++) {
            for(int j = 0; j < 10; j++) {
                s = s + graph[i, j].ToString() + " ";
            }
            s += "\n";  //按行輸出
        }
        return s;
    }
    //過載1:設定圖中某位置的值,通過座標點(i,j)
    public void setValue(int i,int j,int value) {
        //座標點錯誤,Exception
        if(i<0 || i >= 10 || j<0 || j >= 10) throw new Exception("座標無效");
        graph[i, j] = value;
    }
    //過載2:設定圖中某位置的值,通過索引index
    public void setValue(int index,int value) {
        //索引錯誤,Exception
        if(index < 0 || index > 99) throw new Exception("索引無效");
        graph[index/10, index%10] = value;  //通過計算索引對應的點來設定
    }
    //過載1:獲取點在圖中位置的值
    public int getValue(Point p) {
        if(p.Valid) return graph[p.X, p.Y];  //點有效,就返回對應位置的值
        else throw new Exception("點無效");
    }
    //過載2:獲取索引在圖中位置的值
    public int getValue(int index) {
        if(index >= 0 && index <= 99) return graph[index / 10, index % 10];
        else throw new Exception("索引無效");
    }
    //過載1:判斷點對應的圖中是否標記有圖片(非0)
    public bool hasPicture(Point p) {
        if(p.Valid) {
            if(graph[p.X, p.Y] != 0) return true;  //有圖片,true
            else return false;
        }
        else throw new Exception("點無效");
    }
    //過載2:判斷索引對應的圖中是否標記有圖片(非0)
    public bool hasPicture(int index) {
        if(index >= 0 && index <= 99) {
            Point p = new Point(index / 10, index % 10);
            return hasPicture(p);
        }
        else throw new Exception("索引無效");
    }
};

控制變數設計

下面的變數設計中,包含了遊戲的設計思路及各種權衡,因此請仔細檢視。

  • 圖片需要隨機產生,因此有Random random
  • 遊戲音效需要SoundPlayer soundPlayer,要using System.Media;才能使用。
  • 點選兩張圖片才進行消除判斷,因此需要兩個點Point point1Point piont2
  • 判斷是否屬於遊戲狀態(玩家可操控裝態)bool inGame。此變數可根據設計的不同而更改。我的設計是消除時讓消除線顯示一會兒,之後再重置遊戲中的各種狀態(point1point2、點選計數、遊戲圖片更新等)。如果在顯示連線時玩家點選了圖片,後果很難預料。
  • 遊戲時間int time。即遊戲用時,用於玩家分數計算。
  • 遊戲的點選計數int clickCount,在pictureBox的點選事件中更新。當點選次數為1時,只更新點選位置的pictureBox圖片背景顏色(標記為選中),以及point1等。當次數為2時,更新point2等屬性並開始判斷兩點是否能消除,並做出相應操作。並最後通過重置函式(後面會講)進行重置。
  • 連線路徑點陣列Point[] pointRout。此陣列用於在連線時,將連線上的點放入,以便連線完後,將對應圖片重置為空,實現連線消失,否則連線將一直存在。
  • 二線連通的拐點Point tp2。判斷兩點是否能消除,是通過判斷兩點能否通過一條直線連通,或者通過兩條直線連通,或者三條直線連通。這種方法的好處在於,寫好一線連通時,二線連通可呼叫一線連通來實現,而三線連通又可通過呼叫二線連通一線連通來實現。而二線連通時,產生一個拐點,三線連通時,產生兩個拐點。因此用tp2和tp3來記錄,便於後用。
  • 三線連通的拐點Point tp3
  • 判斷是否產生消除int eliminate。此變數不用bool型別是因為,判斷消除時,有4種情況:無法消除、一線連通、二線連通、三線連通。每種情況都對應不同的畫線函式。因此需要用int型。
  • 定義圖片種類及每種圖片可用張數int[] picCount,其中陣列長度為圖片可用種類數。我用了10*10的佈局,因此需要放圖片64張(外圍無圖片),故選用8種圖片,陣列長度為8,每種圖片可用8張,即每個元素的值為8。
  • 判斷遊戲是否結束bool gameOver。遊戲結束時,呼叫結算框。
  • 統計圖中剩餘的圖片數量int leftPic。為避免遊戲出現死迴圈,即無法消除時,需要重置剩餘圖片的位置。網上的解法是迴圈判斷圖中兩兩能否消除,如果不能,則進入死結,需要重置。我也試過此方法,但是當遊戲網格太大時,就會導致嚴重的效能問題,如10*10的網格,i從0~98,j從i+1到99。每次都判斷兩點能否一線連通、二線連通、三線連通,則第一次就會導致接近一萬次判斷三種消除,遊戲就卡死了。因此我放了一個RESET按鈕在窗體上,預設不可點選。當圖片張數小於等於8張(可根據具體情況設定)時,變為可點選。然後為此按鈕編寫Click事件,來實現圖片重置。這樣雖然當遊戲沒有出現死結時也可點選,但是避免了效能問題。
  • 遊戲的紀錄int record,用於在標籤上顯示。
  • 最後是點陣圖物件Bitmap bmx。將圖片匯入資原始檔,然後構造點陣圖物件。也可不用點陣圖物件,直接用將圖片放入image資料夾,並放在工程的bin>debug中,使用時寫相對路徑即可。

下面是變數程式碼

Graph graph;  //圖物件
Random random = new Random();  //隨機數物件
SoundPlayer soundPlayer = new SoundPlayer();  //音效檔案物件
Point point1;  //點選的第一個點
Point point2;  //點選的第二個點
bool inGame;  //遊戲中(用於控制滑鼠點選是否有用,如未按開始按鈕時)
int time;  //遊戲用時
int clickCount;  //點選計數,值為2時開始進行消除判斷,並重置
Point[] pointRout;  //連線路徑點
int pointCount;  //連線路徑長度
Point tp2;  //二線連線時的轉點
Point tp3;  //三線連線時的轉點
int eliminate;  //消除資訊,0,1,2,3對應無消除、一線消除、二線消除、三線消除
int[] picCount;  //陣列長度用於規定圖片種類數,元素值為對應圖片種類可產生的數目
bool gameOver;  //遊戲是否結束
int leftPic;  //圖中剩餘的圖片數量
int record;  //遊戲記錄

//可忽略,用圖片時寫絕對路徑(相對路徑)也可
Bitmap bm0;  //bm0到bm7為此連連看遊戲的8種圖片
Bitmap bm1;
Bitmap bm2;
Bitmap bm3;
Bitmap bm4;
Bitmap bm5;
Bitmap bm6;
Bitmap bm7;
Bitmap bm_updown;  //上下線   ┊
Bitmap bm_leftright;  //左右線  ┈
Bitmap bm_upleft;  //上轉左線  ┘
Bitmap bm_upright;  //上轉右線  └
Bitmap bm_downleft;  //下轉左線  ┐
Bitmap bm_downright;  //下轉右線  ┌

遊戲主要函式

如果直接講遊戲思路,有點空中樓閣的意思,不容易理解。因此通過將遊戲詳細思路嵌入到函式的註釋中,來讓大家看到實際的效果。
如果嫌看函式程式碼過於麻煩,可在最後找到我的資源連結。

一線連通:public bool checkOneLine(Point p1,Point p2);
判斷一線連通,即判斷兩點是否x方向共線,或y方向共線,且中間無圖片。一旦確定x方向共線,但是中間有圖,則false。y方向同理。

//一線連通
public bool checkOneLine(Point p1,Point p2) {
    if(p1.X == p2.X && p1.Y == p2.Y)  //兩點為同一點,false
        return false;
    if(p1.X != p2.X && p1.Y != p2.Y)  //兩點不在同一橫向或豎向,即不在同一直線上
        return false;

    //確定兩點橫向或豎向共線後,只要在此方向上有圖片(阻隔),則不連通(false)
    if(p1.X == p2.X) {  //兩點橫向共線
        //不進行p1和p2位置(左右)判斷,下面的for函式會自動區分兩點的位置
        //即通過類似i<p2.Y來區分。第一點在左則進入第一個for迴圈,否則進入第二個for迴圈
        //橫向+掃描(p1在左)
        for(int i = p1.Y + 1; i < p2.Y; i++) {
            if(graph.hasPicture(new Point(p1.X, i))) return false;
        }
        //橫向-掃描(p1在右)
        for(int i = p1.Y - 1; i > p2.Y; i--) {
            if(graph.hasPicture(new Point(p1.X, i))) return false;
        }
    }
    else {  //兩點豎向共線
        //豎向+掃描(p1在上)
        for(int i = p1.X + 1; i < p2.X; i++) {
            if(graph.hasPicture(new Point(i, p1.Y))) return false;
        }
        //豎向-掃描(p1在下)
        for(int i = p1.X - 1; i > p2.X; i--) {
            if(graph.hasPicture(new Point(i, p1.Y))) return false;
        }
    }
    return true;  //在連點共線的方向上沒有圖片阻隔,true(一線連通)
}

二線連通:public bool checkTwoLine(Point p1,Point p2);
二線連通
兩點可通過兩條直線連線,則兩點必定處於矩形的對角點上。因此只需要找出另外兩個對角點A、B,若p1和A一線連通,且A和p2一線連通;或p1和B一線連通,且B和p2一線連通,則可二線連通。在獲得二線連通時,需要設定轉點tp2的值A或B。

//二線連通
public bool checkTwoLine(Point p1,Point p2) {
    //兩線連通時,兩點組成一個矩形。另外兩個頂點A和B即二線連通情況的可能轉點
    Point A = new Point(p1.X, p2.Y);
    Point B = new Point(p2.X, p1.Y);
    if(graph.hasPicture(A) && graph.hasPicture(B))  //兩頂點都有圖,即兩頂點都不可用作轉點
        return false;
    if(graph.getValue(A.X * 10 + A.Y) == 0) {  //A點無圖情況
        //p1與A可一線連線,且A與p2可一線連線
        if(checkOneLine(p1, A) && checkOneLine(A, p2)) {
            tp2 = A;  //設定兩線連線的轉點為A
            return true;
        }
    }
    if(graph.getValue(B.X * 10 + B.Y) == 0) {  //B點無圖情況
        //p1與B可一線連線,且B與p2可一線連線
        if(checkOneLine(p1, B) && checkOneLine(B, p2)) {
            tp2 = B;
            return true;
        }
    }
    //A、B點都無圖,但是在p1通往A、B或A、B通往p2路徑上有圖片阻隔
    return false;
}

三線連通:public bool checkThreeLine(Point p1,Point p2);
通過p1向上下左右四個方向搜尋,獲取不同的可和p2二線連通的點A。但是此時的A不一定是最優的點。因此用點陣列turnPoint來儲存它們,最後分別判斷p1通過每個點到達p2所需的路徑長度,來獲取最優的A,此A即tp3。
此時需要獲取路徑長度的函式,因此臨時定義兩個獲取路徑長度的函式:
public int distance1(Point p1,Point p2); //計算兩點直線距離
public int distance2(Point p1,Point p2); //計算兩點折線距離

//計算兩點直線距離
public int distance1(Point p1,Point p2) {
    if(p1.X != p2.X && p1.Y != p2.Y) throw new Exception("兩點非同一直線");
    int dis = 0;
    if(p1.X == p2.X) dis = Math.Abs(p1.Y - p2.Y);  //兩點同橫向
    else dis = Math.Abs(p1.X - p2.X);  //兩點同豎向
    return dis;
}
//計算兩點折線距離
public int distance2(Point p1,Point p2) {
    checkTwoLine(p1, p2);  //通過呼叫checkTwoLine來重置tp2,通過tp2來呼叫distance1
    return distance1(p1, tp2) + distance1(tp2, p2);  //通過tp2做連結,兩次呼叫distance1
}

//三線連通
public bool checkThreeLine(Point p1,Point p2) {
    /*
     * 有可能找到的三線連通點不是最優,因此用一個Point[] turnPoint來
     * 儲存所有找到的 能和p2二線連線的轉點,最後通過判斷通過各個點的
     * 路徑長度,來選擇最優轉點作為tp3
     */
    Point[] turnPoint = new Point[100];
    int count = 0;  //找到的轉點計數
    //橫向+搜尋
    for(int i = p1.Y + 1; i < 10; i++) {
        Point A = new Point(p1.X, i);
        if(graph.hasPicture(A)) break;  //有圖,取消接下來的 橫向+ 搜尋
        else {
            if(checkTwoLine(A, p2))  //A點可與p2二線連通,則A點是轉點,放入轉點陣列
                turnPoint[count++] = new Point(A);
        }
    }
    //橫向-搜尋
    for(int i = p1.Y - 1; i >= 0; i--) {
        Point A = new Point(p1.X, i);
        if(graph.hasPicture(A)) break;
        else {
            if(checkTwoLine(A, p2))
                turnPoint[count++] = new Point(A);
        }
    }
    //縱向+搜尋
    for(int i = p1.X + 1; i < 10; i++) {
        Point A = new Point(i, p1.Y);
        if(graph.hasPicture(A)) break;
        else {
            if(checkTwoLine(A, p2))
                turnPoint[count++] = new Point(A);
        }
    }
    //縱向-搜尋
    for(int i = p1.X - 1; i >= 0; i--) {
        Point A = new Point(i, p1.Y);
        if(graph.hasPicture(A)) break;
        else {
            if(checkTwoLine(A, p2))
                turnPoint[count++] = new Point(A);
        }
    }

    //找最優點tp3
    if(count != 0) {  //找到了轉點
        Point p = turnPoint[0];
        //通過p1和轉點p的兩點直線距離 和p與p2的兩點折線距離來獲得
        //p1和p2通過轉點p的三點折線距離
        int dis = distance1(p1, p) + distance2(p, p2);  //dis用於獲取三點最短距離
        for(int i = 1; i < count; i++) {
            //內部_dis,分別獲取p1和p2通過不同轉點的三點折線距離
            int _dis = distance1(p1, turnPoint[i]) + distance2(turnPoint[i], p2);
            if(_dis < dis) {  //找到一個所需距離更短的轉點turnPoint[i]
                dis = _dis;
                p = turnPoint[i];  //p設定為最優轉點
            }
        }
        tp3 = p;  //設定tp3為三線連線的轉點
        /*
         * 每次checkTwoLine都會重置tp2,
         * 而distance2中呼叫了此函式,且checkThreeLine函式最後呼叫的
         * checkTwoLine函式產生的tp2也不一定為正確的tp2。因此需要通過
         * 再次用最優點與p2找二線連通,來設定正確的tp2
        */
        checkTwoLine(tp3, p2);  //checkTwoLine會自動設定tp2
        return true;
    }
    return false;  //沒有找到任何轉點,故無三線連通
}

畫直線函式:public void drawOneLine(Point p1, Point p2);

//畫兩點直線
public void drawOneLine(Point p1, Point p2) {
    //rout用於儲存兩點(直線連線)間的點
    Point[] rout = new Point[10];  //畫直線最多10個點。將此函式拷貝到他處時,注意陣列長度
    int routCount = 0;  //點計數
    if(p1.X == p2.X && p1.Y == p2.Y) return;  //兩點為同一直線,不畫線
    if(p1.X != p2.X && p1.Y != p2.Y) throw new Exception("兩點不共線");
    if(p1.X == p2.X) {  //兩點橫向連通
        //p1在左
        for(int i = p1.Y + 1; i < p2.Y; i++) {
            rout[routCount++] = new Point(p1.X, i);  //將路徑點放入區域性路徑點陣列rout中
            getPictureBox(new Point(p1.X, i)).Image = bm_leftright;  //設定圖片為左右直線
        }
        //p2在左
        for(int i = p2.Y + 1; i < p1.Y; i++) {
            rout[routCount++] = new Point(p1.X, i);
            getPictureBox(new Point(p1.X, i)).Image = bm_leftright;
        }
    }
    else {  //兩點豎向連通
        //p1在上
        for(int i = p1.X + 1; i < p2.X; i++) {
            rout[routCount++] = new Point(i, p1.Y);
            getPictureBox(new Point(i, p1.Y)).Image = bm_updown;  //上下直線
        }
        //p2在上
        for(int i = p2.X + 1; i < p1.X; i++) {
            rout[routCount++] = new Point(i, p1.Y);
            getPictureBox(new Point(i, p1.Y)).Image = bm_updown;
        }
    }
    /*
     * 劃線後,將放入區域性路徑點陣列的路徑放入最終的外部路徑點陣列pointRout中
     * 因為畫直線的陣列可能會被畫折線(drawTwoLine)呼叫,因此不可直接覆蓋
     * pointRout陣列,只能將畫直線(drawOneLine)的點新增到其中
    */
    for(int i = 0; i < routCount; i++) {  //將路徑點新增到最終的路徑點陣列pointRout中
        pointRout[pointCount++] = new Point(rout[i]);
    }
}

畫一折線:public void drawTwoLine(Point p1, Point p2);

//畫兩點折線
public void drawTwoLine(Point p1, Point p2) {
    Point[] rout = new Point[20];  //折線在此程式中最多20個點(實際18個,p1和p2不會入rout)
    int routCount = 0;
    //tp2與p1同橫向
    if(p1.X == tp2.X) {
        //p1在tp2左
        if(p1.Y < tp2.Y) {
            drawOneLine(p1, tp2);  //p1到tp2畫直線
            //tp2在p2上方
            if(tp2.X < p2.X) {
                getPictureBox(tp2).Image = bm_downleft;  //tp2顯示下左轉線
                rout[routCount++] = new Point(tp2);  //將tp2新增入rout中
                drawOneLine(tp2, p2);  //tp2到p2畫直線
            }
            else {  //tp2在p2下方
                getPictureBox(tp2).Image = bm_upleft;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
        }
        else {  //tp2在p1左
            drawOneLine(p1, tp2);
            //tp2在p2上方
            if(tp2.X < p2.X) {
                getPictureBox(tp2).Image = bm_downright;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
            else {  //tp2在p2上方
                getPictureBox(tp2).Image = bm_upright;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
        }
    }
    else {  //tp2與p1同豎向
        //p1在tp2上
        if(p1.X < tp2.X) {
            drawOneLine(p1, tp2);
            if(tp2.Y < p2.Y) {  //tp2在p2左
                getPictureBox(tp2).Image = bm_upright;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
            else {  //tp2在p2右
                getPictureBox(tp2).Image = bm_upleft;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
        }
        else {  //tp2在p1上
            drawOneLine(p1, tp2);
            //tp2在p2左
            if(tp2.Y < p2.Y) {
                getPictureBox(tp2).Image = bm_downright;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
            else {  //tp2在p2右
                getPictureBox(tp2).Image = bm_downleft;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
        }
    }
    //將畫兩點折線的點加入路徑點陣列中
    for(int i = 0; i < routCount; i++)
        pointRout[pointCount++] = new Point(rout[i]);
}

畫二折線(三路連通):public void drawThreeLine(Point p1,Point p2);

//畫三點折線
public void drawThreeLine(Point p1,Point p2) {
    Point[] rout = new Point[30];  //三線連通路徑點少於30個,具體多少懶得算
    int routCount = 0;  //路徑點個數計數
    //p1與tp3同橫向
    if(p1.X == tp3.X) {
        if(p1.Y < tp3.Y) {  //p1在tp3左
            drawOneLine(p1, tp3);  //p1到tp3畫直線
            //tp3在tp2上方
            if(tp3.X < tp2.X) {
                getPictureBox(tp3).Image = bm_downleft;  //tp3畫下左折線
                rout[routCount++] = new Point(tp3);  //將tp3加入路徑點
                drawTwoLine(tp3, p2);
            }
            else {  //tp3在tp2下方
                getPictureBox(tp3).Image = bm_upleft;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
        }
        else {  //tp3在tp1左
            drawOneLine(p1, tp3);
            //tp3在tp2上方
            if(tp3.X < tp2.X) {
                getPictureBox(tp3).Image = bm_downright;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
            else {  //tp3在tp2下方
                getPictureBox(tp3).Image = bm_upright;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
        }
    }
    else {  //tp3與p1同豎向
        //p1在tp3上
        if(p1.X < tp3.X) {
            drawOneLine(p1, tp3);
            if(tp3.Y < tp2.Y) {  //tp3在tp2左
                getPictureBox(tp3).Image = bm_upright;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
            else {  //tp3在tp2右
                getPictureBox(tp3).Image = bm_upleft;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
        }
        else {  //tp3在p1上
            drawOneLine(p1, tp3);
            if(tp3.Y < tp2.Y) {  //tp3在tp2左
                getPictureBox(tp3).Image = bm_downright;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
            else {
                //tp3在tp2右
                getPictureBox(tp3).Image = bm_downleft;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
        }
    }
    //將路徑點放入最終路徑點陣列pointRout中
    for(int i = 0; i < routCount; i++) {
        pointRout[pointCount++] = new Point(rout[i]);
    }
}

判斷兩圖片是否相同:public bool samePicture(Point p1, Point p2);

/*
 * 判斷兩點對應的圖片是否相同
 * 不能通過判斷pictureBox的Image屬性,因為它是引用,會判斷兩者是否為同一物件
 * 網上還有說法是執行緒池的原因,不懂
 * 因此通過判斷兩點對應的圖graph中的值是否相同來實現
 */
public bool samePicture(Point p1, Point p2) {
    if(graph.getValue(p1) == graph.getValue(p2)) return true;
    else return false;
}

播放聲音:public void soundPlay(string s);

//播放聲音,根據傳入的字串來確定音效檔案位置
public void soundPlay(string s) {
    soundPlayer.SoundLocation = s;
    soundPlayer.Load();
    soundPlayer.Play();
}

根據索引或點獲取對應的PictureBox物件:

//過載1:獲取索引對應的pictureBox物件
public PictureBox getPictureBox(int index) {
    switch(index) {
        case 0: return pictureBox0;
        case 1: return pictureBox1;
        case 2: return pictureBox2;
        ...//中間省略n行
        case 99: return pictureBox99;
        default:
            throw new Exception("索引無效");
    }
}

//過載2:獲取點對應的PictureBox物件
public PictureBox getPictureBox(Point p) {
    if(!p.Valid) throw new Exception("點無效");
    int index = p.X * 10 + p.Y;
    switch(index) {
        //這裡當然不是手打的,通過for迴圈輸出到Console,然後copy的。我當然沒有那麼蠢!
        case 0: return pictureBox0;
        case 1: return pictureBox1;
        case 2: return pictureBox2;
        case 3: return pictureBox3;
        ...//省略n行
        case 99: return pictureBox99;
        default: throw new Exception("獲取pictureBox越界");
    }
}

重置遊戲剩餘圖片:public void resetLeftPic();

//遊戲進入死迴圈時,重置剩餘圖片
public void resetLeftPic() {
    int[] index = new int[100];  //存放需要重置圖片的索引陣列
    for(int i = 0; i < 100; i++) index[i] = 0;  //保險起見,重置元素為0
    int count = 0;  //剩餘圖片計數
    //獲取右圖片位置
    for(int i = 0; i < 100; i++) {
        if(graph.getValue(i) != 0) {  //此處有圖
            index[count++] = i;  //將此點索引加入索引陣列
            picCount[graph.getValue(i) - 1]++;  //將此圖片新增到可用圖片陣列中
        }
    }
    //從剩餘圖片中隨機一張放到各個位置
    for(int i = 0; i < count; i++) {
        int pic = random.Next(8);  //隨機圖片索引
        //設定對應pictureBox的圖片,getPic會自動將pictureBox對應的點的值設定為對應圖片索引
        //即只要設定了pictureBox的圖片,就會更新其對應點在graph上的值
        getPictureBox(index[i]).Image = getPic(pic, index[i]);
    }
    //避免點選一次圖片後再點RESET按鈕時,對應pictureBox還是被標記為選中狀態
    //因此手動取消point1對應pictureBox的選中狀態
    if(point1.Valid) setPicBC(point1);
    clickCount = 0;  //點選次數清零
}

重置外圍圖片:public void resetPeripheralPic();

public void resetPeripheralPic() {
    pictureBox0.Image = null;
    ...//根據哪些是外圍圖片,來進行設定
}

獲取圖片:public Bitmap getPic(int x,int index);

//獲取圖片,通過第一個引數選擇圖片種類, 第二個引數選擇pictureBox
//對於用絕對路徑、相對路徑來設定圖片的,可將此函式的返回值換為Image
public Bitmap getPic(int x, int index) {
    int count = 0;  //獲取剩餘圖片種類,主要用於判斷是否為只剩下一種圖片可用
    int onlyPic = 0;  //假如只有一種圖片可用時,標記那種圖片
    for(int i = 0; i < 8; i++) {
        if(picCount[i] != 0) {
            count++;
            onlyPic = i;
        }
    }
    //只剩一種圖片,讓x直接改變為這種圖片的標號
    if(count == 1) x = onlyPic;
    //當可用圖片已用完,則丟擲異常
    if(count == 0) throw new Exception("圖片可用數目已用完");
    //假如index對應的圖片種類的剩餘可生成數量為0,則重新隨機
    //此時可用圖片種類肯定不是1或0,上面的兩個if已經判斷並剔除
    if(picCount[x] == 0) {
        do {
            x = random.Next(8);
        } while(picCount[x] == 0);
    }
    switch(x) {
        case 0: {
                graph.setValue(index, 1);  //設定graph對應位置的值,值1代表圖片0
                picCount[0]--;
                return bm0;
            }
        case 1: {
                graph.setValue(index, 2);  //設定graph對應位置的值,值2代表圖片1
                picCount[1]--;
                return bm1;
            }
        case 2: {
                graph.setValue(index, 3);
                picCount[2]--;
                return bm2;
            }
        case 3: {
                graph.setValue(index, 4);
                picCount[3]--;
                return bm3;
            }
        case 4: {
                graph.setValue(index, 5);
                picCount[4]--;
                return bm4;
            }
        case 5: {
                graph.setValue(index, 6);
                picCount[5]--;
                return bm5;
            }
        case 6: {
                graph.setValue(index, 7);
                picCount[6]--;
                return bm6;
            }
        case 7: {
                graph.setValue(index, 8);
                picCount[7]--;
                return bm7;
            }
        default:
            throw new Exception("圖片種類標記無效");
    }
}

各個PictureBox點選時統一呼叫:

//輔助函式,不同pictureBox_Click事件可通過統一呼叫此函式,簡潔地完成其功能
public void pictureClicked(PictureBox pb,int index) {
    if(!inGame) return;  //非玩家可操控狀態,如在顯示連線且需要讓連線顯示一段時間的時候
    soundPlay("music\\click2.wav");
    clickCount++;  //點選次數+1;
    Point p = new Point(-1, -1);  //建立一個新的無效點
    p.setPoint(index);  //設定此點為 點選的點
    if(clickCount == 1) {  //第一次點選
        point1.setPoint(index);  //設定point1
        pb.BackColor = Color.LightGray;  //設定呼叫此函式的pictureBox的背景顏色,表示選中
    }
    if(clickCount == 2) {  //第二次點選
        inGame = false;  //進入非玩家可操控狀態,進行消除判斷和畫線等操作
        point2.setPoint(index);  //設定point2
        pb.BackColor = Color.LightGray;  //圖片選中
        //判斷是否能消除
        if(samePicture(point1, point2)) {  //兩點圖片相同才進行消除判斷
            if(checkOneLine(point1, point2)) eliminate = 1;  //一線消除
            else if(checkTwoLine(point1, point2)) eliminate = 2;  //二線消除
            else if(checkThreeLine(point1, point2)) eliminate = 3;  //三線消除
            //eliminate預設是0,即無消除狀態
        }
        //開始畫線
        if(eliminate == 1) drawOneLine(point1, point2);
        if(eliminate == 2) drawTwoLine(point1, point2);
        if(eliminate == 3) drawThreeLine(point1, point2);
        if(eliminate == 0) {  //沒有產生消除,不進入延遲,直接重置所有資訊
            soundPlay("music\\notElim.wav");  //消除失敗音效
            reset();
            return;
        }
        //有消除,進入延遲,讓消除線顯示一會兒,時間由delayTimer的Interval屬性來定
        delayTimer.Start();
    }
}

//PictureBox的點選事件呼叫此函式的格式:
private void pictureBox0_Click(object sender, EventArgs e) {
    if(inGame && (graph.getValue(0) > 0))  //假如在遊戲狀態,且此處有圖片
        pictureClicked(pictureBox0, 0);  //傳入本身,及其編號
}

delayTimer的Tick事件: