GoでCUIでマインスイーパ作った
実装方針
- コマンドライン入力によりマス数(行数×列数)を決定
- それぞれのマスは -1~18 の値を持つ (-1~8:非オープン状態、9~18:オープン状態)
- ランダムで地雷の位置を決定 (-1)、その周りのマスを+1する。
- 選択されたマスの周りに地雷がない時、地雷があるマスに隣接するマスまで一度にオープンする
- 操作のたびに画面をリフレッシュする
コード達
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%の時のゲームオーバー画面になります。