Skip to content
Browse files

Made '@board' an instance variable of player implementations and made…

… 'Pentago::Rules' module accessible only through *a* board instance.
  • Loading branch information...
1 parent fdc8e8d commit 0ffcfd21a41f78bb21b601d0c52ece9526199616 @etrepat committed Nov 25, 2011
View
2 lib/pentago.rb
@@ -7,8 +7,8 @@
module Pentago; end
-require_relative 'pentago/board'
require_relative 'pentago/rules'
+require_relative 'pentago/board'
require_relative 'pentago/game'
require_relative 'pentago/version'
View
42 lib/pentago/board.rb
@@ -21,12 +21,31 @@ class Board
ROWS = COLS = 6
SIZE = ROWS * COLS
+ attr_accessor :squares
+
+ include Pentago::Rules
+
+ class << self
+ def restore(board)
+ restored = Board.new
+ restored.squares = case board
+ when Array
+ raise TypeError, "incompatible board array #{board.size}" if board.size != SIZE
+ board.dup
+ when Board
+ board.dup.to_a
+ else
+ raise TypeError, 'incompatible types'
+ end
+
+ restored
+ end
+ end
+
def initialize
clear
end
- attr_accessor :squares
-
def [](x, y)
raise IllegalPositionError, "illegal position [#{x}, #{y}]" unless valid_position?(x, y)
@squares[translate(x, y)]
@@ -116,8 +135,8 @@ def to_a
@squares
end
- def ==(board)
- @squares == board.squares
+ def ==(other)
+ @squares == other.squares
end
alias_method :eql?, :==
@@ -126,21 +145,6 @@ def dup
Board.restore(@squares)
end
- def self.restore(board)
- restored = Board.new
- restored.squares = case board
- when Array
- raise TypeError, "incompatible board array #{board.size}" if board.size != SIZE
- board.dup
- when Board
- board.dup.to_a
- else
- raise TypeError, 'incompatible types'
- end
-
- restored
- end
-
private
def translate(x,y)
View
4 lib/pentago/dummy_player.rb
@@ -1,10 +1,10 @@
module Pentago
class DummyPlayer < Player
- def compute_next_move(board)
+ def compute_next_move
# search for empty space w/ a highly precise algorithm :)
begin
x, y = rand(Board::COLS), rand(Board::ROWS)
- end while board[x,y]
+ end while @board[x,y]
# equally, search for a square to turn? into which direction
square = rand(Board::SQUARES.size)
View
48 lib/pentago/game.rb
@@ -1,16 +1,26 @@
module Pentago
class Game
DuplicatedPlayersError = Class.new(StandardError)
-
+
include Observable
- include Pentago::Rules
-
+
+ class << self
+ def create(options)
+ board = options.fetch(:board, Board.new)
+ player1 = Player.create(options.fetch(:player_engine_1), :board => board,
+ :marble => 1)
+ player2 = Player.create(options.fetch(:player_engine_2), :board => board,
+ :marble => 2)
+
+ Game.new(:board => board, :player1 => player1, :player2 => player2)
+ end
+ end
+
def initialize(params)
+ @board = params.fetch(:board)
@player1 = params.fetch(:player1)
@player2 = params.fetch(:player2)
raise DuplicatedPlayersError if @player1 == @player2
-
- @board = params.fetch(:board, Board.new)
end
attr_reader :player1, :player2, :board, :current_player
@@ -19,17 +29,17 @@ def play
reset
play_turn while !game_over?
end
-
+
def reset
@board.clear
@current_player = nil
@winner = nil
turn.rewind
end
-
+
def play_turn
@current_player = turn.next
- @current_player.play_turn(@board)
+ @current_player.play_turn
changed
notify_observers self
@@ -42,32 +52,32 @@ def turns_played
def players
@players ||= [player1, player2]
end
-
+
def winner
unless @winner
- who_won = find_winner(@board)
+ who_won = @board.find_winner
@winner = players.select { |p| p.marble == who_won }.first
end
-
+
@winner
end
-
+
def board_full?
@board.full?
end
-
+
def game_over?
- check_game_over(@board)
+ @board.game_over?
end
-
+
def tie_game?
- check_tie_game(@board)
+ @board.tie_game?
end
-
+
private
-
+
def turn
@turn ||= players.cycle
end
end
-end
+end
View
12 lib/pentago/human_player.rb
@@ -1,15 +1,15 @@
module Pentago
class HumanPlayer < Player
- def initialize(marble, name='', callback=nil)
- super(marble, name)
- @ask_for_move_callback = callback
+ def initialize(opts={})
+ super(opts)
+ @ask_for_move_callback = opts.fetch(:callback, nil)
end
attr_accessor :ask_for_move_callback
- def compute_next_move(board)
- raise 'how come should I ask a user for a move?' unless ask_for_move_callback
- @ask_for_move_callback.call(self, board)
+ def compute_next_move
+ raise 'how come should I ask a user for a move?' unless @ask_for_move_callback
+ @ask_for_move_callback.call(self, @board)
end
end
end
View
20 lib/pentago/negamax_player.rb
@@ -1,18 +1,19 @@
module Pentago
class NegamaxPlayer < Player
- def initialize(marble, name='', search_depth=1)
- super(marble, name)
- @search_depth = search_depth
+ def initialize(opts={})
+ super(opts)
+
+ @search_depth = opts.fetch(:search_depth, 1)
end
attr_accessor :search_depth
- def compute_next_move(board)
- available_moves(board).sort_by do |square, rotation|
+ def compute_next_move
+ available_moves(@board).sort_by do |square, rotation|
x, y = square
s, d = rotation
- board_copy = board.dup
+ board_copy = @board.dup
board_copy[x, y] = @marble
board_copy.rotate(s, d)
@@ -21,7 +22,7 @@ def compute_next_move(board)
end
def negamax(board, depth, player, alpha=-1, beta=1)
- return score(board, player) if depth == 0 || check_game_over(board)
+ return score(board, player) if depth == 0 || board.game_over?
available_moves(board).each do |square, rotation|
x, y = square
@@ -40,14 +41,14 @@ def negamax(board, depth, player, alpha=-1, beta=1)
# TODO: improve scoring functions
def score(board, player)
- winner = find_winner(board)
+ winner = board.find_winner
return 1000000 if winner && winner == player
return -1000000 if winner && winner == opponent(player)
score_for(board, player) - score_for(board, opponent(player))
end
def score_for(board, marble)
- (board.rows + board.columns + board.diagonals).inject(0) do |sum, run|
+ board.runs.inject(0) do |sum, run|
sum + run.count { |value| value.nil? || value == marble }
end
end
@@ -63,4 +64,3 @@ def available_moves(board)
end
end
end
-
View
44 lib/pentago/player.rb
@@ -1,41 +1,51 @@
module Pentago
class Player
- include Pentago::Rules
+ class << self
+ def create(engine_name, options={})
+ Pentago.const_get(engine_name).new(options)
+ rescue Exception => e
+ raise TypeError, "#{e.message}\n!! Invalid player engine (#{engine_name})!"
+ end
+ end
- def initialize(marble, name='')
- @marble = marble
- @name = name
+ def initialize(opts={})
+ @board = opts.fetch(:board)
+ @marble = opts.fetch(:marble)
+ @name = opts.fetch(:name, '')
@last_move = nil
end
+ attr_accessor :board
attr_reader :marble, :name, :last_move
- def play_turn(board)
- x, y, s, d = compute_next_move(board)
- execute_move(board, x, y, s, d)
+ def play_turn
+ x, y, s, d = compute_next_move
+
+ execute_move(x, y, s, d)
+
@last_move = [x, y, s, d]
end
- def compute_next_move(board)
+ def compute_next_move
# to be overriden by subclasses
raise 'compute_next_move should be overriden by a subclass'
end
- def execute_move(board, x, y, square, direction)
- board[x, y] = marble
- board.rotate(square, direction)
+ def execute_move(x, y, square, direction)
+ @board[x, y] = @marble
+ @board.rotate(square, direction)
end
-
- def ==(player)
- self.marble == player.marble
+
+ def ==(other)
+ @board == other.board && @marble == other.marble
end
-
+
alias_method :eql?, :==
-
+
def to_s
name.empty? ? marble.to_s : name
end
-
+
alias_method :to_str, :to_s
end
end
View
58 lib/pentago/rules.rb
@@ -1,37 +1,43 @@
module Pentago
module Rules
- def find_winner(board)
- winners = players_with_5_in_a_row(board)
- if winners.empty? || winners.size > 1
+ def find_winner
+ winners = players_with_5_in_a_row
+
+ if winners.empty?
nil
- else
+ elsif winners.size == 1
winners.first
- end
+ else
+ winners
+ end
end
-
- def check_game_over(board)
- find_winner(board) || check_tie_game(board)
+
+ def game_over?
+ !!find_winner
end
-
- def check_tie_game(board)
- !find_winner(board) && (board.full? || players_with_5_in_a_row(board).size > 1)
+
+ def tie_game?
+ case find_winner
+ when Array then true
+ when nil then full?
+ else false
+ end
end
-
- private
-
- def players_with_5_in_a_row(board)
- player_marbles = board.squares.compact.uniq
- runs_to_check(board).map do |run|
- player_marbles.map do |marble|
- marble if run.each_cons(5).any? do |part|
- part.count(marble) == part.size
- end
+
+ def runs
+ rows + columns + diagonals
+ end
+
+ private
+
+ def players_with_5_in_a_row
+ marbles = @squares.compact.uniq
+ winners = marbles.each_with_object([]) do |marble, result|
+ result << runs.map do |run|
+ marble if run.each_cons(5).any? { |p| p.count(marble) == 5 }
end
end.flatten.compact.uniq
end
-
- def runs_to_check(board)
- board.rows + board.columns + board.diagonals
- end
+
end
-end
+end
View
43 lib/pentago/ui/console.rb
@@ -11,13 +11,16 @@ def initialize(arguments, stdin, stdout)
def run
if parsed_options? && options_valid?
- @game = Pentago::Game.new(
- :board => @options[:board],
- :player1 => @options[:player1],
- :player2 => @options[:player2]
- )
- @game.add_observer(self)
+ @game = Pentago::Game.create(@options)
+
+ # Assign callbacks to ask for moves (if applicable)
+ @game.players.each do |player|
+ if player.kind_of?(Pentago::HumanPlayer)
+ player.ask_for_move_callback = method(:ask_for_move)
+ end
+ end
+ @game.add_observer(self)
@game.play
else
output_usage
@@ -46,12 +49,12 @@ def parsed_options?
BANNER
opts.on('--player1=PLAYER', String,
'Player 1 Engine (required)') do |engine|
- @options[:player1] = load_player(engine, 1)
+ @options[:player_engine_1] = classify(engine)
end
opts.on('--player2=PLAYER', String,
'Player 2 Engine (required)') do |engine|
- @options[:player2] = load_player(engine, 2)
+ @options[:player_engine_2] = classify(engine)
end
# TODO: option for reading a board from a text file
@@ -68,7 +71,8 @@ def parsed_options?
end
begin
- @options_parser.parse!(@arguments)
+ require "pp"
+ pp @options_parser.parse!(@arguments)
rescue TypeError, OptionParser::ParseError => e
@options_parser.warn e.message
nil
@@ -123,29 +127,16 @@ def ask_for_move(player, board)
attr_reader :terminal
def options_valid?
- @options.keys.include?(:player1) && @options.keys.include?(:player2)
+ @options.keys.include?(:player_engine_1) && @options.keys.include?(:player_engine_2)
end
def output_usage
terminal.say @options_parser.to_s
end
- def load_player(engine_name, player_marble)
- player = player_to_constant(engine_name).new(player_marble)
- raise TypeError if player.instance_of?(Pentago::Player)
-
- if player.kind_of?(Pentago::HumanPlayer)
- player.ask_for_move_callback = method(:ask_for_move)
- end
-
- player
- rescue Exception => e
- raise TypeError, "#{e.message}\n!! invalid player engine (#{engine_name})!"
- end
-
- def player_to_constant(name)
- camel = name.to_s.split('_').map { |s| s.capitalize }.join
- Pentago.const_get("#{camel}Player")
+ # creates a class name from the provided engine name
+ def classify(engine_name)
+ "#{engine_name}_player".to_s.split('_').map { |s| s.capitalize }.join
end
end
end
View
17 spec/pentago/dummy_player_spec.rb
@@ -4,22 +4,21 @@ module Pentago
describe DummyPlayer do
describe '#play_turn' do
before(:each) do
- @board = Board.new
+ @player = DummyPlayer.new(:board => Board.new, :marble => 1)
end
it 'holds last played move' do
- player = DummyPlayer.new(1)
- player.last_move.should be_nil
- player.play_turn(@board)
- player.last_move.should_not be_nil
+ @player.last_move.should be_nil
+
+ @player.play_turn
+ @player.last_move.should_not be_nil
end
it 'should play a move for its player' do
- player = DummyPlayer.new(1)
- player.play_turn(@board)
- @board.squares.compact.first.should == player.marble
+ @player.play_turn
+ @player.board.squares.compact.first.should == @player.marble
end
- end
+ end
end
end
View
18 spec/pentago/game_spec.rb
@@ -3,10 +3,10 @@
module Pentago
describe Game do
before(:each) do
- @player1 = Pentago::DummyPlayer.new(1)
- @player2 = Pentago::DummyPlayer.new(2)
@board = Pentago::Board.new
- @game = Pentago::Game.new(:player1 => @player1, :player2 => @player2,
+ @player1 = Pentago::DummyPlayer.new(:board => @board, :marble => 1)
+ @player2 = Pentago::DummyPlayer.new(:board => @board, :marble => 2)
+ @game = Pentago::Game.new(:player1 => @player1, :player2 => @player2,
:board => @board)
end
@@ -20,33 +20,33 @@ module Pentago
@game.board.should == @board
end
end
-
+
describe 'instance methods' do
before(:each) do
@squares = [nil,1,2,nil,1,nil,nil,2,nil,nil,2,nil,nil,nil,1,2,nil,
nil,2,1,2,nil,nil,2,1,nil,2,nil,nil,1,1,nil,2,nil,1,1]
end
-
+
describe '#turns_played' do
it 'should return number of turns played' do
@game.turns_played.should == 0
-
+
@game.board.squares = @squares
@game.turns_played.should == 18
end
end
-
+
describe '#winner' do
it 'should return nil if no player is in winning position' do
@game.board.squares = @squares
@game.winner.should be_nil
end
-
+
it 'should return player in winning position' do
@game.board.squares = @squares
@game.board[3,5] = 1
@game.board.rotate(2, :counter_clockwise)
- @game.winner.should == @player1
+ @game.winner.should == @player1
end
end
end
View
33 spec/pentago/player_spec.rb
@@ -3,62 +3,59 @@
module Pentago
describe Player do
describe '#initialize' do
- it 'should create a player with a marble' do
- base_1 = Player.new(1)
+ it 'should create a player with a board & marble' do
+ base_1 = Player.new(:board => Board.new, :marble => 1)
+ base_1.board.should == Board.new
base_1.marble.should == 1
-
- base_x = Player.new('x')
- base_x.marble.should == 'x'
end
end
describe '#play_turn' do
it 'should raise RuntimeError when this method is called' do
- base = Player.new(1)
- board = Board.new
+ base = Player.new(:board => Board.new, :marble => 1)
expect {
- base.play_turn(board)
+ base.play_turn
}.to raise_error(RuntimeError)
end
end
describe '#compute_next_move' do
it 'should raise RuntimeError when this method is called' do
- base = Player.new(1)
- board = Board.new
+ base = Player.new(:board => Board.new, :marble => 1)
expect {
- base.compute_next_move(board)
+ base.compute_next_move
}.to raise_error(RuntimeError)
end
end
describe '#execute_move' do
before(:each) do
- @base_player1 = Player.new(1)
- @base_player2 = Player.new(2)
@board = Board.new
@other_board = Board.new
+ @base_player1 = Player.new(:board => @board, :marble => 1)
+ @base_player2 = Player.new(:board => @board, :marble => 2)
end
it 'should place a marble of its own player' do
- @base_player1.execute_move(@board, 0, 0, 0, :clockwise)
- @base_player2.execute_move(@board, 1, 0, 1, :counter_clockwise)
+ @base_player1.execute_move(0, 0, 0, :clockwise)
+ @base_player2.execute_move(1, 0, 1, :counter_clockwise)
@board[2,0].should == @base_player1.marble
@board[1,0].should == @base_player2.marble
end
it 'should do exactly the same as doing it directly' do
- @base_player1.execute_move(@board, 1, 1, 0, :clockwise)
- @base_player2.execute_move(@board, 2, 1, 1, :counter_clockwise)
+ @base_player1.execute_move(1, 1, 0, :clockwise)
+ @base_player2.execute_move(2, 1, 1, :counter_clockwise)
@other_board[1,1] = @base_player1.marble
@other_board.rotate(0, :clockwise)
@other_board[2,1] = @base_player2.marble
@other_board.rotate(1, :counter_clockwise)
- @board.squares.should == @other_board.squares
+ @base_player1.board.should == @other_board
+ @base_player2.board.should == @other_board
end
end
end
View
144 spec/pentago/rules_spec.rb
@@ -1,131 +1,107 @@
module Pentago
describe Rules do
- class RulesTest
- include Pentago::Rules
- end
-
before(:each) do
# full board, no winning conditions
- @full_squares = [1,2,2,2,2,1,2,1,1,1,1,2,1,2,2,2,2,1,2,1,1,1,1,2,1,2,2,
- 2,2,1,2,1,1,1,1,2]
- @full_board = Pentago::Board.restore(@full_squares)
+ @full_squares = [1,2,2,2,2,1,2,1,1,1,1,2,1,2,2,2,2,1,2,1,1,1,1,2,1,
+ 2,2,2,2,1,2,1,1,1,1,2]
+ @full_board = Board.restore(@full_squares)
# from an actual game, actually this one:
# http://en.wikipedia.org/wiki/File:Pentago-Game-Winning-Position.jpg
- @winning_squares = [1,1,1,nil,2,nil,nil,2,1,2,2,1,nil,nil,2,1,2,nil,
+ @winning_squares = [1,1,1,nil,2,nil,nil,2,1,2,2,1,nil,nil,2,1,2,nil,
nil,1,2,nil,1,nil,2,nil,nil,2,nil,1,nil,nil,nil,nil,nil,nil]
- @winning_board = Pentago::Board.restore(@winning_squares)
+ @winning_board = Board.restore(@winning_squares)
# open board, no full, no winners
- @open_squares = [nil,1,2,nil,1,nil,nil,2,nil,nil,2,nil,nil,nil,1,2,nil,
- nil,2,1,2,nil,nil,2,1,nil,2,nil,nil,1,1,nil,2,nil,1,1]
- @open_board = Pentago::Board.restore(@open_squares)
-
- @rules = RulesTest.new
+ @open_squares = [nil,1,2,nil,1,nil,nil,2,nil,nil,2,nil,nil,nil,1,2,
+ nil,nil,2,1,2,nil,nil,2,1,nil,2,nil,nil,1,1,nil,2,nil,1,1]
+ @open_board = Board.restore(@open_squares)
+
+ # two winners board (tie-game)
+ @two_winners_squares = [nil,1,2,nil,1,nil,nil,2,nil,nil,2,nil,nil,nil,1,2,
+ nil,nil,2,2,2,2,2,2,1,nil,nil,nil,nil,1,2,1,1,1,1,1]
+ @two_winners_board = Board.restore(@two_winners_squares)
end
describe '#find_winner' do
it 'should return nil if no winner' do
- @rules.find_winner(@full_board).should be_nil
- @rules.find_winner(@open_board).should be_nil
+ @full_board.find_winner.should be_nil
+ @open_board.find_winner.should be_nil
end
-
+
it 'should detect a winner when 5 consecutive marbles' do
- @rules.find_winner(@winning_board).should_not be_nil
+ @winning_board.find_winner.should_not be_nil
@full_board.squares[0] = 2
@full_board.squares[3] = 1
@full_board.squares[5] = 2
- @rules.find_winner(@full_board).should be_nil
-
+ @full_board.find_winner.should be_nil
+
@full_board.squares[3] = 2
- @rules.find_winner(@full_board).should_not be_nil
+ @full_board.find_winner.should_not be_nil
end
-
+
it 'should return marble value of winner' do
- @rules.find_winner(@winning_board).should == 1
-
- @rules.find_winner(@open_board).should be_nil
+ @winning_board.find_winner.should == 1
+
+ @open_board.find_winner.should be_nil
@open_board[3,5] = 1
@open_board.rotate(2, :counter_clockwise)
- @rules.find_winner(@open_board).should == 1
-
- @rules.find_winner(@full_board).should be_nil
+ @open_board.find_winner.should == 1
+
+ @full_board.find_winner.should be_nil
@full_board.squares[0] = 2
- @rules.find_winner(@full_board).should == 2
+ @full_board.find_winner.should == 2
end
-
- it 'should return nil if more than one winner (tie game)' do
+
+ it 'should return array of winners if more than one (tie game)' do
@full_board.squares[0] = 2
- @full_board.squares[Pentago::Board::SIZE-1] = 1
- @rules.find_winner(@full_board).should be_nil
-
- two_winners = Board.restore([nil,1,2,nil,1,nil,nil,2,nil,nil,2,nil,nil,
- nil,1,2,nil,nil,2,1,2,2,2,2,1,nil,2,nil,nil,1,1,nil,2,nil,1,1])
- two_winners[3,5] = 1
- two_winners.rotate(2, :counter_clockwise)
- # now there's two winners there (tie)
- @rules.find_winner(two_winners).should be_nil
+ @full_board.squares[Board::SIZE-1] = 1
+ @full_board.find_winner.should have(2).items
+
+ @two_winners_board.find_winner.should have(2).items
end
end
- describe '#check_game_over' do
+ describe '#game_over?' do
it 'should return true if there is a winner' do
- @rules.check_game_over(@winning_board).should be_true
+ @winning_board.game_over?.should be_true
end
-
- it 'should return true if tie game' do
- def @rules.check_tie_game(board)
- true
- end
-
- @rules.check_game_over(@open_board).should be_true
+
+ it 'should return true if there is more than one winner (tie game)' do
+ @two_winners_board.game_over?.should be_true
end
-
+
it 'should return false if game is open' do
- @rules.find_winner(@open_board).should be_nil
- def @rules.check_tie_game(board)
- false
- end
-
- @rules.check_tie_game(@open_board).should be_false
+ @open_board.game_over?.should be_false
end
end
-
- describe '#check_tie_game' do
+
+ describe '#tie_game?' do
it 'should return false when game is open (not full)' do
- @rules.find_winner(@open_board).should be_nil
- @open_board.full?.should be_false
- @rules.check_tie_game(@open_board).should be_false
+ @open_board.tie_game?.should be_false
end
-
- it 'should return true when board is full & no winner, false othw' do
- @rules.find_winner(@full_board).should be_nil
- @full_board.full?.should be_true
- @rules.check_tie_game(@full_board).should be_true
+
+ it 'should return true when board is full' do
+ @full_board.tie_game?.should be_true
end
-
- it 'should return false when there is only one winner' do
+
+ it 'should return false when there is only one winner' do
@full_board.squares[0] = 2
- @rules.find_winner(@full_board).should == 2
- @rules.check_tie_game(@full_board).should be_false
-
- @rules.find_winner(@winning_board).should == 1
- @rules.check_tie_game(@winning_board).should be_false
+ @full_board.find_winner.should == 2
+ @full_board.tie_game?.should be_false
+
+ @winning_board.find_winner.should == 1
+ @winning_board.tie_game?.should be_false
end
-
+
it 'should return true if two winners at the same time' do
@full_board.squares[0] = 2
- @full_board.squares[Pentago::Board::SIZE-1] = 1
- @rules.check_tie_game(@full_board).should be_true
-
- two_winners = Board.restore([nil,1,2,nil,1,nil,nil,2,nil,nil,2,nil,nil,
- nil,1,2,nil,nil,2,1,2,2,2,2,1,nil,2,nil,nil,1,1,nil,2,nil,1,1])
- two_winners[3,5] = 1
- two_winners.rotate(2, :counter_clockwise)
- # now there's two winners there (tie)
- @rules.check_tie_game(two_winners).should be_true
+ @full_board.squares[Board::SIZE-1] = 1
+ @full_board.tie_game?.should be_true
+
+ @two_winners_board.tie_game?.should be_true
end
end
end
-end
+end

0 comments on commit 0ffcfd2

Please sign in to comment.
Something went wrong with that request. Please try again.