無聊的週末用Java寫個掃雷小遊戲

秦懷雜貨店發表於2022-01-17

週末無聊,用Java寫了一個掃雷程式,說起來,這個應該是在學校的時候,寫會比較好玩,畢竟自己實現一個小遊戲,還是比較好玩的。說實話,掃雷程式裡面核心的東西,只有點選的時候,去觸發更新資料這一步。

Swing 是過時了,但是好玩不會過時,不喜勿噴

原始碼的地址:https://github.com/Damaer/Gam...

下面講講裡面的設計:

  • 資料結構設計
  • 檢視和資料儘可能分開
  • 點選時候使用BFS掃描
  • 判斷成功失敗

資料結構設計

在這個程式裡面,為了方便,使用了全域性的資料類Data類來維護整個遊戲的資料,直接設定為靜態變數,也就是一次只能有一個遊戲視窗執行,否則會有資料安全問題。(僅僅是為了方便)

有以下的資料(部分程式碼):

public class Data {
    // 遊戲狀態
    public static Status status = Status.LOADING;
    // 雷區大小
    public static int size = 16;
    // 雷的數量
    public static int numOfMine = 0;
    // 表示是否有雷,1:有,0沒有
    public static int[][] maps = null;
    // 是否被訪問
    public static boolean[][] visited = null;
    // 周邊雷的數量
    public static int[][] nums = null;
    // 是否被標記
    public static boolean[][] flags = null;
    // 上次被訪問的塊座標
    public static Point lastVisitedPoint = null;
    // 困難模式
    private static DifficultModeEnum mode;
      ...
}

需要維護的資料如下:

  • 遊戲狀態:是否開始,結束,成功,失敗等等
  • 模式:簡單,中等或者困難,這個會影響自動生成的雷的數量
  • 雷區的大小:16*16的小方塊
  • 雷的數量:與模式選擇有關,是個隨機數
  • 標識每個方塊是否有雷:最基礎的資料,生成之後需要同步更新這個資料
  • 標識每個方塊是否被掃過:預設沒有掃過
  • 每個方塊周邊類雷的數量:生成的時候同步計算該結果,不想每次點選後再計算,畢竟是個不會更新的資料,一勞永逸
  • 標識方塊是否被標記:掃雷的時候我們使用小旗子標記方塊,表示這裡是雷,標識完所有的雷的時候,成功
  • 上次訪問的方塊座標:這個其實可以不記錄,但是為了表示爆炸效果,與其他的雷展示不一樣,故而記錄下來

檢視與資料分開

儘量遵循一個原則,檢視與資料或者資料變更分開,方便維護。我們知道Java裡面是用Swing來畫圖形介面,這個東西確實難畫,檢視寫得比較複雜但是畫不出什麼東西。

檢視與資料分開,也是幾乎所有框架的優秀特點,主要是方便維護,如果檢視和資料糅合在一起,更新資料,還要操作檢視,那就會比較亂。(當然我寫的是粗糙版本,只是簡單區分了一下)

在這個掃雷程式裡面基本都是點選事件,觸發了資料變更,資料變更後,呼叫檢視重新整理,檢視渲染的邏輯與資料變更的邏輯分開維護。

每個小方塊都新增了點選事件,Data.visit(x, y)是資料重新整理,repaintBlocks()是重新整理檢視,具體的程式碼就不放了,有興趣可以Github看看原始碼:

new MouseListener() {
                    @Override
                    public void mouseClicked(MouseEvent e) {
                        if (Data.status == Status.GOING) {
                            int c = e.getButton(); // 得到按下的滑鼠鍵
                            Block block = (Block) e.getComponent();
                            int x = block.getPoint_x();
                            int y = block.getPoint_y();
                            if (c == MouseEvent.BUTTON1) {
                                Data.visit(x, y);
                            } else if (c == MouseEvent.BUTTON3) {// 推斷是滑鼠右鍵按下
                                if (!Data.visited[x][y]) {
                                    Data.flags[x][y] = !Data.flags[x][y];
                                }
                            }
                        }
                        repaintBlocks();
                    }
}

