まあ着手可能な手からランダムにピックアップするだけのものですが。
ゲーム部分の実装が簡単であろうという理由から、今回の題材は将棋ではなくオセロです。ゲーム木うんぬんとか、その辺の基礎理論的なことの勉強も必要ですし、実装していく上でのノウハウとかも吸収していきたい。で、これらを同時に効率よく身につけていこうと考えた時に、将棋とかいう複雑なゲームを題材にするのはまだ早いでしょうよ?と思いました。まぁAIに限らず、って気はしますが。
で、以下がソースコードです。所要時間は2~3時間ほどでした。プレイヤーは存在せず、黒と白に同一の思考ルーチンを使って終局まで打たせるようにしています。完全にランダムに置くだけなので、AIっぽい処理はほぼないです。BoardをSubjectとしたObserverを作ることで棋譜の記録、及びそのエクスポート(Loggerクラス)をサポートしてます。この辺は以前の記事でやってたことがちょろっと活きてます。
今回オブジェクト指向っぽいことはObserverくらいです。カプセル化とかは一切考慮してないので、このコードを基に色々やろうとするといずれスパゲティになると思います。なのでこのコードは今回でほぼ廃棄。対局の終了条件とかも雑に判定してて、多分(空白のマスがある&両方これ以上置けないのケースとか)漏れてますし。まぁでも、こうしてちゃちゃっと書いてみたコードが実際に対局を行っている様子を見るのはとても満足です。こういうのがモチベーションて奴なんでしょうね。
#!/usr/bin/env python # -*- coding: utf-8 -*- import os,sys import random import string,re class CannotPut(Exception): pass class Cell: def __str__(self): return " " def value(self): return 0 class Empty(Cell): pass class Black(Cell): def __str__(self): return " o" def value(self): return 1 class White(Cell): def __str__(self): return " x" def value(self): return -1 class Board: def __init__(self): self.obs = [] self.b = [] for i in range(0,8): self.b.insert(len(self.b), [Empty()]*8) def initBoard(self): self.b[3][3] = Black() self.b[4][4] = Black() self.b[3][4] = White() self.b[4][3] = White() def __str__(self): s = "" for i in range(0,8): s += "---"*8 + "\n" s += "|" + "|".join([c.__str__() for c in self.b[i]]) + "|\n" s += "---"*8 + "\n" return s def notify(self,cmd): for o in self.obs: o.update(cmd) def registObs(self, o): if isinstance(o, BoardObserver): self.obs.insert(len(self.obs), o) return 1 class Put: def __init__(self, board, point, turn): self.board = board self.point = point # (x,y)self.reverses = [] self.turn = turn if not isinstance(board.b[point[0]][point[1]], Empty): raise CannotPut, "Already putting" rel = [(i,j) for i in range(-1,2) for j in range(-1,2)] for i,elm in enumerate(rel): if(rel[i]==(0,0)): del rel[i] break org = self.point for v in rel: for i in range(1,8): tgtx = org[0]+v[0]*i tgty = org[1]+v[1]*i try: if(self.board.b[tgtx][tgty].value() == self.turn): if(i==1): break self.reverses.insert(len(self.reverses), (tgtx,tgty)) break elif(self.board.b[tgtx][tgty].value() == 0): break else: continue except IndexError,e: break if(self.reverses == []): raise CannotPut, "Exception: Cannot put on given position" print self.board return def execute(self): c = Empty() if self.turn==1: c = Black() else: c = White() self.board.b[self.point[0]][self.point[1]] = c for rel in self.reverses: v = (rel[0] - self.point[0], rel[1] - self.point[1]) print "Vector: "+ v.__str__() getbase = lambda x: 0 if x==0 else x/abs(x) base = (getbase(v[0]), getbase(v[1])) border = max( abs(v[0]), abs(v[1]) ) for i in range(1, border): p = (base[0]*i, base[1]*i) p = (self.point[0]+p[0], self.point[1]+p[1]) self.board.b[p[0]][p[1]] = c #print self.reverses #print self.board self.board.notify(self) def undo(self): pass class BoardObserver: def update(self, cmd): pass class Logger(BoardObserver): def __init__(self): self.log = [] def update(self, cmd): self.log.insert(len(self.log), cmd) def export(self): s = "" for i,cmd in enumerate(self.log): s += str(i) + ": "+str(cmd.point) + "\n" return s def think(board,turn): nonfill = False for i in range(0,8): for j in range(0,8): if isinstance(board.b[i][j], Empty): nonfill = True break if not nonfill: print "Game End." return -1 available = [] for i in range(0,8): for j in range(0,8): try: cmd = Put(board, (i,j), turn) available.insert(len(available), cmd) except CannotPut,e: continue if(available==[]): print "Game end." return -1 idx = random.randint(0, len(available)-1) available[idx].execute() print board t = "Black's" if turn==-1 else "White's" print t+" turn.\n" return 1 def main(): board = Board() board.initBoard() logger = Logger() board.registObs(logger) print board print "\n" """ cmd = Put(board, (5,3), 1) cmd.execute() cmd = Put(board, (5,2), -1) cmd.execute() """ #print "You are Black" #cmd = Put(board, (5,3), 1) #cmd.execute() #print board turn = 1 while(True): ret = think(board,turn) if ret==-1: break turn *= -1 b=0 w=0 for i in range(0,8): for j in range(0,8): if isinstance(board.b[i][j], Black): b+=1 elif isinstance(board.b[i][j], White): w+=1 print "Black: "+str(b) print "White: "+str(w) if b>w: print "Black Win." elif w>b: print "White Win." else: print "Draw Game." print "\n*** logger ***" print logger.export() if __name__=="__main__": main()
実行結果は以下。対局中の出力(現在の局面)と、終局後の棋譜の出力とで2つあるので別々に出します。
------------------------ | | | | | | | | | ------------------------ | | | | | | | | | ------------------------ | | | | | | | | | ------------------------ | | | | o| x| | | | ------------------------ | | | | x| o| | | | ------------------------ | | | | | | | | | ------------------------ | | | | | | | | | ------------------------ | | | | | | | | | ------------------------ Vector: (2, 0) ------------------------ | | | | | | | | | ------------------------ | | | | | | | | | ------------------------ | | | | | o| | | | ------------------------ | | | | o| o| | | | ------------------------ | | | | x| o| | | | ------------------------ | | | | | | | | | ------------------------ | | | | | | | | | ------------------------ | | | | | | | | | ------------------------ White's turn. ... 中略 ... ------------------------ | o| o| o| o| x| x| o| o| ------------------------ | o| o| x| o| o| x| o| o| ------------------------ | o| o| x| o| o| o| x| x| ------------------------ | o| o| x| x| x| x| x| x| ------------------------ | o| x| x| o| o| o| x| x| ------------------------ | o| x| x| x| o| x| o| x| ------------------------ | o| o| o| o| x| o| o| o| ------------------------ | o| o| o| x| x| x| x| o| ------------------------ Black's turn. Game End. Black: 37 White: 27 Black Win.
そして棋譜部分。
*** logger *** 0: (2, 4) 1: (2, 5) 2: (3, 5) 3: (4, 5) ... 中略 ... 57: (5, 7) 58: (6, 5) 59: (3, 7)
※座標にはタテヨコの区別がありますが、棋譜出力の際とかにその辺のことは考慮してません。将棋の表記である筋=>段と同じ順でオセロも表記しますが、もし局面の出力形式と整合をとるのであれば(最終手を例とすると)本当は(3,7)でなく(7,3)となります。もっとも、配列の添字である0~7番をそのまま出してるだけなのでそこからさらに+1する必要もありますし、「筋」にあたる所は正しくはアルファベット表記しなくてはなりません。ま、こまけぇことはいいんだよということです。
実際にコードを書いてみて思ったことは、Model部分と思考部分が独立になればいいなってことです。強いAIを作るには思考部分の高速化が必要なので、内部表現が変更される可能性は大いにあります。で、そういう高速化に重点を置いた内部表現とUI関係のコードはあまり相性がよくない気がします。Modelの表現を「自分」が扱える形式に変換するAdapterを書くことになるのでしょうが、「自分」に当てはまる要素はView側なのか、思考ルーチン側なのか。はたまた両方なのか。その辺は考える必要があるかもしれません。言い換えると、Modelの内部表現はUIとAIどっち寄りにすべきか?それともどっちにも寄らないようにすべきか?という所でしょうか。