reversi
This commit is contained in:
parent
37e91b7c23
commit
6645a15a7b
5 changed files with 383 additions and 1 deletions
66
position.rb
Normal file
66
position.rb
Normal 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
reversi
1
reversi
|
@ -1 +0,0 @@
|
||||||
Subproject commit 165d5aafaeaeba50c855ea1f6998e0170bca8474
|
|
58
reversi.rb
Normal file
58
reversi.rb
Normal 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
95
reversi_methods.rb
Normal 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
164
reversi_methods_test.rb
Normal 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
|
Loading…
Reference in a new issue