【Ruby】 三目並べのコードを読む
うぃろぅです。
業務時間で暇なとき作業の空き時間が生じたときにCodewarsで遊ぶ自己研鑽に励むことがたまにあるのですが、その中で「なんやこれ…」ってなったコードがあったので読み解いていきます。
今回の課題
「Tic-Tac-Toe Checker」というKata(問題)。
Codewarsに登録済みの方は上記リンクからとべる。
内容は以下。雑に訳すので間違ってたらゴメン。
「Tic-Tac-Toeゲーム(三目並べ / ○×ゲーム)」で遊ぶとき、勝敗を判別する機能が欲しいよね。そのチェックをする機能を作ることが目標だよ。
盤面は3 * 3
で、次のように配列で表すこととする。
0
は空白、1
は×
、2
は○
とする。
配列:
[[0, 0, 1],
[0, 1, 2],
[2, 1, 0]]
入力に対して以下のように返したい。
-1
: まだ勝負がついておらず、空白(0
)が存在する
1
:×
の人が勝利している
2
:○
の人が勝利している
0
: 引き分け((○×ゲームにおいては?)cat's gameと言うことがある…のかも)
例外処理は考えないものとして進めていく。
自分のコード
def judge(arr) arr.uniq.size == 1 && arr.first != 0 ? arr.first : false end def is_solved(board) if result = (0..2).map { |j| judge((0..2).map { |i| board[i][j] }) }.find { |r| r } || (0..2).map { |j| judge((0..2).map { |i| board[j][i] }) }.find { |r| r } || judge((0..2).map { |i| board[i][i] }) || judge((0..2).map { |i| board[i][2 - i] }) result else board.flatten.include?(0) ? -1 : 0 end end
特に何も思いつかなかったので特に難しいことはしていなくて、愚直に勝敗に関係する並びのパターンを書いて勝敗チェックをするメソッドを呼ぶ、という流れ。
勝敗がついていたらその値を返し、ついていなかったら0
の有無によって出し分ける。
おしゃれなやり方が思いつかなかったのでとりあえず通して先人の知恵を借りよう、という意図がちょっとある。
見直してみると0..2
を変数か定数にした方が見やすいなこれ。まぁそれは置いておくとして。
参考コード
で、答えた後に参照できるコードから自分の学びに活かそうとした結果これが最初に出てきた。
def is_solved(board) case board.join when /1..(1|.1.)..1|1.1.1..$|111(...)*$/ then 1 when /2..(2|.2.)..2|2.2.2..$|222(...)*$/ then 2 when /0/ then -1 else 0 end end
なにこれ…さぱらん(さっぱりわからん)…。
ということでこちらを読み解いていくのが今回の目的。
join
の結果を見る
board.join
で何が得られるのか。やってみた。
$ irb irb(main):001:0> board = [[1, 1, 2], [2, 1, 1], [2, 2, 1]] => [[1, 1, 2], [2, 1, 1], [2, 2, 1]] irb(main):002:0> puts board.join 112211221 => nil
なるほど単純に全部結合していると。Array#join
なのでstring
で得られる。
多次元化していても平滑化する。便利。
正規表現でチェック
ということは結合した文字列に対して正規表現を使ってチェックを行っているっぽい。
どちらかが勝つパターン
今回の場合は1
か2
が勝つパターン。大別すると以下。
- タテ
- ヨコ
- ナナメ
/1..(1|.1.)..1|1.1.1..$|111(...)*$/
を解析する。
regular expressionsに貼り付けて挙動を確認してみるとわかりやすい。
ヨコ
ヨコが一番簡単で、/1..(1|.1.)..1|1.1.1..$|111(...)*$/
の111(...)*
の箇所がそれ。
111
が1回、任意の3文字が0回以上。正しい入力がここまで来る想定なのでこれで問題なし。
タテ
単純化すると1..1..1
がどこかにあれば良いということになる。
/1..(1|.1.)..1|1.1.1..$|111(...)*$/
の1..(1|.1.)..1|
の箇所がそれということになる。
ナナメ
^1...1...1$
(複数行でないため\A\z
でなくても良い認識)か、^..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..$| # 右上→左下のナナメチェック 111(...)*$/ # ヨコチェック
ということがわかった。なんてスマート。
勝者がいないパターン
あとはもう消化試合。勝者がいないパターンかつ0
があったら-1
を返し、どれでもなかったら0
を返却する。
今回の学び
正規表現を使うことはもちろんあったし知識として知ってはいるが、この問題に応用できなかった。
正規表現センターを研ぎ澄ませていきたいところ。
自分のコードと参考コード
main.rb
が自分で考えたコード、clever.rb
が参考コード。
テストコードつき。
いやーこれは楽しい。まだまだ成長できるなぁ。
ではまた。