diff --git a/position.rb b/position.rb new file mode 100644 index 0000000..abb70f7 --- /dev/null +++ b/position.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +class Position + # マスを'f3','d6'などの表記で表現する。変数名cell_refとして取り扱う。 + COL = %w[a b c d e f g h].freeze + ROW = %w[1 2 3 4 5 6 7 8].freeze + + DIRECTIONS = [ + TOP_LEFT = :top_left, + TOP = :top, + TOP_RIGHT = :top_right, + LEFT = :left, + RIGHT = :right, + BOTTOM_LEFT = :bottom_left, + BOTTOM = :bottom, + BOTTOM_RIGHT = :bottom_right + ].freeze + + attr_accessor :row, :col + + def initialize(row_or_cell_ref, col = nil) + if col + # Position.new(1, 5) のような呼び出し + @row = row_or_cell_ref + @col = col + else + # Position.new('f7')のような呼び出し + @row = ROW.index(row_or_cell_ref[1]) + @col = COL.index(row_or_cell_ref[0]) + end + end + + def invalid? + row.nil? || col.nil? + end + + def out_of_board? + !((0..7).cover?(row) && (0..7).cover?(col)) + end + + def stone_color(board) + return nil if out_of_board? + + board[row][col] + end + + def to_cell_ref + return '盤面外' if out_of_board? + + "#{COL[col]}#{ROW[row]}" + end + + def next_position(direction) + case direction + when TOP_LEFT then Position.new(row - 1, col - 1) + when TOP then Position.new(row - 1, col) + when TOP_RIGHT then Position.new(row - 1, col + 1) + when LEFT then Position.new(row, col - 1) + when RIGHT then Position.new(row, col + 1) + when BOTTOM_LEFT then Position.new(row + 1, col - 1) + when BOTTOM then Position.new(row + 1, col) + when BOTTOM_RIGHT then Position.new(row + 1, col + 1) + else raise 'Unknown direction' + end + end +end diff --git a/reversi b/reversi deleted file mode 160000 index 165d5aa..0000000 --- a/reversi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 165d5aafaeaeba50c855ea1f6998e0170bca8474 diff --git a/reversi.rb b/reversi.rb new file mode 100644 index 0000000..5667776 --- /dev/null +++ b/reversi.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require_relative './reversi_methods' + +class Reversi + include ReversiMethods + + QUIT_COMMANDS = %w[quit exit q].freeze + + def initialize + @board = build_initial_board + @current_stone = BLACK_STONE + end + + def run + loop do + output(@board) + + if finished?(@board) + puts '試合終了' + puts "白○:#{count_stone(@board, WHITE_STONE)}" + puts "黒●:#{count_stone(@board, BLACK_STONE)}" + break + end + + unless placeable?(@board, @current_stone) + puts '詰みのためターンを切り替えます' + toggle_stone + next + end + + print "command? (#{@current_stone == WHITE_STONE ? '白○' : '黒●'}) > " + command = gets.chomp + break if QUIT_COMMANDS.include?(command) + + begin + if put_stone(@board, command, @current_stone) + puts '配置成功、次のターン' + toggle_stone + else + puts '配置失敗、ターン据え置き' + end + rescue StandardError => e + puts "ERROR: #{e.message}" + end + end + + puts 'finished!' + end + + private + + def toggle_stone + @current_stone = @current_stone == WHITE_STONE ? BLACK_STONE : WHITE_STONE + end +end + +Reversi.new.run if __FILE__ == $PROGRAM_NAME diff --git a/reversi_methods.rb b/reversi_methods.rb new file mode 100644 index 0000000..6550e34 --- /dev/null +++ b/reversi_methods.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require_relative './position' + +module ReversiMethods + WHITE_STONE = 'W' + BLACK_STONE = 'B' + BLANK_CELL = '-' + + def build_initial_board + # boardは盤面を示す二次元配列 + board = Array.new(8) { Array.new(8, BLANK_CELL) } + board[3][3] = WHITE_STONE # d4 + board[4][4] = WHITE_STONE # e5 + board[3][4] = BLACK_STONE # d5 + board[4][3] = BLACK_STONE # e4 + board + end + + def output(board) + puts " #{Position::COL.join(' ')}" + board.each_with_index do |row, i| + print Position::ROW[i] + row.each do |cell| + case cell + when WHITE_STONE then print ' ○' + when BLACK_STONE then print ' ●' + else print ' -' + end + end + print "\n" + end + end + + def copy_board(to_board, from_board) + from_board.each_with_index do |cols, row| + cols.each_with_index do |cell, col| + to_board[row][col] = cell + end + end + end + + def put_stone(board, cell_ref, stone_color, dry_run: false) + pos = Position.new(cell_ref) + raise '無効なポジションです' if pos.invalid? + raise 'すでに石が置かれています' unless pos.stone_color(board) == BLANK_CELL + + # コピーした盤面にて石の配置を試みて、成功すれば反映する + copied_board = Marshal.load(Marshal.dump(board)) + copied_board[pos.row][pos.col] = stone_color + + turn_succeed = false + Position::DIRECTIONS.each do |direction| + next_pos = pos.next_position(direction) + turn_succeed = true if turn(copied_board, next_pos, stone_color, direction) + end + + copy_board(board, copied_board) if !dry_run && turn_succeed + + turn_succeed + end + + def turn(board, target_pos, attack_stone_color, direction) + return false if target_pos.out_of_board? + return false if target_pos.stone_color(board) == attack_stone_color + return false if target_pos.stone_color(board) == BLANK_CELL + + next_pos = target_pos.next_position(direction) + if (next_pos.stone_color(board) == attack_stone_color) || turn(board, next_pos, attack_stone_color, direction) + board[target_pos.row][target_pos.col] = attack_stone_color + true + else + false + end + end + + def finished?(board) + !placeable?(board, WHITE_STONE) && !placeable?(board, BLACK_STONE) + end + + def placeable?(board, attack_stone_color) + board.each_with_index do |cols, row| + cols.each_with_index do |cell, col| + next unless cell == BLANK_CELL + position = Position.new(row, col) + return true if put_stone(board, position.to_cell_ref, attack_stone_color, dry_run: true) + end + end + false + end + + def count_stone(board, stone_color) + board.flatten.count { |cell| cell == stone_color } + end +end diff --git a/reversi_methods_test.rb b/reversi_methods_test.rb new file mode 100644 index 0000000..0a17589 --- /dev/null +++ b/reversi_methods_test.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require_relative './reversi_methods' + +class ReversiMethodsTest < Minitest::Test + include ReversiMethods + + def build_board(board_text) + board = build_initial_board + board_text.split("\n").each_with_index do |row, i| + row.each_char.with_index do |cell, j| + board[i][j] = cell + end + end + board + end + + def test_invalid_position + board = build_initial_board + e = assert_raises RuntimeError do + put_stone(board, 'x0', BLACK_STONE) + end + assert_equal '無効なポジションです', e.message + end + + def test_already_have_a_stone + board = build_initial_board + e = assert_raises RuntimeError do + put_stone(board, 'd5', BLACK_STONE) + end + assert_equal 'すでに石が置かれています', e.message + end + + def test_put_stone + board = build_initial_board + assert put_stone(board, 'e6', BLACK_STONE) + assert_equal build_board(<<~BOARD), board + -------- + -------- + -------- + ---WB--- + ---BB--- + ----B--- + -------- + -------- + BOARD + assert put_stone(board, 'f4', WHITE_STONE) + assert_equal build_board(<<~BOARD), board + -------- + -------- + -------- + ---WWW-- + ---BB--- + ----B--- + -------- + -------- + BOARD + end + + def test_cannot_put_stone + initial_data = <<~BOARD + W-WWWW-- + W-BWWW-- + WBWWWWW- + WWBWWW-- + WBBBBB-- + --B----- + --B----- + --B----- + BOARD + board = build_board(initial_data) + refute put_stone(board, 'b1', BLACK_STONE) + assert_equal build_board(initial_data), board + end + + def test_turn + board = build_board(<<~BOARD) + -------- + ---B---- + --WB---- + --WBB--- + --WWW--- + -------- + -------- + -------- + BOARD + assert put_stone(board, 'b4', BLACK_STONE) + assert_equal build_board(<<~BOARD), board + -------- + ---B---- + --BB---- + -BBBB--- + --WWW--- + -------- + -------- + -------- + BOARD + end + + def test_finished_of_initial_board + board = build_initial_board + refute finished?(board) # 初期盤面 + end + + def test_finished_of_full_board + assert finished?(build_board(<<~BOARD)) # 全て埋まった盤面 + WWWWWWWW + WBBWWBWB + WBBBBWBB + WBWBBBBB + WBWWBBBB + WBWWWBBB + WWWWWWBB + WBBBBBBB + BOARD + end + + def test_finished_of_quickest_win_board + assert finished?(build_board(<<~BOARD)) # 白最短勝利 + -------- + ---W---- + ---WW--- + -WWWWW-- + ---WWW-- + ---WWW-- + -------- + -------- + BOARD + assert finished?(build_board(<<~BOARD)) # 黒最短勝利 + -------- + -------- + ----B--- + ---BBB-- + --BBBBB- + ---BBB-- + ----B--- + -------- + BOARD + end + + def test_finished_of_player_skip_board + refute finished?(build_board(<<~BOARD)) # 白配置可・黒配置不可 + WWWWWWWB + WBBWWBWB + WBBBBWBB + WBWBBBB- + WBWWBBBB + WBWWWBBB + WWWWBWBB + WBBBBBBB + BOARD + refute finished?(build_board(<<~BOARD)) # 白配置不可・黒配置可 + WWWWWWWW + WBBWWBWB + WBBBBWBB + WBWBBBBB + WBWWBBBB + WBWWWBBB + BBBB-WBB + WBBBBBBB + BOARD + end +end