這裡很遺憾的一點是每個方塊裡面還有一個背景的`url沒有抽取出來,這個是變化的資料,不應該放在檢視裡面:

public class Block extends JPanel {
    private int point_x;
    private int point_y;

    private String backgroundPath = ImgPath.DEFAULT;

    public Block(int x, int y) {
        this.point_x = x;
        this.point_y = y;
        setBorder(BorderFactory.createEtchedBorder());
    }
}

重新設定方塊背景,需要居中處理,重新繪製,重寫void paintComponent(Graphics g)方法即可:

    @Override
    protected void paintComponent(Graphics g) {
        refreshBackground();
        URL url = getClass().getClassLoader().getResource(backgroundPath);
        ImageIcon icon = new ImageIcon(url);
        if (backgroundPath.equals(ImgPath.DEFAULT) || backgroundPath.equals(ImgPath.FLAG)
                || backgroundPath.equals(String.format(ImgPath.NUM, 0))) {
            g.drawImage(icon.getImage(), 0, 0, getWidth(), getHeight(), this);
        } else {
            int x = (int) (getWidth() * 0.1);
            int y = (int) (getHeight() * 0.15);
            g.drawImage(icon.getImage(), x, y, getWidth() - 2 * x, getHeight() - 2 * y, this);
        }
    }

BFS掃描

BFS,也稱為廣度優先搜尋,這算是掃雷裡面的核心知識點,也就是點選的時候,如果當前方塊是空的,那麼就會觸發掃描周邊的方塊,同時周邊方塊如果也是空的,會繼續遞迴下去,我用了廣度優先搜尋,也就是先將它們放到佇列裡面,取出來,再判斷是否為空,再將周邊符合的方塊新增進去,進行一一處理。

廣度優先搜尋在這裡不展開,其本質是優先搜尋與其直接關聯的資料,也就是方塊周圍的點,這也是為什麼需要佇列的原因,我們需要佇列來儲存遍歷的順序。

    public static void visit(int x, int y) {
        lastVisitedPoint.x = x;
        lastVisitedPoint.y = y;
        if (maps[x][y] == 1) {
            status = Status.FAILED;
            // 遊戲結束,暴露所有的雷
        } else {
            // 點選的不是雷
            Queue<Point> points = new LinkedList<>();
            points.add(new Point(x, y));
            while (!points.isEmpty()) {
                Point point = points.poll();
                visited[point.x][point.y] = true;
                if (nums[point.x][point.y] == 0) {
                    addToVisited(points, point.x, point.y);
                }
            }
        }
    }

    public static void addToVisited(Queue<Point> points, int i, int j) {
        int x = i - 1;
        while (x <= i + 1) {
            if (x >= 0 && x < size) {
                int y = j - 1;
                while (y <= j + 1) {
                    if (y >= 0 && y < size) {
                        if (!(x == i && j == y)) {
                            // 沒訪問過且不是雷
                            if (!visited[x][y] && maps[x][y] == 0) {
                                points.add(new Point(x, y));
                            }
                        }
                    }
                    y++;
                }
            }
            x++;
        }
    }

值得注意的是,周邊的點,如果它的周邊沒有雷,那麼會繼續擴充,但是隻要周邊有雷,就會停止擴充,只會顯示數字。

判斷成功失敗

當挖到雷的時候,就失敗了,同時會將所有的雷暴露出來,為了展示我們當前挖到的點,有爆炸效果,我們記錄了上一步操作的點,在重新整理檢視後,彈窗提示:

image-20211229091811385

判斷成功則需要將所有的雷遍歷一次,判斷是否被標記出來,這是我簡單想的規則,忘記了掃雷是不是這樣了,或者可以實現將其他所有非雷區都挖空的時候,成功,也是可以的。

總結

掃雷,一個簡單的遊戲,無聊的時候可以嘗試一下,但是JavaSwing真的難用,想找一個資料驅動檢視修改的框架,但是貌似沒有,那就簡單實現一下。其實大部分時間都在找圖示,測試UI,核心的程式碼並沒有多少。

在這裡推薦一下icon網站:https://www.iconfont.cn/,即使是沒有什麼技術含量的掃雷,寫一下還是挺有趣的。

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,個人網站:http://aphysia.cn,技術之路不在一時,山高水長,縱使緩慢,馳而不息。

劍指Offer全部題解PDF

開源程式設計筆記

相關文章