コンピュータと対戦できる!3目並べ(tic-toc-toe)をGo言語で実装してみよう
プログラミングの学習教材の例として、3目並べ(俗にいう○×ゲーム)をPythonで作成する記事を作成していましたが、今回は同じものをGo言語で書き直してみました。プログラムのアルゴリズムは、Pythonと全く同じなので詳細は以下の記事を参考にしてください。
3目並べとは
英語ではtic-tac-toeと呼ばれるゲームです。日本では○×ゲームといった方がわかりやすいかもしれません。
ルールは、2人のプレイヤーが3×3のマス目に交互にマーク(○または×)をつけていくというシンプルなものです。最初のプレイヤーは○か×を選び、次のプレイヤーは残ったマークを使います。プレイヤーは自分の番が来たら、グリッドの空いているマスに自分のマークをつけていきます。
勝利条件は簡単です。先に縦、横、または斜めのいずれかに自分のマークを3つ並べたプレイヤーが勝ちとなります。もし、全てのマスが埋まってもどちらも3つ並べることができなかった場合、そのゲームは引き分けとなります。
このゲームの魅力は、そのシンプルさにあります。子供でもすぐに理解できるルールでありながら、戦略を考える余地が十分にあるためコンピュータの思考ルーチンを考えるプログラミグ教材としても利用できます。
この記事では、プログラミング言語としてGo言語を使って3目並べを作っていきたいと思います。
Go言語による3目並べのコード
このプログラムですが、以下の手順で進めて行けば初心者〜中級者の学習に活用できます。
- ボードの表示、キーボード入力などの基本部分を作成する
- 勝敗の判定を加えて、人対人の対戦ができるプログラムにする
- コンピュータの思考ルーチンを考えて実装してみる
コンピュータの思考ルーチンは、現在の盤面+どちらのターンかを受け取って、置く場所を返すものにします。中身は自由に考えると楽しいです。 - 探索で手を読み切る思考ルーチンを作成してみる
①、②、③までは初級レベルでも可能だと思います。④は中級レベルのプログラムで結構考える必要があります。どこまで自力で作れるかでプログラミングスキルがある程度わかります。ぜひ、自力で作ってみてください。
なお、プログラムのアルゴリズムについては、以下の記事を参考にしてください。
参考コード
以下、コード全体になります。このプログラムは人対コンピュータの対戦ができるもので、コンピュータの思考ルーチンを4つ用意しています。
以下、各関数の処理です。
printBoard()
3マス並べの盤面を描画する関数です。マス目と(a,b,c)と(1,2,3)のラベルを表示します。getInput()
1a
といった形で、マスの位置をしていする入力を受け取る関数です。1a
でもa1
でもどちらでも受け取る様にしています。checkWin()
勝ちを判定する関数です。どちらかが勝っている場合は勝っている方(-1 or 1)を返します。どちらもまだ勝っていない場合は0を返します。
以下思考処理です
thinkSeq()
左上から見ていって、最初に置ける場所を返します。弱いです。thinkRandom()
まだ○も×も書かれていないマスを探して、ランダムに返します。これも弱いです。thinkStop2()
相手が2マス連続になっている場合(リーチしている場合)に、3マス目が置けない様に塞ぎます。リーチがなければ、ランダムに空いたマスを返します。そこそこ遊べます(乱数しだいで勝てます)think()
深さ優先探索を行なって、勝ち、または、引き分けにできるマスにコマをおきます。人間がミスしない限り、引き分けになります。
3マス並べは、両者が間違えずに打った場合、引き分けになります
package main
import (
"fmt"
"math/rand"
)
func printBoard(board [][]int) {
fmt.Println("--------------------")
fmt.Println(" a b c")
for i, row := range board {
fmt.Print(i+1, " ")
for _, v := range row {
switch v {
case 0:
fmt.Print(". ")
case 1:
fmt.Print("X ")
case -1:
fmt.Print("O ")
}
}
fmt.Println()
}
}
func getInput() (int, int) {
var s string
fmt.Scan(&s)
x, y := 0, 0
if '0' <= s[0] && s[0] <= '9' {
y = int(s[0]-'0') - 1
x = int(s[1] - 'a')
} else {
y = int(s[1]-'0') - 1
x = int(s[0] - 'a')
}
return x, y
}
func checkWin(board [][]int) int {
for i := 0; i < 3; i++ {
if board[i][0] != 0 && board[i][0] == board[i][1] && board[i][0] == board[i][2] {
return board[i][0]
}
if board[0][i] != 0 && board[0][i] == board[1][i] && board[0][i] == board[2][i] {
return board[0][i]
}
}
if board[0][0] != 0 && board[0][0] == board[1][1] && board[0][0] == board[2][2] {
return board[0][0]
}
if board[0][2] != 0 && board[0][2] == board[1][1] && board[0][2] == board[2][0] {
return board[0][2]
}
return 0
}
// 最初に見つけた場所に置く
func thinkSeq(board [][]int) (int, int) {
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if board[i][j] == 0 {
return j, i
}
}
}
return 0, 0
}
type pos struct {
x, y int
}
// ランダムに置く場所を決める
func thinkRandom(board [][]int) (int, int) {
p := []pos{}
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if board[i][j] == 0 {
p = append(p, pos{j, i})
}
}
}
sel := rand.Intn(len(p))
return p[sel].x, p[sel].y
}
// 負けない様ににする
func thinkStop2(boaard [][]int, turn int) (int, int) {
fmt.Println("turn", turn)
p := []pos{}
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if boaard[i][j] != 0 {
continue
}
boaard[i][j] = -turn
if checkWin(boaard) == -turn {
boaard[i][j] = 0
return j, i
}
boaard[i][j] = 0
p = append(p, pos{j, i})
}
}
sel := rand.Intn(len(p))
return p[sel].x, p[sel].y
}
// 深さ優先探索で、最終的な勝者を返す
func think(board [][]int, turn int) (int, int, int) {
// 9マス全てが埋まっているかどうか確認
cnt := 0
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if board[i][j] == 0 {
cnt++
}
}
}
if cnt == 0 {
return -1, -1, 0 // 全て埋まっていればDRAW
}
x, y := 0, 0
win := false
res := -turn
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if board[i][j] != 0 {
continue // 既に置かれている場合はスキップ
}
board[i][j] = turn
winner := checkWin(board)
if winner == turn {
board[i][j] = 0
return j, i, turn
}
_, _, r := think(board, -turn)
board[i][j] = 0
if r == turn { // 自分が勝つ場合は、その手を覚える
win = true
x, y = j, i
res = turn
}
if win == false { // 自分の価値が決定してない場合
if r == 0 { // 引き分けの場合は、手を覚える
x, y = j, i
res = 0
}
if r == -turn && res == -turn { //現在まだ引き分け以上の手が見つかっていなければ、場所を記録
x, y = j, i
}
}
}
}
return x, y, res
}
func main() {
board := [][]int{
{0, 0, 0},
{0, 0, 0},
{0, 0, 0},
}
for i := 0; i < 9; i++ {
printBoard(board)
winner := checkWin(board)
if winner != 0 {
fmt.Println("--------------------")
if winner == 1 {
fmt.Println("Winner is player1")
}
if winner == -1 {
fmt.Println("Winner is player2")
}
fmt.Println("--------------------")
return
}
var x, y int
if i%2 == 0 {
for true {
fmt.Print("Enter position(ex. 1a):")
x, y = getInput()
if board[y][x] != 0 {
fmt.Println("already occupied. try again.")
} else {
break
}
}
} else {
// x, y = thinkSeq(board)
// x, y = thinkRandom(board)
// x, y = thinkStop2(board, 1)
x, y, _ = think(board, 1)
}
if i%2 == 0 {
board[y][x] = -1
} else {
board[y][x] = 1
}
}
printBoard(board)
fmt.Println("--------------------")
fmt.Println("Draw")
fmt.Println("--------------------")
}
PythonからGo言語への移植はそれほど難しくありません。戻り値も複数個受け取ることができるので、pythonで書いたプログラムまんまでOKです。
まとめ
Go言語でも3目並べのプログラムを作成してみました。結構簡単なプログラムですが、深さ優先探索まで行う場合はそれなりのスキルが必要になるため、初級〜中級者までが練習用にプログラムする素材としてはちょうど良い気がします。