Python プログラムで動かすフェアリーチェスアプリ開発、連載第8回です。
はで画像を読み込み、盤面上に駒を表示しました。今回はマウス操作で駒を動かせるようにしていきます。
完成イメージ
駒をクリックすると移動可能なマスが表示され、移動先をクリックするとそこに移動するようにします。
丸の描画
駒をクリックしたら丸を表示するので、丸を描画する関数を定義します。
utils.py1from math import pi, sin, cos23...45def circle(x, y, opponent, r=0.25):6'''7円を描画する89Parameters10----------11x, y : float12中心の座標.13opponent : bool14True のとき,赤色で描画する.15r : float, default 0.2516半径.17'''18glPushMatrix() # 変形範囲の開始19glTranslate(x, y, 0) # 平行移動2021# 色の指定22if opponent:23glColor(1.0, 0.5, 0.5, 0.7) # 赤24else:25glColor(0.5, 0.5, 1.0, 0.7) # 青2627# 円の描画28glBegin(GL_POLYGON) # 多角形の描画29for k in range(12):30xr = r * cos(2 * pi * k / 12)31yr = r * sin(2 * pi * k / 12)32glVertex(xr, yr, 0)33glEnd()3435glPopMatrix() # 変形範囲の終了
円の描画としていますが、実は正十二角形を描画しているだけです。
数学の話になりますが、三角関数の引数は
で表されるので、360 度は円周率の 2 倍の値()になります。
角度と座標の関係は次の図のようになるので、
30-31 行目のような式が出来上がります。
マウスポインタ座標の取得
OpenGL では、glutMouseFunc()
にユーザ定義の関数を登録しておけば、
マウスのどのボタンが押され離されたか、そのときのマウスポインタの位置はどこかなどの情報を取得できます。
マウスで動作を制御できるようになるので、キーボードからコマンドを入力するための関数や、
盤面が画面に表示されるので、コマンドラインに盤面を表示する関数は消してしまいます。
(赤は削除行です。可読性のため、キャメルケース CamelCase からスネークケース snake_case に変更するなど、表記を変えた部分があります。)
main.py1class Game:2def __init__(self):3...4# マウスポインタの位置5self.mousepos = [-1.0, -1.0]6# 行先の指定7self.select_dest = False8# 始点・終点9self.startpos, self.endpos = (None, None), (None, None)10...11...12def main(self):13self.printBoard()14print(self.message)15self.message = ""16startpos, endpos = self.parseInput()17...18startpos, endpos = self.startpos, self.endpos19if None not in startpos + endpos:20...21...22def parseInput(self):23try:24a,b = input().split()25a = ((ord(a[0])-97), int(a[1])-1)26b = (ord(b[0])-97, int(b[1])-1)27print(a,b)28return (a,b)29except:30print("error decoding input. please try again")31return((-1,-1),(-1,-1))3233def printBoard(self):34print(" 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |")35for i in range(0,8):36print("-"*32)37print(chr(i+97),end="|")38for j in range(0,8):39item = self.gameboard.get((i,j)," ")40print(str(item)+' |', end = " ")41print()42print("-"*32)4344def parse_mouse(self):45'''マウスポインタの位置から指定したマス目を出力'''46a, b = self.mousepos47file_, rank = None, None48for i in range(8):49if abs(a - i) < 0.5: file_ = i50for i in range(8):51if abs(b - i) < 0.5: rank = i52return (file_, rank)5354...5556def mouse(self, button, state, x, y):57'''58マウス入力コールバック5960Parameters61----------62button : GLUT_LEFT_BUTTON, GLUT_MIDDLE_BUTTON, GLUT_RIGHT_BUTTON or int > 0, 1, 263マウスボタン.64GLUT_LEFT_BUTTON, 0 -- 左65GLUT_MIDDLE_BUTTON, 1 -- 中66GLUT_RIGHT_BUTTON, 2 -- 右67state : GLUT_DOWN, GLUT_UP or int > 0, 168ボタンの状態.69GLUT_DOWN, 0 -- 押された70GLUT_UP, 1 -- 離された71x, y : int72ウィンドウ座標.73'''74# ウィンドウ座標をワールド座標に変換する75self.mousepos = window2world(x, y, WSIZE)76# 左クリック77if (button == GLUT_LEFT_BUTTON78and state == GLUT_DOWN):79try:80# 行先選択81if (self.select_dest82and self.is_valid_move(self.gameboard[self.startpos],83self.startpos, self.parse_mouse(), self.gameboard)):84self.select_dest = False85self.endpos = self.parse_mouse()86# 駒選択87elif self.parse_mouse() in self.gameboard:88self.startpos, self.endpos = (None, None), (None, None)89self.select_dest = True90self.startpos = self.parse_mouse()91except KeyError: pass9293glutPostRedisplay() # 再描画9495...9697def glmain(self):98...99glutMouseFunc(self.mouse) # マウス入力コールバック100...
クリック操作をするたびにmouse()
メソッドが呼び出されて処理をします。
parse_mouse()
で、クリック時のマウスポインタの位置をもとに盤面上のどのマスが指定されたかを出力し、
駒やその移動先を指定することができます。
予期せぬ場所がクリックされたときのために、try-except
で例外処理をしています。
glutPostRedisplay()
は画面の再描画を行う関数で、draw()
メソッドを呼び出します。
19 行目のif None not in startpos + endpos:
は、開始位置と終了位置の両方が指定されていることを保証するものです。
ウィンドウ座標をワールド座標に変換
mouse()
メソッドの引数として渡されるx, y
はウィンドウ座標です。
ウィンドウ座標は、表示されているウィンドウの左上を(0, 0)とし、ピクセルを単位とする座標系です。
つまり、ウィンドウのサイズを 600x400 だとすると、右下の座標は(600, 400)となります。
これに対して、実際に描画に使用されているのはワールド座標です。
ワールド座標はユーザが定義できます。
でglOrtho()
を使って指定しているのもワールド座標です。さて、実際にマウスポインタの位置を情報として使用するにあたっては、
ワールド座標に変換したほうが何かと便利です。
そこで、ウィンドウ座標をワールド座標に変換する関数を作ります。
utils.py1def window2world(x, y, wsize):2'''3ウィンドウ座標を世界座標に変換する45Parameters6----------7x, y : int8変換するもとの座標.9wsize : int10画面の大きさ.1112Returns13-------14list > [float, float]15変換先の座標.16'''17return [9*x / wsize - 1, 7 - (9*y / wsize - 1)]
この変換によって、左下端をクリックしたときには(-1, -1)という座標が、右上端をクリックしたときには(8, 8)という座標が取得できることになります。
移動可能なマスに丸を表示
さきほど定義した丸を描画する関数を使って、駒を指定したときにその駒が移動可能なマスに丸を表示するようにします。
まずは、指定された位置すべてに丸を描画する関数を定義します。
utils.py1def draw_available_moves(poslist, opponent=None):2'''動かせる位置を描画する34Parameters5----------6poslist : list > [(int, int), ...]7移動先の座標のリスト.8opponent : bool or None, default None9True のとき,赤色で描画する.10'''11for pos in poslist:12circle(*pos, opponent)
*pos
はアンパックというもので、pos
に格納されているデータのそれぞれをそのまま関数の引数に渡すために使っています。
これで、移動可能なマスの座標のリストを渡せばそこに丸を書いてくれるようになります。
main.py1def draw(self):2...3draw_pieces(self.gameboard, piece_ID)4# 可能な移動先の表示5if self.select_dest and None not in self.startpos:6piece = self.gameboard[self.startpos]7draw_available_moves(8[move for move in piece.available_moves(*self.startpos, self.gameboard, color=piece.color)9if self.is_valid_move(piece, self.startpos, move, self.gameboard)],10opponent=self.playersturn != piece.color)11...
条件文は、行先選択中であり、開始位置が指定されているということ。
それで、draw_available_moves()
のopponent
では、自分の手番ではないときに相手の駒を触ったときにTrue
となるような条件式を与えています。
ただ、8-9 行目の内包表記が面倒くさいです。
available_moves()
は移動先のリストを返しますが、チェック回避を考慮していないので、そのままでは使えません。
そのため、でチェック回避を考慮したis_valid_move()
を作ったのですが、
こちらは移動可能かどうかを判定するだけで、移動先の座標のリストを返すわけではありません。
なんでこうしたんだろう、とちょっと後悔。
このままにしておいても面倒なので、移動先の座標のリストを返すようにis_valid_move()
を書き換えます。
main.py1def is_valid_move(self, piece, startpos, endpos, gameboard):2if endpos in piece.availableMoves(*startpos, gameboard, color=piece.color):3# 盤面の複製4gameboardTmp = copy(gameboard)5# 複製した盤面の更新6self.renew_gameboard(startpos, endpos, gameboardTmp)7# チェック判定8if self.is_check(piece.color, gameboardTmp):9return False10else:11return True12else:13return False14def valid_moves(self, piece, startpos, gameboard):15'''16動ける位置を出力.味方駒上には移動不可.1718Parameters19----------20piece : obj21駒.22startpos : tuple > (int, int)23開始位置.絶対座標.24gameboard : dict > {(int, int): obj, ...}25盤面.2627Returns28-------29result : list > [(int, int), ...]30'''31result = piece.available_moves(*startpos, gameboard, color=piece.color)32# チェック回避のため動き縛り33result_tmp = copy(result)34for endpos in result_tmp:35gameboard_tmp = copy(gameboard)36self.renew_gameboard(startpos, endpos, gameboard_tmp)37if self.is_check(piece.color, gameboard_tmp):38result.remove(endpos)39return result
それに伴って関係各所の調整をします。
ところで、マウス操作では移動可能な場所をクリックしないと動作しないようにしているので、
main()
内部の条件式は省いても構いません。
main.py1def main(self):2...3if target:4print("found "+str(target))5if target.color != self.playersturn:6self.message = "you aren't allowed to move that piece this turn"7if self.is_valid_move(target, startpos, endpos, self.gameboard):8self.message = "that is a valid move"9...10if target and target.color == self.playersturn:11print("found "+str(target))12self.message = "that is a valid move"13...14...1516def draw(self):17...18if self.select_dest and None not in self.startpos:19piece = self.gameboard[self.startpos]20draw_available_moves(21[move for move in piece.available_moves(*self.startpos, self.gameboard, color=piece.color)22if self.is_valid_move(piece, self.startpos, move, self.gameboard)],23self.valid_moves(piece, self.startpos, self.gameboard),24opponent=self.playersturn != piece.color)25...26...2728def mouse(self, button, state, x, y):29...30# 左クリック31if (button == GLUT_LEFT_BUTTON32and state == GLUT_DOWN):33try:34# 行先選択35if (self.select_dest36and self.is_valid_move(self.gameboard[self.startpos],37self.startpos, self.parse_mouse(), self.gameboard)):38and self.parse_mouse() in self.valid_moves(39self.gameboard[self.startpos], self.startpos, self.gameboard)):40...
これでマウス操作で駒を動かせるようになりました!
これで結構ゲームっぽくなってきましたね。
ただ、駒の動きがカクカクしています。
はおまけ的な感じで、駒の動きをスムーズにしてみたいと思います。参考になったらうれしいです。
では