うぃろぅ.log

140字で綴りきれない日々の徒然備忘録

【Ruby】 三目並べのコードを読む

うぃろぅです。

業務時間で暇なとき作業の空き時間が生じたときにCodewars遊ぶ自己研鑽に励むことがたまにあるのですが、その中で「なんやこれ…」ってなったコードがあったので読み解いていきます。

今回の課題

「Tic-Tac-Toe Checker」というKata(問題)。

www.codewars.com

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で得られる。
多次元化していても平滑化する。便利。

ref.xaio.jp

正規表現でチェック

ということは結合した文字列に対して正規表現を使ってチェックを行っているっぽい。

どちらかが勝つパターン

今回の場合は12が勝つパターン。大別すると以下。

  • タテ
  • ヨコ
  • ナナメ

/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を返却する。

今回の学び

正規表現を使うことはもちろんあったし知識として知ってはいるが、この問題に応用できなかった。
正規表現センターを研ぎ澄ませていきたいところ。

自分のコードと参考コード

github.com

main.rbが自分で考えたコード、clever.rbが参考コード。
テストコードつき。

いやーこれは楽しい。まだまだ成長できるなぁ。

ではまた。