●●●●●●●● ●●●●●●●● ●●●●●●●● ●●●●●●●● ●●●●●●●● ●●●●●●●● ●●●●●●●● ●●●●●●●● |
オセロという名のボードゲーム
ボードゲームは種類がたくさんありますが、ルールの規模が比較的小さいものにオセロ*1が挙げられます。
*1ゲーム名について諸説ありますが、知名度の高さで「オセロ」とそのルール*2を採用しています。
検索結果数 : Reversi game(約 1,470,000 件) / Othello game(約 4,860,000 件)
*2石の初期配置をクロス、色は黒白の 2 色、先手は黒番とします。
前稿の会話プログラムを使って、オセロの対戦ゲームを作ります。
近年ではコンピュータでの対戦オセロというと、相手がコンピュータのこともありますが、ここで扱うのは人間同士の対戦です。
プログラムの簡便さを優先させているため、自動進行*3の機能はありません。相手石の読み込みやパスは手動で実施してください。
*3相手の打った石を自動的に読み込んだり、パスをする機能のこと。
0-1. この章では、python を使います。
インストールがまだなら、ここ (Windows10) を参考にしてインストールしておいてください。0-2. この章では、テキストエディタを使います。
Rocky なら既に OS と一緒に入っています。
Linux なら付属の「Vim」や「gedit」で十分です。0-3. 未経験者向けの情報を省いています。
(Vim の初心者向け使い方はここにあります)
Windows なら「メモ帳」でも構いませんが、本格的なテキストエディタをお勧めします。
(強力なアンドゥや、キーマクロが使えるようになります)
インストールがまだなら、ここ (秀丸エディタ) を参考にしてインストールしておいてください。
また、プログラムを起動するには拡張子が重要となるので、エクスプローラーで拡張子が表示されるようにしておいてください。
プログラミング未経験の方は、予めこちら (Python であそぼう 1) を学習しておくことを推奨します。0-4. ひとつの PC で Python を複数起動します。
狭い画面でも実行できますが、一度に複数のプログラムを起動するので、見易くするためには広い画面を用意してください。
1. 前回 Python であそぼう 7 - 第 2 項 のおさらい
1-1. 複数で会話するプログラム
<テキスト22> では以下の様に記述していました。
これを変更していきます。
<テキスト22> ファイル名「user2.py」
import time import comm comm.Init('user2', './cardstand/') # 自分の識別名と伝言台を指定。 comm.Quiet() while True: # メッセージ送信内容の入力。 # 送りたいメッセージがなければ受信だけを行う。 to = input('誰へ?') if to == '.': # プログラムを終了するときは「.」を入力する。 print('終了します。') break if to != '': msg = input('内容?') else: msg = '' result, msgFrom, msgBody = comm.Talk(to, msg) if result == -1: # 発言なし。 print('問題なし。') elif result == 0: print('発言失敗。') elif result == 1: print('発言成功。') elif result == 2: # 発言の前にメッセージが到着していた。 print('メッセージ受信。From:' + msgFrom + ' ' + msgBody) if to != '': print('発言は失敗。')
2. 対戦オセロプログラム
2-1. オセロプログラムの作成
<テキスト23> ファイル名「black.py」(黒番=先手)2-2. オセロプログラムの実行
上記 <テキスト22> を変更して作成する。
*4黒番は 1、白番は 2 を設定してください。
myColor = 0 # 自分の石の色*4。(0:黒 / 1:白) revColor = 1 - myColor # 相手の色 (= 自分の反対色) を求める。 # 自分と相手の識別名を用意する。 commId = [ 'black', 'white' ] import time import comm import othellotool as ot comm.Init(commId[myColor], './cardstand/') # 自分の識別名と伝言台を指定。 comm.Quiet() # 盤面を用意する。 board = ot.SetBoard() # 石を置ける場所のリスト。 legalPlaces = [[],[]] # 0:黒番 / 1:白番 while True: # 石を置ける場所を探す。 legalPlaces[myColor ] = ot.ListLegalPlaces(myColor, board) # 黒番の分。 legalPlaces[revColor] = ot.ListLegalPlaces(revColor, board) # 白番の分。 myAvailLength = len(legalPlaces[myColor ]) # 自分の設置可能数。 revAvailLength = len(legalPlaces[revColor]) # 相手の設置可能数。 # 盤面を表示する。 ot.BoardDisplay(board, legalPlaces[myColor]) if myAvailLength == 0: if revAvailLength == 0: print('双方ともに石を置く場所がありません。終局します。') break else: print('石を置く場所がありません。パスしてください。') # メッセージ送信内容の入力。 # 送りたいメッセージがなければ受信だけを行う。 to = '' msg = input('内容?' + '-> ' + commId[revColor] + ' ') if msg == '.': print('終了します。') break myDL = [] if msg != '': myPy, myPx = ot.MsgToPos(msg) # 位置指定文字を数値に変換する。 if myPx == -1 or myPy == -1: print('置く石の位置は a1~h8 で指定してください。') elif ot.IsLegalPlace(legalPlaces[myColor], myPy, myPx) == -1: print('石を置けません。') else: to = commId[revColor] # 裏返せる相手石数を調査する。 myDL = ot.GetDirLength(board, myPy, myPx, myColor) result, msgFrom, msgBody = comm.Talk(to, msg) if result == -1: # 発言なし。 print('問題なし。') elif result == 0: print('発言失敗。') elif result == 1: print('発言成功。') if myDL.count(0) < 8: # 0 の個数が 8 未満。(= 0 以外がある → 自分の石を置くことができる) board[myPy][myPx] = myColor # 石を置く。 # 相手石を裏返す。 board = ot.RevDirLength(board, myPy, myPx, myColor, myDL) elif result == 2: # 発言の前にメッセージが到着していた。 print('メッセージ受信。From:' + msgFrom + ' ' + msgBody) if to != '': print('発言は失敗。') revPy, revPx = ot.MsgToPos(msgBody) if ot.IsLegalPlace(legalPlaces[revColor], revPy, revPx) != -1: revDL = ot.GetDirLength(board, revPy, revPx, revColor) board[revPy][revPx] = revColor # 相手石を置く。 # 自石を裏返す。 board = ot.RevDirLength(board, revPy, revPx, revColor, revDL) black, white = ot.CountingStone(board) # 黒、白それぞれの数を数える。 print('black:%d / white:%d' % (black, white))
comm.py と exLock.py は black.py と同じ場所へ置いてください。
<テキスト24> ファイル名「white.py」(白番=後手)
<テキスト23> との違いは「自分の石の色*4」の 1 行だけなので、ファイルをコピーして編集すると簡単ですが、
コピー&ペーストをしたい人のために掲載します。(キーボードに慣れている人は手で打ち込んでください)
myColor = 1 # 自分の石の色*4。(0:黒 / 1:白) revColor = 1 - myColor # 相手の色 (= 自分の反対色) を求める。 # 自分と相手の識別名を用意する。 commId = [ 'black', 'white' ] import time import comm import othellotool as ot comm.Init(commId[myColor], './cardstand/') # 自分の識別名と伝言台を指定。 comm.Quiet() # 盤面を用意する。 board = ot.SetBoard() # 石を置ける場所のリスト。 legalPlaces = [[],[]] # 0:黒番 / 1:白番 while True: # 石を置ける場所を探す。 legalPlaces[myColor ] = ot.ListLegalPlaces(myColor, board) # 黒番の分。 legalPlaces[revColor] = ot.ListLegalPlaces(revColor, board) # 白番の分。 myAvailLength = len(legalPlaces[myColor ]) # 自分の設置可能数。 revAvailLength = len(legalPlaces[revColor]) # 相手の設置可能数。 # 盤面を表示する。 ot.BoardDisplay(board, legalPlaces[myColor]) if myAvailLength == 0: if revAvailLength == 0: print('双方ともに石を置く場所がありません。終局します。') break else: print('石を置く場所がありません。パスしてください。') # メッセージ送信内容の入力。 # 送りたいメッセージがなければ受信だけを行う。 to = '' msg = input('内容?' + '-> ' + commId[revColor] + ' ') if msg == '.': print('終了します。') break myDL = [] if msg != '': myPy, myPx = ot.MsgToPos(msg) # 位置指定文字を数値に変換する。 if myPx == -1 or myPy == -1: print('置く石の位置は a1~h8 で指定してください。') elif ot.IsLegalPlace(legalPlaces[myColor], myPy, myPx) == -1: print('石を置けません。') else: to = commId[revColor] # 裏返せる相手石数を調査する。 myDL = ot.GetDirLength(board, myPy, myPx, myColor) result, msgFrom, msgBody = comm.Talk(to, msg) if result == -1: # 発言なし。 print('問題なし。') elif result == 0: print('発言失敗。') elif result == 1: print('発言成功。') if myDL.count(0) < 8: board[myPy][myPx] = myColor # 石を置く。 # 相手石を裏返す。 board = ot.RevDirLength(board, myPy, myPx, myColor, myDL) elif result == 2: # 発言の前にメッセージが到着していた。 print('メッセージ受信。From:' + msgFrom + ' ' + msgBody) if to != '': print('発言は失敗。') revPy, revPx = ot.MsgToPos(msgBody) if ot.IsLegalPlace(legalPlaces[revColor], revPy, revPx) != -1: revDL = ot.GetDirLength(board, revPy, revPx, revColor) board[revPy][revPx] = revColor # 相手石を置く。 # 自石を裏返す。 board = ot.RevDirLength(board, revPy, revPx, revColor, revDL) black, white = ot.CountingStone(board) # 黒、白それぞれの数を数える。 print('black:%d / white:%d' % (black, white))
<テキスト25> ファイル名「othellotool.py」
·「black.py」「white.py」と同じフォルダーに置くこと。
# 盤面に初期の石を配置する。 def SetBoard(): return [ [-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1, 1, 0,-1,-1,-1], [-1,-1,-1, 0, 1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1], ]
# 位置指定文字を数値に変換する。例: "a1" -> [0, 0] def MsgToPos(msg): x = 'abcdefgh'.find(msg[0:1].lower()) y = '12345678'.find(msg[1:2]) return y, x
# 指定位置から色の連続数を求める。 def GetSeries(board, y, x, dy, dx, c): h = len(board) n = 0 while True: if not (0 <= y and y < h): break w = len(board[y]) if not (0 <= x and x < w): break if board[y][x] != c: break n += 1 y += dy x += dx return n
# 石を置いたときに裏返せる全方向の相手石数を調査する。 def GetDirLength(board, y, x, c): h = len(board) # 盤面の高さを求める。 w = len(board[0]) # 盤面の幅を求める。 revColor = 1 - c # 石の反対色を求める。 dirLength = [0] * 8 # 上を調査する。 n = GetSeries(board, y - 1, x + 0, -1, 0, revColor) if n > 0 and y - n > 0 and board[y - n - 1][x] == c: dirLength[0] = n # 右上を調査する。 n = GetSeries(board, y - 1, x + 1, -1, +1, revColor) if n > 0 and y - n > 0 and x + n < w - 1 and board[y - n - 1][ x + n + 1] == c: dirLength[1] = n # 右を調査する。 n = GetSeries(board, y + 0, x + 1, 0, +1, revColor) if n > 0 and x + n < w - 1 and board[y][x + n + 1] == c: dirLength[2] = n # 右下を調査する。 n = GetSeries(board, y + 1, x + 1, +1, +1, revColor) if n > 0 and y + n < h - 1 and x + n < w - 1 and board[y + n + 1][x + n + 1] == c: dirLength[3] = n # 下を調査する。 n = GetSeries(board, y + 1, x + 0, +1, 0, revColor) if n > 0 and y + n < h - 1 and board[y + n + 1][x] == c: dirLength[4] = n # 左下を調査する。 n = GetSeries(board, y + 1, x - 1, +1, -1, revColor) if n > 0 and y + n < h - 1 and x - n > 1 and board[y + n + 1][x - n - 1] == c: dirLength[5] = n # 左を調査する。 n = GetSeries(board, y + 0, x - 1, 0, -1, revColor) if n > 0 and x - n > 1 and board[y][x - n - 1] == c: dirLength[6] = n # 左上を調査する。 n = GetSeries(board, y - 1, x - 1, -1, -1, revColor) if n > 0 and y - n > 1 and x - n > 1 and board[y - n - 1][x - n - 1] == c: dirLength[7] = n return dirLength
# 相手石を裏返す。 # dl[] には 8 方向の石の数が入っていること。 def RevDirLength(board, py, px, bw, dl): # 上下左右増分の符号 deltaSign = [ [-1, 0], # 上側 [-1, +1], # 右上側 [ 0, +1], # 右側 [+1, +1], # 右下側 [+1, 0], # 下側 [+1, -1], # 左下側 [ 0, -1], # 左側 [-1, -1], # 左上側 ] # 相手石を裏返す。 for i in range(len(deltaSign)): sy, sx = deltaSign[i] for j in range(1, dl[i] + 1): board[py + j * sy][px + j * sx] = bw return board
# 石を設置できる場所を調査する。 def ListLegalPlaces(bw, board): lp = [] for y in range(len(board)): for x in range(len(board[y])): if board[y][x] == -1 and GetDirLength(board, y, x, bw).count(0) < 8: lp.append([y, x]) return lp
# 指定位置が設置可能リストに含まれるかどうかを返す。 # -1: 含まれない。 # 0~59: 含まれる。 def IsLegalPlace(lp, y, x): for p in range(len(lp)): ny, nx = lp[p] if ny == y and nx == x: return p return -1
# 盤面を表示する。 def BoardDisplay(board, lp): mark = '□○●' # Python は背景が黒の場合が多いので 黒=○ として表示する。 print(' a b c d e f g h') # 横のインデックスを表示する。 for y in range(len(board)): print('', y + 1, end=' ') # 縦のインデックスを表示する。 for x in range(len(board[y])): if IsLegalPlace(lp, y, x) > -1: print('・', end='') # 石を置ける場所を示す。 else: n = board[y][x] + 1 print(mark[n:n+1], end='') # 黒白の石を表示する。 print()
# 黒、白それぞれの数を数える。 def CountingStone(board): nBlack = 0 nWhite = 0 for i in range(len(board)): for j in range(len(board[i])): c = board[i][j] if c == 0: nBlack += 1 if c == 1: nWhite += 1 return nBlack, nWhiteblack.py, white.py の両方を起動してから対戦を実験してください。2-3. オセロプログラムの説明
(下記例では、読みやすくするために行間を広げています。実際は広がっていません)
プログラムで先手・後手の制限を設けていないので、どちらが先でも打ててしまいます。
また、相手の隙をついて連続で打つことも可能です。
動作確認のために、まずは基本ルールで対戦してください。
black(黒番=先手) を起動
>_ Windows PowerShell
Windows PowerShell Copyright (C) Microsoft Corporation. All rights reserved. 新しいクロスプラットフォームの PowerShell をお試しください https://aka.ms/pscore6 PS C:\Users\who> python3 black.py ⏎ a b c d e f g h 1 □□□□□□□□ 2 □□□□□□□□ 3 □□□・□□□□ 4 □□・●○□□□ 5 □□□○●・□□ 6 □□□□・□□□ 7 □□□□□□□□ 8 □□□□□□□□ 内容?-> white d3 ⏎ 発言成功。 a b c d e f g h 1 □□□□□□□□ 2 □□□□□□□□ 3 □□□○□□□□ 4 □□□○○□□□ 5 □□□○●・□□ 6 □□□□・・□□ 7 □□□□□□□□ 8 □□□□□□□□ 内容?-> white ⏎ メッセージ受信。From:white c3 a b c d e f g h 1 □□□□□□□□ 2 □□□□□□□□ 3 □・●○□□□□ 4 □□・●○□□□ 5 □□□○●・□□ 6 □□□□・□□□ 7 □□□□□□□□ 8 □□□□□□□□ 内容?-> white c4 ⏎ 発言成功。 a b c d e f g h 1 □□□□□□□□ 2 □・・□□□□□ 3 □・●○□□□□ 4 □□○○○□□□ 5 □□□○●・□□ 6 □□□□・・□□ 7 □□□□□□□□ 8 □□□□□□□□ 内容?-> white
経過が長いので省略します
a b c d e f g h 1 ○●●●●●●○ 2 ○○○○○○●○ 3 ○○○●○○●○ 4 ○○○○○○●○ 5 ○○○○○○●○ 6 ○○○○○○●○ 7 ○●●●●●●○ 8 ●○○○○●●○ 双方ともに石を置く場所がありません。終局します。 black:43 / white:21 PS C:\Users\who> exit ⏎white(白番=後手) を起動
>_ Windows PowerShell
Windows PowerShell Copyright (C) Microsoft Corporation. All rights reserved. 新しいクロスプラットフォームの PowerShell をお試しください https://aka.ms/pscore6 PS C:\Users\who> python3 white.py ⏎ a b c d e f g h 1 □□□□□□□□ 2 □□□□□□□□ 3 □□□□・□□□ 4 □□□●○・□□ 5 □□・○●□□□ 6 □□□・□□□□ 7 □□□□□□□□ 8 □□□□□□□□ 内容?-> black ⏎ メッセージ受信。From:black d3 a b c d e f g h 1 □□□□□□□□ 2 □□□□□□□□ 3 □□・○・□□□ 4 □□□○○□□□ 5 □□・○●□□□ 6 □□□□□□□□ 7 □□□□□□□□ 8 □□□□□□□□ 内容?-> black c3 ⏎ 発言成功。 a b c d e f g h 1 □□□□□□□□ 2 □□□・□□□□ 3 □□●○・□□□ 4 □□□●○・□□ 5 □□・○●□□□ 6 □□□・□□□□ 7 □□□□□□□□ 8 □□□□□□□□ 内容?-> black
経過が長いので省略します
a b c d e f g h 1 ○●●●●●●○ 2 ○○○○○○●○ 3 ○○○●○○●○ 4 ○○○○○○●○ 5 ○○○○○○●○ 6 ○○○○○○●○ 7 ○●●●●●●○ 8 ●○○○○●●○ 双方ともに石を置く場所がありません。終局します。 black:43 / white:21 PS C:\Users\who> exit ⏎2-3-1. プログラムの構成
プログラムは個別部 (主プログラム) と共通部 (主プログラム以外) の 2 種類に分かれます。2-3-2. プログラムの内容
プログラムの構成図 [1]個別部
「プロシージャ」とは、いくつかの命令・処理をひとつにまとめたもので、Python においては関数を指します。
主プログラム black.py white.py othellotool.py
便利プロシージャ群↑
|
↓SetBoard, BoardDisplay, MsgToPos,
GetDirLength, RevDirLength, CountingStone,
(GetSeries),
ListLegalPlaces, IsLegalPlace
↑
|
↓comm.py 抽象化ブロック Init, Quiet, Talk 実務ブロック SendMessage, RecvMessage, WipeMessage ↔ メッセージ 伝言台 ライブラリ exLock ↔ 排他制御
個別部は black.py と white.py で、プレイヤーごとに存在します。共通部
人間に近い部分で利便性を図るプログラムを記述しています。
相手の着手 (石を打つこと) を受け取り、自分の着手を相手に伝える部分です。
比較的自由度の高いプログラムになります。
ここでは、black.py と white.py の違いがほとんどありませんが、
同じプログラムにする必要はありません。
会話のルールさえ守っていれば、どのようにでも記述できます。
自分の着手をマウスで指定できるようにしたり、音を出す改造をしても良いかもしれません。
共通部は othellotool.py と comm.py, exLock.py です。
comm.py, exLock.py は前項と同じなのでそちらを参照してください。
othellotool.py はオセロにあると便利な副プログラムと外部関数を集めて記述しています。
将棋の達人の様に棋譜を頭の中だけで動かせる人であれば、無くても構わないプログラムです。
comm.py や exLock.py とは全く関係せず、個別部の利便性を図る目的のためだけに存在しています。
このプログラム群は、トップダウンとボトムアップを同時にスパイラルしながらも疎結合の割合を高くし、最終的にウォーターフォールにするという、割と難度の高い手法で組んでいます。日本人が最も不得意とする手法です。
肝となるのはパイププログラミング*5という手法です。この手法を骨子にして組むと、見通しの良いプログラムを記述できます。
*5UNIX のパイプをプログラムに応用したような手法です。30 年近く前に流行りかけたものの、理解できなかったのか重要視されなかったのか、業界に行き渡ることはありませんでした。いまでは Google に聞いても知らないようです。
その代わりフレームワーク等の、技術者のレベルが低くてもそれなりの保守性を確保できる環境が流行っています。
本稿の「個別部」は、共通部を適宜呼んでいる構造になっていることに加え、コメントに記述してあるので、説明の必要はないと思います。
なので「共通部」の othellotool.py について説明します。
(comm.py と exLock.py については前稿を参照してください)
すべて個別部から呼ばれます。
項番 プロシージャ名 説明 1 SetBoard 個別プログラムでは board[8][8] という配列変数を使って盤面の状況を管理します。
当プロシージャは、その盤面 borard[8][8] に初期状態として未着手(-1) / 黒石(0) / 白石(1) の状態を作成します。2 BoardDisplay 配列変数 board[8][8] がそのままでは読みにくいので記号で表示します。
また、石を置ける場所 (着手可能位置) も記号で表示します。
石を置ける場所は、第 2 引数の lp という配列変数で受け付けます。3 MsgToPos 位置指定文字列 (a1 等) を横軸・縦軸の数値に変換します。
横軸は a~h または A~H、縦軸は 1~8 の文字である必要があります。
規定外の文字列が渡された場合は -1 を返します。4 GetDirLength 指定された横・縦を中心にして周囲 8 方向に相手の石がいくつ連続しているかを調査します。
★の右側に○が 3 個連続している例。連続した相手の石のさらにひとつ向こうに自分の石があればその相手石の数を採用します。
★○○○□
相手の石が連続していても、その先に自分の石が存在しなければ、連続数を 0 にします。
例:
★の右側に○が 3 個連続しているが、その先に●がないので★に●は置けない。
右の連続数 → 0 とする。
★○○○□
例:
★の右側に○が 3 個連続しており、その先に●があるので★に●を置ける。
右の連続数 → 3 とする。
★○○○●
5 RevDirLength 上記 GetDirLength で作成された値を 8 方向分取り出し、その個数分を指定の石に変更します。
6 CountingStone 盤面の 2 次元配列 board[8][8] を参照し、黒石(0), 白石(1) それぞれの個数を求めます。
7 GetSeries 単一方向に並ぶ同じ石の数を数えます。
GetDirLength の中から呼び出されます。主プログラムから呼び出されることはありません。
(必要なら主プログラムから呼び出しても構わないが、本プログラムでは必要がない)
8 ListLegalPlaces 黒または白の着手可能位置を配列 lp にリストアップします。
この 1 次元配列 lp は、以下 2 つの目的に使用しています。
- 着手可能位置の表示
- パスの要否の判断 (→ 終局の判断)
9 IsLegalPlace 指定の横位置・縦位置が着手可能位置リストに含まれる場所を求めます。
着手の有効 / 無効を判別するために使用されます。
3. さらに対戦を実験
3-1. 他の実装と対戦する。この章は、ここで終了です。
会話のルールさえ守られていれば、Python ではない他のプログラムと対戦することができます。3-2. ネットワーク越しに対戦する。
こちらを参照して、異なる言語で作られた同じプログラムとの対戦を実験してください。
プログラム exLock.py は、排他制御にディレクトリを使用しているため
SMB (Windows で構成するファイル共有) や、NFS (Linux のファイル共有) でも動作できます。
他の PC やファイルサーバ (NAS とも呼ばれます) に共有フォルダーを設定し、
そこを伝言台にすることにより、自分以外の PC との対戦を実験してください。
(共有フォルダーの設定は、PC の持ち主や LAN の管理者に相談してください)
伝言台にする PC やファイルサーバで必ずしもユーザプログラムが動作する必要はありません。