ami_GS's diary

情報系大学院生の備忘録。ネットワークの勉強にハマっています。

GoでCUIでマインスイーパ作った

はじめに

こんにちは、Goで何か作りたいな〜と思っていたらマインスイーパができていたのでレポです。
全体のコードは長いので要点を載せて説明します。

実装方針

  1. コマンドライン入力によりマス数(行数×列数)を決定
  2. それぞれのマスは -1~18 の値を持つ (-1~8:非オープン状態、9~18:オープン状態)
  3. ランダムで地雷の位置を決定 (-1)、その周りのマスを+1する。
  4. 選択されたマスの周りに地雷がない時、地雷があるマスに隣接するマスまで一度にオープンする
  5. 操作のたびに画面をリフレッシュする

コード達

1, コマンドライン入力によりマス数(行数×列数)を決定
単純明快、パッケージを使えば実現可能です。

        var input string
        var field *Field
        var h, w, m int
set:
        fmt.Printf("Input height, width, (num of mine) (e.g : 8,8(,9))\n>> ")
        fmt.Scanln(&input)
        pos := strings.Split(input, ",") //入力を分割
        if len(pos) == 2 || len(pos) == 3 {
                w, _ = strconv.Atoi(pos[0])
                h, _ = strconv.Atoi(pos[1])
                if len(pos) == 2 {
                        // 地雷数が指定されていない場合はマス数の25%を地雷に
                        m = w * h / 4
                } else {
                        m, _ = strconv.Atoi(pos[2])
                }
                if w == 0 || h == 0 || m == 0 {
                        fmt.Println("Please input 2 or 3 numerical values (value > 0)")
                        goto set //不適切な入力の時、setまでgoto
                }
                field = NewField(byte(h), byte(w), byte(m)) //地雷原構造体を生成
        } else {
                fmt.Println("Please input 2 or 3 numerical values (value > 0)")
                goto set
        }


2, それぞれのマスは -1~18 の値を持つ

type Field struct {
           width  byte
           height byte
           state  [][]int  // 地雷原のもつ値、-1 ~ 18 が入る
}


3, ランダムで地雷の位置を決定 (-1)、その周りのマスを+1する。
実装の簡単のため、入力された行数+2、列数+2の地雷原を生成し、ダミーとして使います。

func NewField(width, height, mineNum byte) *Field {
        field := &Field{width, height, [][]int{}}
        field.state = make([][]int, height+2)
        for i := 0; i < int(height)+2; i++ {
                field.state[i] = make([]int, width+2)
        }

        //生成した地雷の場所を一旦保持
        var pos [][2]byte = make([][2]byte, mineNum)
        // [0 width*height) をランダムに並び替え
        idx := rand.Perm(int(width * height))
        for i := 0; i < int(mineNum); i++ {
                //地雷数分 idx の先頭から取り出す
                pos[i] = [2]byte{(byte(idx[i]) / width) + 1, (byte(idx[i]) % width) + 1}
                // 地雷の周りを+1する
                field.state[pos[i][0]-1][pos[i][1]-1] += 1
                field.state[pos[i][0]-1][pos[i][1]] += 1
                field.state[pos[i][0]-1][pos[i][1]+1] += 1
                field.state[pos[i][0]][pos[i][1]-1] += 1
                field.state[pos[i][0]][pos[i][1]+1] += 1
                field.state[pos[i][0]+1][pos[i][1]-1] += 1
                field.state[pos[i][0]+1][pos[i][1]] += 1
                field.state[pos[i][0]+1][pos[i][1]+1] += 1
        }
        for i := 0; i < int(mineNum); i++ {
                //地雷を置く(-1)
                field.state[pos[i][0]][pos[i][1]] = -1
        }
        return field
}

4, 選択されたマスの周りに地雷がない時、地雷があるマスに隣接するマスまで一度にオープンする
RecursiveOpen(row, column)の中身は大したこと無いのに長ったらしいので省略します。

func (self *Field) Open(row, column byte) {
        // オープン => マスに +10
        self.state[row][column] += 10
}

func (self *Field) Choose(row, column byte) (gameover bool) {
        // ダミーマスの関係上、1オリジン
        gameover = false
        if 0 == self.state[row][column] {
                // 周りに地雷がない時、再帰的にオープンする
                self.RecursiveOpen(row, column)
        } else if 0 < self.state[row][column] && self.state[row][column] <= 8 {
                //周りに地雷がある時、そこだけをオープンする
                self.Open(row, column)
        } else if self.state[row][column] == -1 {
                //地雷がある時、全てをオープンし、ゲームオーバーフラグを返す
                self.AllOpen()
                return true
        }
        return
}

5, 操作のたびに画面をリフレッシュする
操作(どこをオープンするかの入力)ごとにFieldString()を呼び出し、画面をリフレッシュします。
"\x1b[2J"という文字列が画面をクリアしてくれるので、それを利用します。

func (self *Field) FieldString() string {
        // headerに列数を入力
        header := " "
        for len(header) < int(math.Log10(float64(self.height)))+2 {
                header += " "
        }
        for c := 0; c < int(self.width); c++ {
                tmp := fmt.Sprintf(" %d", c+1)
                for len(tmp) < 4 {
                        tmp += " " // TODO: here should be optimized
                }
                header += tmp
        }

        // 行数とマスを入力
        field := fmt.Sprintf("%s\n", header)
        for r := 1; r < int(self.height)+1; r++ {
                // 行数
                tmp := fmt.Sprintf("%d", r)
                for len(tmp) < int(math.Log10(float64(self.height)))+2 {
                        tmp += " "
                }
                field += tmp

                // マス
                for c := 1; c < int(self.width)+1; c++ {
                        if -1 <= self.state[r][c] && self.state[r][c] <= 8 {
                                field += CLOSED // "[ ]"
                        } else if self.state[r][c] == 10 {
                                field += OPENED // "___"
                        } else if 10 < self.state[r][c] {
                                // _1_ ~ _8_
                                field += OPEN_NUM[self.state[r][c]-11]
                        } else if self.state[r][c] == 9 {
                                field += MINE // "_*_"
                        }
                        field += " "
                }
                if r < int(self.height) {
                        field += "\n"
                }
        }
        // "\x1b[2J"で画面をリフレッシュ(クリア)
        return fmt.Sprintf("\x1b[2J%s>> ", field) 
}

最後に

これでコアな部分は完成しました!
あとは全体のコードを参照してください!プルリクエスト大歓迎です!
以下が 15行,15列,地雷数25%の時のゲームオーバー画面になります。
f:id:ami_GS:20150531235443p:plain