Go言語で五目並べを作ってみた

はじめに

数か月前、Go言語で書かれたソースコードを読まなければいけなくなり A Tour of Go を読んで勉強したり、 実際にソースコードを読んだりしていました。 それ以来まったくGo言語を使っていなかったのですが、 最近Go言語を使ってみたいと思い、 A Tour of Goを読んで勉強しなおしました。(まだgoroutineの手前までしか読んでないけど)

何か作らないといろいろ忘れてしまいそうなので Go言語で五目並べを実装することにしました。 禁じ手などの実装が面倒なので、 禁じ手なし、5つ以上石を並べれば勝利のシンプルなルールで実装しました。

今回作ったものはこちらです。

実装

まず、盤面の状態を表現する型Boardを定義します。 盤面のサイズは15x15で固定で石は'X'と'O'、石がない場所は'-'で表現することにしたので 1次元のbyte型の配列を使いました。

const (
    Length      = 15
    Size        = 225
    ChainLength = 5
)

type Board [Size]byte

コンストラクNewBoardを定義しました。 配列をすべて'-'で初期化するだけです。

func NewBoard() *Board {
    var board Board
    for i := 0; i < Size; i++ {
        board[i] = '-'
    }
    return &board
}

石を指定した位置に置くメソッドPlaceを定義しました。

  • 石が'X'または'O'であるか
  • 指定された位置は盤面内か
  • 指定された位置に石がないか

をチェックし、 すべてのチェックが通ったら 配列の指定された位置に石を表現する文字を代入します。

func (b *Board) Place(stone byte, x, y uint8) error {
    if stone != 'O' && stone != 'X' {
        return InvalidStoneError(stone)
    }

    if x >= Length || y >= Length {
        return OutOfRangeError{x, y}
    }

    pos := x + y*Length

    if b[pos] != '-' {
        return AlreadyExistError{x, y}
    }

    b[pos] = stone
    return nil
}

最後に指定した位置の石を含む5つ以上の並びを見つけるメソッドIsChainを定義しました。

Go言語のMin関数はfloat64型にしか対応していなかったので 自分でuint8型のmin関数を定義しました。

shiftメソッドはposの位置からstepで指定した方向に同じ石が何個並んでいるか数える関数です。 引数maxIterで指定した回数以上数えないようにできます。

IsChainは縦・横・斜め4方向にshift関数を適用し、 同じ石が5つ並んでいる場所がひとつでもあればtrue、 ひとつもなければfalseを返します。 私はプログラム苦手なのでこんな方法しか思いつかないですが、 もっと効率の良い方法があるかもしれません。

func min(vals ...uint8) uint8 {
    minVal := vals[0]
    for _, val := range vals[1:] {
        if val < minVal {
            minVal = val
        }
    }
    return minVal
}

func (b *Board) shift(stone byte, pos uint8, step uint8, maxIter int) uint8 {
    var length uint8 = 0
    for i := 0; i < maxIter; i++ {
        pos += step
        if b[pos] != stone {
            break
        }
        length++
    }
    return length
}

func (b *Board) IsChain(x, y uint8) (bool, error) {
    if x >= Length || y >= Length {
        err := OutOfRangeError{x, y}
        return false, err
    }

    var (
        pos   = x + y*Length
        stone = b[pos]
    )

    if stone != 'O' && stone != 'X' {
        return false, InvalidStoneError(stone)
    }

    steps := [4]uint8{1, Length - 1, Length, Length + 1}
    for _, step := range steps {
        var (
            length         uint8 = 1
            maxShifts      uint8 = Length - 1
            shiftV, shiftH uint8
        )

        if step != 1 {
            shiftV = maxShifts - y
        } else {
            shiftV = Length
        }

        switch step % Length {
        case 0:
            shiftH = Length
        case 1:
            shiftH = maxShifts - x
        default:
            shiftH = x
        }

        maxIter := int(min(ChainLength-length, shiftV, shiftH))
        length += b.shift(stone, pos, step, maxIter)

        if shiftV != Length {
            shiftV = maxShifts - shiftV
        }

        if shiftH != Length {
            shiftH = maxShifts - shiftH
        }

        maxIter = int(min(ChainLength-length, shiftV, shiftH))
        length += b.shift(stone, pos, -step, maxIter)

        if length >= ChainLength {
            return true, nil
        }
    }

    return false, nil
}

上で説明した以外にもターミナルに表示するためのコードをいろいろ書きました。 \033[Gとか\033[Aみたいなエスケープシーケンス?といわれるものを使って 石を置くたびに盤面の表示を更新するようにしました。

完成

下のようなプログラムが完成しました。

f:id:stkns1024:20211219113059j:plain
できたもの

Go言語の良いところ

文法がシンプルで覚えやすかったです。 Go言語の文法を思い出すためにA Tour of GOを読みましたが、 数日でほとんど読み終わり、 すぐにコードを書き始めることができました。

この記事内で説明していない箇所で構造体の埋め込みを使ったのですが、 便利な機能だと思いました。

おわりに

Go言語でプログラムを書くのははじめてでしたが、 書いていて楽しかったので、 今後も使い続けてGo言語マスターを目指したいです。

五目並べのプログラムを作ったあと、 一緒に五目並べをしてくれる友達がいないことに気付いたので友達を作りたいです。