将棋の局面を記述する書式にSFENという記述方式がある。
これは盤上の駒の配置、手番、先手と後手の持ち駒、手数などがコンパクトに文字列にまとめられたもので、Kyokumen.jpの局面検索に使われていたり、将棋エンジンの定跡ファイルにも使われていたりする。
SFENの書式についてはこの記事にうまく説明されている。
今回、YaneuraouさんがMIT Licenseで新たに公開された233万手の定跡ファイルをみたところ、後手番の局面がなく、先手後手盤面を反転して検索する方法がとられている。つまり、 例えば初期画面から後手が3四歩と指した局面が先手番として登録されている。 屋根裏王のエンジンにはflippedBoardというパラメーターがあり、これをOnするだけで対応するよう設計されているのだが、定跡ファイルをブラウザで将棋盤上に表示させる自分のWebページに追加するためには、後手の手番の時に検索をかけるSFENの局面を反転させてサーバーに問い合わせる必要が生じた。サーバーから帰ってくる候補手も同様に、例えば2六歩を8四歩などと反転させる必要がある
最初の局面を反転させる部分、TypeScript関数として用意すると以下のようになる。
<pre>export const flipSFEN=(sfen:string):string=> {
const [board, turn, hands, moveCount] = sfen.split(' ');
const swapUpperLower=(str:string)=>{
const strArr=str.split('');
return strArr.map(char=>{
if (char===char.toUpperCase() && char!==char.toLowerCase()){
//string is uppercase
return char.toLowerCase(); //change to uppercase
} else //string is lowercase or number/symbol
{
if(char!==char.toUpperCase()){ //string is not symbol nor number. therefore string is lowercase
return char.toUpperCase(); //change to lower case
}
// string is number/symbol
return char;
}
}).join('');
}
const pivotHands=(hands:string)=>{
const flipped=swapUpperLower(hands);
const reversedHandsArray=flipped.split('').reverse();
let pivotPoint=-1
for (const [index,hand] of reversedHandsArray.entries()){
if (hand.toLowerCase()===hand && hand.toUpperCase()!==hand){
pivotPoint = index;
break;
}
}
if (pivotPoint>0) //if pivotpoint is either -1 or 0 then there is no need to manipulate flipped.
{
const breakPoint=flipped.length-pivotPoint;
return flipped.slice(breakPoint)+flipped.slice(0,breakPoint);
}
else return flipped
}
// Flip the board rows
const flippedBoard = board
.split('/')
.reverse()
.map(row => {
return row
.split('')
.map(char => {
return swapUpperLower(char);
})
.reduce((acc,item)=>{
const {elementArray, promoted}=acc
if (item==='+'){ return {elementArray,promoted:true}}
if (promoted){
item='+'+item.charAt(0)
}
elementArray[elementArray.length]=item
return {elementArray,promoted:false};
},{elementArray:[] as string[], promoted:false})
.elementArray //create a row array with promoted piece as +(piece name)
.reverse() // reverse board columns
.join(''); // processed one board row
})
.join('/'); //assemble 9 rows
// Flip the turn
const flippedTurn = turn === 'b' ? 'w' : 'b';
const flipHands =pivotHands(hands);
// Return the new SFEN
return `${flippedBoard} ${flippedTurn} ${flipHands} ${moveCount}`;
}
このコードで何をやっているかというと
1.まずは SFEN文字列をスペースをデリミターとして文字列アレイに分解する。
2.それぞれのアレイ因子を board, turn, hands, moveCountという変数にわりあてる。 ちなみにMoveCountは今回の定跡ファイルに使用するにはゼロに置き換える必要がある。(上のコードでは未反映)
3.board(駒の盤面位置情報)をさらにdelimeter ‘/’ で各列に分解したアレイを生成
4. array.reverse()を使って列順をひっくり返す。上段の列が下段に、下段の列が上段になる。
5.さらに、さらに各列の文字列を一文字づつに分解してアレイを作る。
6.大文字の駒(後手の駒)を小文字(先手の駒)、小文字の駒(先手の駒)を大文字に置換する
7.array.reduce()を使って、+シンボル(成り駒)因子と次の駒の因子を繋げて一つのアレー因子にまとめる
8. これを.reverse()で左右反転
9. 各列のアレイを文字列に変換
10。各列の文字列アレーをを’デリミター/’で接合し、一つの文字列を生成。→flippedBoard
11. 手番もひっくり返す (flippedTurn)
12. 持ち駒を先手と後手入れ替える (pivotHands) この部分、使ったロジックは以下のとおり
B3Ps14pを S14Pb3pとしたい。 まずは大文字小文字を入れ替える b3p2S14P (flipped) 最初の大文字のインデックスを取ればよいと思えるが、数字がついた場合(2S)となっていた場合はNG なので文字列を逆転させてみる P41S2p3b reversed これで最初の小文字のインデックスを取る。上の例だと5。文字列長さ8なので 8-5=3 secondPart =flipped.slice(0,3) = b3p firstPart = flipped.slice(3) =2S14P これを組み立てる final = firstPart+secondPart = 2S14Pb3p うまくいっているのでは? 先手の持ち駒だけの場合 例えば 2P 小文字のインデックスはマイナス1 この場合は処理をスキップ 後手の持ち駒だけの場合 例えば14p 逆転文字列での小文字のインデックスは0 この場合も処理をスキップ
13 .flifppedboard flippedTurn flippedHands moveCountを一つの文字列につなぎ合わせ、この値を返す
とまあ、書いてしまえば簡単な作業の連続だが、実はこのコード、最初AIに聞いて提示されたものが動かなくて、修正を入れ始めたら、結局七割がたは書き直してしまった、というものになる。
最初、Brave Brouserの検索窓に ”Given sfen string, write a javascript function that returns flipped sfen” と打ったらあっさり回答をよこしたのでこれをTypeScriptに書き直し、そのまま定跡ファイルに使ってみたのだた、局面検索がまったくヒットしなかった。 コードを解析して分かったことはAIがくれた回答は上記のステップから 7,8そして12の処理が抜けていた。その結果盤面の上下反転はしたが、そのあとが左右反転をしていないため、ミラーイメージになってしまい、さらに持ち駒があった場合は先手と後手が逆のまま。 Webページの仕組みはこのsfenをサーバーに送ってサーバーのデータベースからこのsfenに対応するmoves情報を返してもらい、次の指し手の候補をリストアップするというものなので、ミラーイメージの局面照会にはヒットする候補手はありませんという答えしか返ってこない。
AIはSFENが何たるかをあまり理解できないまま、WEBに落ちている情報をかき集めて使ったようで、プロンプトで、SFENの定義から始め、期待されるアウトプットのイメージをしっかり伝えておかなければだめだったか。
逆に良いねと思ったのはアレイやオブジェクトの関数をチェイニングして記述する、いわゆるFunctionalな書き方になっていてスマートである。 これは真似したい。
とにかく、AIに仕事をさせたら、今の時点では検証が大事なわけだが、それなら最初から提供されたコードは参考までに留めておいて、実際に使うコードは自分で書いたほうが楽しいかもね(仕事でプログラミングしている人はこうは言えまい)。
追記:Yaneuraouさんからflipsfenをpythonで作成した時の顛末のリンクを教えていただいた。 https://yaneuraou.yaneu.com/2023/12/15/chatgpt-wrote-a-program-to-flip-a-shogi-board/ 同じようなところでハマっているんだ、と可笑しくなった半面。 ちょっと待て、これは2年前の記事。 これを読んでいたらこんなに時間を使わなくてもよかったじゃん とも思った次第。