Twitterでも言いましたが、pythonを勉強してプログラムを自作してみました。
- 詳しい方に添削して欲しい
- 気になる方へのコードの開示
- 自分の理解を深める
などの理由でブログにまとめておきます。
プログラミングが分からない方にもある程度理解できる記事だと思います。
Pythonって?
pythonはプログラミング言語の一つです。
最近、話題の言語のようで、プログラミング言語初心者でもとっつきやすいものでした。
トップオセラーのうみがめさんが使っているみたいなので真似して勉強してみました。
どんなプログラムを作ったん?
棋譜(f5d6c3...)からOX形式にするプログラムです。
どんな感じで進むかというと、
入力
棋譜を入力してください:
の後に、棋譜を入力します。
例えば、f5d6c3(虎定石)と入力します。
棋譜を入力してください:f5d6c3
出力
すると、次のように出力されます。
一列にした文字列:
------------------X--------XX------OXX-----O--------------------
8×8で視覚的に表示:
--------
--------
--X-----
---XX---
---OXX--
---O----
--------
--------
黒がX、白がO、何もないマスが-です。
虎定石になってますよね。
作った目的
先程出した「------------------X--------XX------OXX-----O--------------------」という文字列が先述したOX形式です。
このブログで使っているhamliteの初期配置を設定するのにこの形式が必要なので、棋譜から作れるプログラムを作りました。
これが目的です。
プログラムのコード
ひとまずコードを書いておきます。
この後に、工夫した点や全体の流れを書くのでまずはサラッと見てもらえればと思います。
(スマホは飛び出すので横画面表示にしてね。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
#棋譜(f5d6)から○×形式に変換 import numpy as np import sys #1.座標から番号への変換 #scoreは棋譜の文字列(f5d6c3...) score = input('棋譜を入力してください:') #scoresは盤面の位置情報を数字にしたもののリスト([57,65,34,...]) scores = [] #lは試合の合計手数 l = len(score) / 2 for i in range(int(l)): #棋譜がF5D6形式か確認。 if not 65<= ord(score[2*int(i)]) <=72 or not 1<= int(score[2*int(i)+1]) <=8 : print("エラー: \n入力された棋譜が適切でありません。\n棋譜はF5D6形式で入力してください。") sys.exit() #locationの計算で、文字情報から数字へ変換。 location = 10 * int(score[2*int(i)+1]) + ord(score[2*int(i)]) - 63 scores.append(location) #2.棋譜に従って、石を返していく。 #2-1.colorsにその時点での盤面(10×10)を収納して管理。 #無は0,黒は1,白は2。 #初期配置を代入。 colors = [] for i in range(100): if i+1==46 or i+1==55: colors.append(1) elif i+1==45 or i+1==56: colors.append(2) else: colors.append(0) #2-2.石を返していく。 #パスの回数を記録。 pass_count=0 #n(厳密にはn+1)は何手目かを表す。 for n in range(int(l)): #loctionは(n+1)手目に置いた場所 location = scores[n] #置いた場所にすでに置かれている場合、エラー。 if not colors[location-1] == 0 : print("エラー: \nすでに埋まったマスに置かれました。") sys.exit() #pass_judgはパスかどうかの証。0のままならパス、1なら少なくとも一石返している。 pass_judg = 0 #turnは手番(黒は0,白は1) #通常は(n%2+パスの回数)%2で判断できるが、 #パスになる時はturnを入れ替えてもう一度探索する。 turn =((n%2) + pass_count)%2 #loop_countは次のループの回数。無限ループの回避のため。 loop_count=0 while pass_judg==0 and loop_count<2 : #xyは探索する方向を表すベクトル([1,1]が右下,[-1,0]は左) xy = [(1,1),(1,0),(1,-1),(0,1),(0,-1),(-1,1),(-1,0),(-1,-1)] #8方向を探索 for i in range(8): A=[1,10] B=xy[i] #searchの色で判断 #np.dot(A,B)は行列A,Bの積。 search=location + np.dot(A,B) #手番と同色または、無の時、返らないので別の方向へ #(turn+1)は常に手番と同色を表す。 if colors[search-1]==0 or colors[search-1]==turn+1: continue #手番と異色の時、その先に同色があるか探索。 else: #kはこの次のループを何回したかの記録。返す枚数に使う。 k=0 #無か同色に当たるまでループ探索。 while True: #searchを一つ先に進める。 search += np.dot(A,B) k += 1 #無がある場合、返らないので別の方向へ if colors[search-1]==0: break #同色がある場合、k個返す elif colors[search-1]==turn+1: #passではなくなったので、pass_judg=1とする。 pass_judg=1 #ret(return)は返す場所 ret=search #k枚返して、置いた場所も増やす。 for j in range(k+1): #一つ戻る ret -= np.dot(A,B) #石を返す(同色にする)。 colors[ret-1]=turn+1 #別の方向へ break #異色がある場合、その先を探すためにループ(while)に戻る。 else: continue #forに戻る continue #8方向全ての探索を終えて、pass_judg==0ならパスなのでturnを入れ替えてもう一度探索。 if pass_judg == 0: pass_count += 1 loop_count += 1 #0と1を入れ替える。 turn = (turn+1)%2 #黒でも白でも打てなかった場合 if loop_count == 2 : print("エラー: \n置けないマスに置かれました。") sys.exit() #3.盤面情報([1,1,2,...])を○×形式に変換 result=[] for i in range(100): #端はいらないので、スキップ。 if i+1<=10 or (i+1)%10==1 or (i+1)%10==0 or i+1>=91: continue #無を-に。 elif colors[i] == 0: result.append('-') #黒をXに。 elif colors[i] == 1: result.append('X') #白をOに。 elif colors[i] == 2: result.append('O') result1 = ' ' #リストを文字列に変換。 for x in result: result1 += x #目的のOX形式 print('\n一列にした文字列:') print(result1) print('\n8×8で視覚的に表示:') print(result1[1:9]) print(result1[9:17]) print(result1[17:25]) print(result1[25:33]) print(result1[33:41]) print(result1[41:49]) print(result1[49:57]) print(result1[57:65]) |
工夫した点
工夫した点は次の三点です。
- 8×8ではなく、10×10の盤面を用意した。
- パスに対応するために様々な工夫をした。
- 返す方向を8つのベクトルで表現した。
軽く説明していきます。
流れをすっ飛ばしているので分からない部分も多いと思いますが、流しながら見て頂けると嬉しいです。
8×8ではなく、10×10の盤面を用意した
8×8の外側に常時空白のマスを設置しました。
これのメリットは次の二つです。
- 「変数search」が「探索」をやめるサインになる
- 桁が統一されて直感的に理解しやすくなる。
この後詳しく説明しますが、変数searchに盤上を「探索」させます。
その時に「この方向はハズレだよ」と教えてくれるサインになります。
(なんのこっちゃ分からないですよね...笑。)
個人的にこれを思いついた時は興奮しました笑。
パスに対応するために様々な工夫をした
プログラムを作るにあたって、パスの存在が結構厄介でした。
棋譜の文字列だけではパスを判断できないからです。
例えば、...h1PAc1... のようにパスが記述されていればそれを元にパスさせればいいのですが、実際は ...h1c1... と詰めて表示されています。
対策として、
- パスか否かを表現する変数pass_judg
- パスをカウントする変数pass_count
を導入しました。
最終的にデバック作業で手こずったのがこのパスの処理でした。
返す方向を8つのベクトルで表現した。
8つの方向を変数searchで探索するのですが、その時の計算を簡略化し、直感的に捉えやすくするためにベクトルを使いました。
xy = [(1,1),(1,0),(1,-1),(0,1),(0,-1),(-1,1),(-1,0),(-1,-1)]
x軸は右を正に、y軸は下を正にとっています。
例えば(1,1)は右下、(-1,0)は左を表しています。
数学が得意な人からすれば「方向なんだからそりゃベクトル使うっしょ」と感じるかもしれませんが、そうでもない僕からすると奇抜なナイスアイデアだったなと感じています。
ベクトルって実生活で役に立ちそうで立たない数学分野という印象で、これを応用出来たのは結構嬉しかったです。
全体の流れ
全体の流れを簡単に説明します。
丁寧に説明すると長くなるので、要点だけで。
そもそもどういう仕組みなの?
(少しイメージの話をします。)
入力された棋譜情報(f5d6c3...)を元にプログラム内に作ったオセロ盤で石を返していきます。
こんな感じ。(スタート)
初期配置
--------
--------
--------
---OX---
---XO---
--------
--------
--------
↓
文字情報「f5」を受け取り、f5に打つ
↓
--------
--------
--------
---OX---
---XXX--
--------
--------
--------
↓...(以下同様)
こんな感じ。(終わり)
そして最後まで行ったらその盤面を文字情報に変換して出力する、という仕組みです。
こういうと簡単そうですが、それをプログラミングで実行するには地道な変換作業が必要になります。
順番に見ていきましょう。
1.オセロ盤をどう表現するか
最初の課題はプログラム内でどうやってオセロ盤を表現するか、です。
まず、オセロ盤の特定のマスを表すのに座標では扱いにくいので番号で表現します。
左上から右下へ1マスずつ1~64の番号を割り当てるということです。
が、先程言ったように便宜上10×10のマスを使うので、同様にして1~100で番号付けします。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12(a1) | 13(b1) | 14(c1) | 17(f1) | 19(h1) | 20 | |||
21 | 30 | ||||||||
31 | 40 | ||||||||
41 | 50 | ||||||||
51 | 57(f5) | 60 | |||||||
61 | 70 | ||||||||
71 | 80 | ||||||||
81 | 90 | ||||||||
91 | 100 |
(スマホは飛び出すので横表示にしてね。)
例:f5は57番、d6は65番。
2.座標から番号への変換
オセロ盤の表現方法が決まったので、次の課題は座標から番号への変換です。
具体的に言うと、文字列'f5d6c3...'をリストscores[57,65,34,...]に変換します。
「f5」の数字部分が10の位に、アルファベット部分の順番(fなら6番目)に+1したものが1の位になっているのがポイントです。
アルファベットの順番を返す関数としてord関数を使いました。
ord(A)=65,ord(B)=66,ord(F)=70
みたいな関数なので、(大雑把...笑)
ord(数字にしたいalphabet)-63
をすれば1の位になります。
以上より、次のように変換します。
文字列「F5」から「F」,「5」を抽出。
番号=10×「5」 + ord(「F」)-63 =57
求めた番号をリストに追加。
こんな感じ。
3.オセロ盤に石を配置(初期盤面設定)
さて、次は盤面の石の色の情報をどう表現するかです。
これはまあ簡単で、0,1,2を使って表します。
黒は1、白は2、無(空きマス)は0としました。
100マスの色の情報をリストcolors[]に収納します。
初期盤面として、46番,55番に1、45番,56番に2、他に0を代入します。
colors[0,0,...,2,1,...,1,2,...,0]
と言った感じになります。
4.石を返していく
いよいよ石を返していきます。
人間は視覚を使って、挟むことができる相手の石を認識しますが、プログラムには視覚がありません。
これを解決する作業が先ほどから登場している「探索」です。
変数searchに8方向を探索させます。
以下のような条件分岐を行います。
- 置いた場所の隣が 無or手番と同じ色(黒番で黒,白番で白) だった場合、返らない
- 置いた場所の隣が 手番と違う色 だった場合、その先を調べる。
- 一つ先のマスが無だった場合、返らない
- 手番と同じ色だった場合、返る。(・・・①)
- 手番と違う色だった場合、その先を調べる。(無か手番と同じ色に当たるまでループ)
このループの際に8×8盤面の端までたどり着いた時に探索をやめさせるには、どうすればいいか悩みました。
これの解決策が10×10盤面です。
常に端に0(無)を置いておくことでそれを探知して探索を止めることができます。
具体的に見てましょう。
まず、変数searchにさぐってほしい位置(location)を代入します。
例えば、初手F5を考えてみましょう。
まず、上方向が返せるか確認するために上方向へ探索させます。
F5が57番なので、一つ上は47番です。
location=47 (変数loctionに47を代入)として、colors[loction]を求めます。
(colors[n]はリストcolorsのn番目を取り出す作業)
今回の例だと、colors[47]=0 (無) となります。
この場合、返せる石がないので上方向が返せないと分かり、別方向を探索させます。
こんな感じです。
5.パスへの対応
パスへの対応に手こずりました。
パスになると何が困るかと言うと、手番(turn)が入れ替わってしまうのです。
変数searchの探索の際に手番の情報が必要になります。
手番が実際の手番と逆になってしまうと、返る場所が変わってしまうのです。
これを解決するために二つの変数を導入しました。
- パスか否かを表現する変数pass_judg
- パスをカウントする変数pass_count
5-1.pass_judg
まず、プログラム上でパスを定義してやらなくてはなりません。
そのために変数pass_judgを使いました。
まずpass_judg=0 (pass_judgに0を代入) として、0のままならパス、1になったらパスではない、と定義します。
先ほど紹介した探索の条件分岐で、石を返した時(①の所)、同時にpass_judg=1 (1を代入) という処理をします。
もしも、8方向を探索して石が返ってなかったらpass_judg==0(pass_judgが0に等しい) のままのはずです。
この時、手番を入れ替え、pass_count+=1(pass_countの値に+1をする) として、もう一度8方向探索させます。
確かに、その通りですが、棋譜が正しいという前提があれば、どちらかがその場所に打ったわけなので、無限ループにはなりません。
ただ、棋譜が間違っていた場合、無限ループになってしまう可能性があるので、その対策は必要ですね。
後で追記しなくては。
(追記しました)
5-2.pass_count
基本的に、手番は今何手目か(n手目)で判断します。
奇数なら黒番、偶数なら白番です。(1手目は黒、2手目は白ですよね。)
しかし、パスになるとそれが入れ替わります。
なので、pass_countにパスの回数を記録して、(n + pass_count)の偶奇で手番を判断するようにしました。
これで手番を修正できます。
6.盤面情報をOX形式に変換
最後に、終局となった盤面情報(リストcolors[1,2,...2])をOX形式に変換します。
これも簡単で、
0→「-」(無),1→「X」(黒),2→「O」(白)
と変換すれば完了です。
終わり!
これにて全行程の大まかな流れを紹介し終えました。
お疲れ様でした。
作った感想
- 疲れた
- pythonの勉強になった
- デバック大変
- なんでブログでこんな説明してるんだ
さよなら、バイバイ!
追記
Twitterのリプライにてベテランプログラマーからアドバイス頂いたのですが、入力値が正しいことが前提のコードになっていました。
なので、次の場合にエラーを表示するように追記、変更しました。
- F5D6形式以外の文字列やA1~H8の範囲外が入力された時
- 既に石が置かれているマスに置かれた時
- 延々挟む場所がないなど、8方向の探索が無限ループになる時
追記内容
sysモジュールの使用
sys.exit()を使用。
エラーの際、プログラムを終了できる。
実際に書くコード
4行目に追記
import sys
a1~h1の範囲外の棋譜の入力への対策
偶数がアルファベットのa~hか確認。
ord(score[2*int(i)]) == 65~72
奇数が数字の1~8か確認。
int(score[2*int(i)+1]) == 1~8
実際に書くコード
14行目に追記。
既に置いたマスに置かれた時の対策
置いたマスが無であるか毎回確認する。
実際に書くコード
43行目に追記
置けないはずのマスに置かれた時(turnが無限に入れ替わる)の対策
ループの回数を数え、3回目に突入したら終了。
55行目に追記
loop_count=0
57行目を書き換える
before
while pass_judg==0 :
after
while pass_judg==0 and loop_count<2 :
106行目
loop_count+=1
109行目
if loop_count == 2 :
print("エラー: \n置けないマスに置かれました。")
sys.exit()
適用されているか確認
ちゃんと間違った入力値にエラーを出すことに成功しました。
今度こそ終わり!
入力エラーも考慮しなくてはなんですね。
勉強になりました。
ただ、もう一つ追記したいことが...
F5D6ではなくf5d6とすると、エラーになってしまいます。(ord(F)≠ord(f)なので)
疲れた&緊急性がないので後で追記します(^^;)