This commit is contained in:
Rikuoh Tsujitani 2024-04-16 20:15:18 +09:00
parent 37e91b7c23
commit 6645a15a7b
Signed by: riq0h
GPG key ID: 010F09DEA298C717
5 changed files with 383 additions and 1 deletions

66
position.rb Normal file
View file

@ -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

@ -1 +0,0 @@
Subproject commit 165d5aafaeaeba50c855ea1f6998e0170bca8474

58
reversi.rb Normal file
View file

@ -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

95
reversi_methods.rb Normal file
View file

@ -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

164
reversi_methods_test.rb Normal file
View file

@ -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