Initial commit
[cartagena.git] / lib / .svn / text-base / libcartagena.rb.netbeans-base
1 # == Synopsis
2 #
3 # Library to support Cartagena play
4 #
5 # == Author
6 # Neil Smith
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 #
21 # == Change history
22 # Version 1.0:: 11 Jun 2008
23 # * Initial build
24
25 # Symbols
26 $SYMBOLS = [:bottle, :dagger, :gun, :hat, :keys, :skull]
27
28 # The number of cards of each symbol in the deck
29 $CARDS_PER_SYMBOL = 17
30
31 # The number of cards initiall dealt to each player
32 $INITIAL_CARDS_PER_PLAYER = 3
33
34 # Maximum number of pieces on a position
35 $MAX_PIECES_PER_POSITION = 3
36
37 # Number of actions that can be taken by each player in sequence
38 $MAX_MOVES_PER_TURN = 3
39
40 # Errors for a game
41
42 # Moves can only be [1..6] spaces
43 class InvalidMoveError < StandardError
44 end
45
46 # Game is won when only one player has uncaptured pieces
47 class GameWonNotice < StandardError
48 end
49
50 # A position on the board.
51 class Position
52 attr_accessor :symbol
53 attr_accessor :contains
54
55 def initialize(symbol)
56 @symbol = symbol
57 @contains = []
58 end
59 end
60
61 # A tile that makes up the board. Each tile has two sides, each with six
62 # positions. Tiles can be either way up, and either way round.
63 class Tile
64 attr_reader :exposed, :front, :back
65
66 def initialize(front, back)
67 @front = front.collect {|s| Position.new(s)}
68 @back = back.collect {|s| Position.new(s)}
69 @exposed = @front
70 @exposed_side = :front
71 end
72
73 def flip!
74 if @exposed_side == :front
75 @exposed = @back
76 @exposed_side = :back
77 else
78 @exposed = @front
79 @exposed_side = :front
80 end
81 end
82
83 def reverse!
84 @exposed = @exposed.reverse
85 end
86 end
87
88
89 # The game board
90 class Board
91
92 attr_accessor :positions
93 attr_reader :tiles
94
95 # A laborious procedure to create all the positions and tie them all together
96 def initialize(tiles_used)
97 # A hash of all positions, indexed by position names
98 @tiles = [Tile.new([:hat, :keys, :gun, :bottle, :skull, :dagger],
99 [:hat, :keys, :gun, :bottle, :skull, :dagger]),
100 Tile.new([:gun, :hat, :dagger, :skull, :bottle, :keys],
101 [:gun, :hat, :dagger, :skull, :bottle, :keys]),
102 Tile.new([:skull, :gun, :bottle, :keys, :dagger, :hat],
103 [:skull, :gun, :bottle, :keys, :dagger, :hat]),
104 Tile.new([:dagger, :bottle, :keys, :gun, :hat, :skull],
105 [:dagger, :bottle, :keys, :gun, :hat, :skull]),
106 Tile.new([:keys, :dagger, :skull, :hat, :gun, :bottle],
107 [:keys, :dagger, :skull, :hat, :gun, :bottle]),
108 Tile.new([:bottle, :skull, :hat, :dagger, :keys, :gun],
109 [:bottle, :skull, :hat, :dagger, :keys, :gun])
110 ].shuffle[0...tiles_used]
111 @tiles = @tiles.each do |t|
112 if rand < 0.5
113 t.reverse!
114 else
115 t
116 end
117 end
118
119 @positions = [Position.new(:cell)]
120 @tiles.each {|t| t.exposed.each {|p| @positions << p}}
121 @positions << Position.new(:boat)
122 end # def
123
124 def to_s
125 layout
126 end
127
128 def to_str
129 to_s
130 end
131
132
133 # For each position, show its name and what it touches
134 def layout
135 out_string = ""
136 @positions.each {|position| out_string << "#{position.symbol}\n"}
137 out_string
138 end
139
140 end
141
142
143 # Each piece on the board is an object
144 class Piece
145 attr_reader :player, :number
146 attr_accessor :position
147
148 def initialize(position, player, number)
149 @position = position
150 @player = player
151 @number = number
152 end
153
154 def to_s
155 "#{@player}:#{@number}"
156 end
157
158 def to_str
159 to_s
160 end
161
162 def show(convert_to_zero_based_player_number = false)
163 if convert_to_zero_based_player_number
164 "#{@player + 1}:#{@number}"
165 else
166 "#{@player}:#{@number}"
167 end
168 end
169
170 def move_to(new_position)
171 @position = new_position
172 end
173
174 end
175
176
177 # A move in a game
178 class Move
179 attr_reader :piece, :origin, :destination, :card_played
180
181 def initialize(piece, origin, destination, card_played = :unspecified)
182 @piece = piece
183 @origin = origin
184 @destination = destination
185 @card_played = card_played
186 end
187
188 def show(board, convert_to_zero_based_player_number = false)
189 if @card_played == :unspecified
190 "#{@piece.show(convert_to_zero_based_player_number)}: #{board.positions.index(@origin)} -> #{board.positions.index(@destination)}"
191 else
192 "#{@piece.show(convert_to_zero_based_player_number)}: #{board.positions.index(@origin)} -> #{board.positions.index(@destination)} (#{@card_played})"
193 end
194 end
195
196 def format(board, convert_to_zero_based_player_number = false)
197 display_player_number = if convert_to_zero_based_player_number then
198 @piece.player + 1
199 else
200 @piece.player
201 end
202 if @card_played ==:unspecified
203 "#{display_player_number} #{board.positions.index(@origin)} #{board.positions.index(@destination)}"
204 else
205 "#{display_player_number} #{board.positions.index(@origin)} #{board.positions.index(@destination)} #{@card_played}"
206 end
207 end
208
209 # Write a move to a string
210 # Note the inverse, String#to_move, is defined below
211 def to_s
212 "#{@piece.player} #{@origin.to_s} #{@destination.to_s} #{@card_played}"
213 end
214
215 def to_str
216 to_s
217 end
218
219 end
220
221
222
223 # A class to record each of the states previously found in a game.
224 # Note that this is a deep copy of the pieces and what they've captured, so
225 # you'll need to convert back
226 class GameState
227 attr_accessor :move, :player, :board, :pieces_before_move, :deck_before_move,
228 :players_cards_before_move, :moves_by_current_player
229 # this_game_state = GameState.new(move, player, @board, @pieces, @deck, @players_cards)
230
231
232 def initialize(move, player, board, pieces, deck, players_cards, moves_by_current_player)
233 @move, @player, @board, @pieces_before_move, @deck_before_move,
234 @players_cards_before_move, @moves_by_current_player =
235 copy_game_state(move, player, board, pieces, deck, players_cards,
236 moves_by_current_player)
237 end
238
239 # def ==(other)
240 # @move.to_s == other.move.to_s and
241 # @player == other.player and
242 # @piece_after_move == other.pieces_after_move
243 # end
244
245 def copy_game_state(move, player, board, pieces, deck, players_cards, moves_by_current_player)
246 copy_player = player
247 moving_player = move.piece.player
248
249 copy_board = board.dup
250 copy_board.positions = board.positions.collect {|pos| pos.dup}
251 copy_board.positions.each {|pos| pos.contains = []}
252
253 copy_pieces = pieces.collect do |players_pieces|
254 players_pieces.collect do |piece|
255 new_piece_position = copy_board.positions[board.positions.index(piece.position)]
256 new_piece = Piece.new(new_piece_position, piece.player, piece.number)
257 new_piece_position.contains << new_piece
258 new_piece
259 end
260 end
261
262 piece_index = pieces[moving_player].index(move.piece)
263 origin_index = board.positions.index(move.origin)
264 destination_index = board.positions.index(move.destination)
265 copy_move = Move.new(copy_pieces[moving_player][piece_index],
266 copy_board.positions[origin_index],
267 copy_board.positions[destination_index])
268
269 copy_deck = deck.dup
270 copy_players_cards = players_cards.collect {|p| p.dup}
271 copy_moves_by_current_player = moves_by_current_player
272
273 return copy_move, copy_player, copy_board, copy_pieces, copy_deck, copy_players_cards, copy_moves_by_current_player
274 end
275
276 end
277
278
279 # A game of Cartagena. It keeps a history of all previous states.
280 class Game
281
282 attr_reader :history
283 attr_accessor :current_player
284 attr_reader :players
285 attr_accessor :players_cards
286 attr_accessor :moves_by_current_player
287 attr_reader :board
288 attr_reader :pieces
289 attr_reader :cards
290 attr_accessor :deck
291
292 # Create a new game
293 def initialize(players = 5, number_of_tiles = 6, pieces_each = 6)
294 @board = Board.new(number_of_tiles)
295 @history = []
296 @pieces = []
297 @players = [].fill(0, players) {|i| i}
298 @players_cards = []
299 @current_player = 0
300 @moves_by_current_player = 0
301 @cards = []
302 1.upto($CARDS_PER_SYMBOL) {|x| @cards.concat($SYMBOLS)}
303 @deck = @cards.shuffle
304 @players.each do |p|
305 @pieces[p] = []
306 0.upto(pieces_each - 1) do |count|
307 piece = Piece.new(@board.positions[0], p, count)
308 @pieces[p][count] = piece
309 @board.positions[0].contains << piece
310 end
311 @players_cards[p] = []
312 deal_cards!($INITIAL_CARDS_PER_PLAYER, p)
313 end
314 end
315
316 # Deal some cards to a player. Remove them from the deck. Refill the deck
317 # if necessary
318 def deal_cards!(number_of_cards, player = @current_player)
319 1.upto(number_of_cards) do
320 if @deck.empty?
321 @deck = @cards
322 @players_cards.each do |p|
323 p.each do |c|
324 @deck.delete_at(@deck.index(c))
325 end
326 end
327 @deck = @deck.shuffle
328 end
329 @players_cards[player] << @deck.pop
330 end
331 end
332
333 # Check that a move is valid. Throw an exception if it's invalid
334 def validate_move(move, player = @current_player)
335 # Check the move is a valid one
336 raise(InvalidMoveError, "Move #{move}: Player #{player} does not exist") unless @players.include?(player)
337 raise(InvalidMoveError, "Move #{move}: None of player #{player}'s pieces on position #{move.origin}") unless move.origin.contains.find {|pc| pc.player == player}
338 raise(InvalidMoveError, "Move #{move}: Origin and destination are the same") if move.origin == move.destination
339
340 origin_position = @board.positions.index(move.origin)
341 destination_position = @board.positions.index(move.destination)
342 # Is this move an advance or a retreat?
343 if destination_position > origin_position
344 # Advancing a piece
345 if move.destination == @board.positions[-1] # A move into the boat
346 raise(InvalidMoveError, "Move #{move}: Move into boat and card unspecified") if move.destination == @board.positions[-1] and move.card_played == :unspecified
347 unless @players_cards[player].find {|c| c == move.card_played}
348 raise(InvalidMoveError, "Player #{player} does not have a card to move a piece into the boat")
349 end
350 else
351 if move.card_played != :unspecified and move.destination.symbol != move.card_played
352 raise(InvalidMoveError, "Player #{player} trying to move to #{move.destination}, a #{move.destination.symbol} square with a a #{move.card_played} card")
353 end
354 unless @players_cards[player].find {|c| c == move.destination.symbol}
355 raise(InvalidMoveError, "Player #{player} does not have a card to move a piece to a #{move.destination.symbol} square")
356 end
357 # Check target square is vacant
358 raise(InvalidMoveError, "Advance move #{move}: destination occupied") unless move.destination.contains.empty?
359 end
360 # Check all the intervening squares with this symbol are occupied
361 intervening_empty_position = @board.positions[origin_position...destination_position].index_find do |p|
362 p.symbol == move.destination.symbol and
363 p.contains.empty?
364 end
365 raise(InvalidMoveError, "Advance move #{move}: location #{intervening_empty_position} is empty") if intervening_empty_position
366 else
367 # Retreating a piece
368 # Check target position has one or two pieces already on it
369 destination_count = move.destination.contains.length
370 raise(InvalidMoveError, "Retreat move #{move}: destination has no pieces already on it") if destination_count == 0
371 raise(InvalidMoveError, "Retreat move #{move}: destination has too many (#{destination_count}) pieces already on it") if destination_count >= $MAX_PIECES_PER_POSITION
372 # Check none of the intervening squares have any pieces on them
373 # puts "Checking positions #{destination_position} to #{origin_position}"
374 intervening_target_position = @board.positions[(destination_position + 1)...origin_position].index_find do |p|
375 # puts "Examining postition #{p} at location #{@board.positions.index(p)} which contains #{p.contains.length} pieces"
376 p.contains.length > 0 and
377 p.contains.length < $MAX_PIECES_PER_POSITION
378 end
379 raise(InvalidMoveError, "Retreat move #{move.show(@board)}: location #{intervening_target_position} is a viable target") if intervening_target_position
380 end
381 end
382
383 # Apply a single move to a game.
384 def apply_move!(move, player = @current_player, validate = true)
385
386 validate_move(move, player) if validate
387
388 raise(InvalidMoveError, "Too many consecutive moves by #{@current_player}: has taken #{@moves_by_current_player} and validate is #{validate}") if validate and player == @current_player and @moves_by_current_player >= $MAX_MOVES_PER_TURN
389
390 # Record the old state
391 this_game_state = GameState.new(move, @current_player, @board, @pieces, @deck, @players_cards, @moves_by_current_player)
392 @history << this_game_state
393
394 # Apply this move
395 move.origin.contains.delete move.piece
396 move.destination.contains << move.piece
397 move.piece.position = move.destination
398
399 if player == @current_player
400 @moves_by_current_player += 1
401 else
402 @current_player = player
403 @moves_by_current_player = 1
404 end
405
406 # Update cards
407 if @board.positions.index(move.destination) > @board.positions.index(move.origin)
408 # Advance move
409 if validate
410 card_to_remove = if move.card_played != :unspecified
411 move.card_played
412 else
413 move.destination.symbol
414 end
415 if @players_cards[player].include?(card_to_remove)
416 @players_cards[player].delete_at(@players_cards[player].index(card_to_remove))
417 end
418 end
419 else
420 # Retreat move
421 deal_cards!(move.destination.contains.length - 1, player)
422 end
423
424 # If this player has all their pieces in the boat, declare a win.
425 if @pieces[player].all? {|pc| pc.position == @board.positions[-1]}
426 raise(GameWonNotice, "Game won by #{player}")
427 end
428 end
429
430 # Undo a move
431 def undo_move!
432 state_to_restore = @history[-1]
433 move, @current_player, @board, @pieces, @deck, @players_cards,
434 @moves_by_current_player =
435 state_to_restore.copy_game_state(state_to_restore.move,
436 state_to_restore.player,
437 state_to_restore.board,
438 state_to_restore.pieces_before_move,
439 state_to_restore.deck_before_move,
440 state_to_restore.players_cards_before_move,
441 state_to_restore.moves_by_current_player)
442 @history.pop
443 end
444
445 # Apply a list of moves in order
446 def apply_moves!(moves)
447 moves.each do |move|
448 @current_player = move.piece.player
449 apply_move!(move, @current_player)
450 next_player! if @moves_by_current_player >= $MOVES_PER_TURN
451 end
452 end
453
454
455 # Set the current player to be the next player
456 def next_player!
457 if @current_player == @players[-1]
458 @current_player = @players[0]
459 else
460 @current_player = @players[@players.index(@current_player) + 1]
461 end
462 @moves_by_current_player = 0
463 @current_player
464 end
465
466
467 def reset_current_player(new_current_player)
468 @current_player = new_current_player
469 @moves_by_current_player = 0
470 end
471
472
473 # Return an array of all possible moves from this state, given the active player
474 def possible_moves(player = @current_player)
475 moves = []
476 @pieces[player].each do |piece|
477 # Do a forward move for each card held
478 unless piece.position == @board.positions[-1]
479 @players_cards[player].each do |card|
480 destination = @board.positions[@board.positions.index(piece.position)..-1].find do |pos|
481 (pos.symbol == card and pos.contains == []) or
482 pos.symbol == :boat
483 end
484 # puts "Player #{player}, card #{card}, piece #{piece.number} at position #{@board.positions.index(piece.position)} to #{@board.positions.index(destination)}, a #{destination.symbol}"
485 moves << Move.new(piece, piece.position, destination, card)
486 end
487 end
488 # Do a reverse move for the piece
489 unless piece.position == board.positions[0]
490 destination = @board.positions[1...@board.positions.index(piece.position)].reverse.find do |pos|
491 pos.contains.length == 1 or pos.contains.length == 2
492 end
493 if destination
494 # puts "Player #{player}, piece #{piece.number} at position #{@board.positions.index(piece.position)} retreats to #{@board.positions.index(destination)}, a #{destination.symbol} containing #{destination.contains.length} pieces"
495 moves << Move.new(piece, piece.position, destination)
496 end
497 end
498 end
499 # moves.each {|m| puts m.show(@board)}
500 moves
501 end
502
503 def build_state_string
504 outstr = "Current player = #{@current_player}\n"
505 0.upto((@board.positions.length)-1) do |i|
506 outstr << "#{i}: #{@board.positions[i].symbol}: "
507 @board.positions[i].contains.each do |piece|
508 outstr << "P#{piece.player}:#{piece.number} "
509 end
510 outstr << "\n"
511 end
512 0.upto((@players.length)-1) do |i|
513 outstr << "Player #{i} holds " << (@players_cards[i].sort_by {|c| c.to_s}).join(', ') << "\n"
514 end
515 outstr << "Deck holds " << @deck.join(', ') << "\n"
516 outstr
517 end
518
519 # Show the state of the board
520 def show_state
521 puts build_state_string
522 # @pieces.keys.sort.each do |piece_name|
523 # if @pieces[piece_name].captured
524 # puts "Piece #{piece_name} captured, at #{@pieces[piece_name].position}"
525 # else
526 # puts "Piece #{piece_name} is at #{@pieces[piece_name].position}, holds #{(@pieces[piece_name].contains.collect{|c| c.name}).join(' ')}"
527 # end
528 # end
529 end
530
531 def to_s
532 show_state
533 end
534
535 def to_str
536 to_s
537 end
538
539
540 def set_testing_game!
541 srand 1234
542 @board = nil
543 initialize(5, 6, 6)
544 board_symbols = [:cell, :gun, :keys, :dagger, :hat, :skull, :bottle,
545 :keys, :dagger, :skull, :hat, :gun, :bottle,
546 :dagger, :bottle, :keys, :gun, :hat, :skull,
547 :dagger, :skull, :bottle, :gun, :keys, :hat,
548 :hat, :dagger, :keys, :bottle, :gun, :skull,
549 :keys, :bottle, :skull, :dagger, :hat, :gun, :boat]
550 board_symbols.each_index do |i|
551 @board.positions[i].symbol = board_symbols[i]
552 end
553
554 @players_cards[0] = [:gun, :hat, :skull]
555 @players_cards[1] = [:bottle, :keys, :keys]
556 @players_cards[2] = [:bottle, :hat, :keys]
557 @players_cards[3] = [:bottle, :skull, :skull]
558 @players_cards[4] = [:bottle, :gun, :gun]
559
560 @deck = [:hat, :dagger, :hat, :hat, :skull, :bottle, :bottle, :bottle, :dagger,
561 :keys, :hat, :dagger, :skull, :skull, :dagger, :gun, :skull, :gun,
562 :gun, :skull, :keys, :keys, :gun, :hat, :dagger, :keys, :dagger,
563 :hat, :gun, :skull, :keys, :gun, :skull, :gun, :hat, :gun, :dagger,
564 :gun, :gun, :hat, :hat, :bottle, :gun, :dagger, :hat, :skull, :dagger,
565 :bottle, :hat, :skull, :gun, :bottle, :keys, :hat, :bottle, :keys,
566 :dagger, :bottle, :bottle, :dagger, :keys, :dagger, :dagger, :dagger,
567 :skull, :hat, :dagger, :dagger, :hat, :keys, :skull, :bottle, :skull,
568 :keys, :bottle, :keys, :bottle, :gun, :keys, :hat, :keys, :dagger,
569 :gun, :skull, :keys, :bottle, :skull]
570
571 end
572
573 # Given a set of lines from an input file, turn them into a Game object and
574 # a set of Move objects.
575 # Note the multiple return values.
576 # Class method
577 def Game.read_game(gamelines)
578 gamelines.each {|l| l.chomp!}
579 # puts "Creating game with #{gamelines[0].to_i} players"
580 game = Game.new(gamelines[0].to_i, 6, 6)
581
582 # create the board
583 2.upto(38) do |i|
584 game.board.positions[i-1] = Position.new(gamelines[i].to_sym)
585 # puts "Making board position #{i-1} a #{gamelines[i].to_sym}"
586 end
587
588 # read all the moves so far
589 moves = []
590 i = 39
591 current_player_moves = 0
592 current_player = 0
593 player_card_counts = game.players_cards.map {|p| $INITIAL_CARDS_PER_PLAYER}
594 unused_cards = game.cards.map {|c| c}
595 while gamelines[i].in_move_format?
596 move = gamelines[i].to_move(game, true)
597 # puts "Applying move #{move.show(game.board)} from line #{i} of the file. Current player before move application is #{game.current_player}"
598 if move.piece.player == current_player
599 current_player_moves += 1
600 else
601 current_player = move.piece.player
602 current_player_moves = 1
603 end
604 # if move is an advance move, decrement the cards held by the player and remove the card from unused cards
605 # throw an error if an advance move is made when all the cards of that type have been used
606 # if unused_cards becomes empty, restock it.
607 if game.board.positions.index(move.destination) > game.board.positions.index(move.origin)
608 raise(InvalidMoveError, "Game reading: Player #{move.piece.player} attempting an advance move with no cards in hand. Move is #{gamelines[i]} on line #{i}") if player_card_counts[move.piece.player] == 0
609 raise(InvalidMoveError, "Game reading: Player #{move.piece.player} attempting an advance move to a #{move.destination.symbol} place when all those cards have been used. Move is #{gamelines[i]} on line #{i}") unless unused_cards.include? move.destination.symbol
610 player_card_counts[current_player] -= 1
611 unused_cards.delete_at(unused_cards.index(move.destination.symbol))
612 # if move is a retreat move, increment the cards held by the player
613 else
614 player_card_counts[current_player] += move.destination.contains.length
615 if unused_cards.length == player_card_counts.inject {|sum, n| sum + n }
616 unused_cards = game.cards.map {|c| c}
617 end
618 end
619 game.apply_move!(move, move.piece.player, false)
620 moves << move
621 # puts "Applied move #{move.show(game.board)} from line #{i} of the file"
622 i = i + 1
623 end
624
625 # note the current player
626 game.current_player = gamelines[i].to_i - 1
627 if game.current_player == current_player
628 game.moves_by_current_player = current_player_moves
629 else
630 game.moves_by_current_player = 0
631 end
632 # puts "Setting current player to #{game.current_player} on line #{i}"
633 current_player = game.current_player
634
635 # read the cards
636 game.players_cards = game.players_cards.map {|c| []}
637 # player_card_counts.each_index {|index| puts "#{index} should have #{player_card_counts[index]} cards"}
638 # game.players_cards.each_index {|index| puts "#{index} has #{game.players_cards[index]} cards"}
639 # puts "Current player is #{game.current_player} whose cards are #{game.players_cards[game.current_player]}"
640 (i+1).upto(gamelines.length - 1) do |j|
641 game.players_cards[game.current_player] << gamelines[j].to_sym
642 unused_cards.delete_at(unused_cards.index(gamelines[j].to_sym))
643 # puts "Reading line #{j} to give player #{game.current_player} a #{gamelines[j].to_sym} card"
644 end
645 raise(InvalidMoveError, "Player #{game.current_player} given #{game.players_cards[game.current_player].length} cards, but should have #{player_card_counts[game.current_player]} cards from play") if game.players_cards[game.current_player].length != player_card_counts[game.current_player]
646
647 # Update the game deck
648
649 puts "#{unused_cards.length} unused cards"
650 player_card_counts.each {|c| puts "has #{c} cards"}
651 game.deck = unused_cards.shuffle
652 player_card_counts[game.current_player] = 0
653 player_card_counts.each_index do |player|
654 if player_card_counts[player] > 0
655 game.deal_cards!(player_card_counts[player], player)
656 end
657 end
658 puts "#{game.deck.length} cards remaining in deck"
659 game.players_cards.each {|cards| puts "holds #{cards}"}
660 return game, moves
661 end
662
663 end
664
665
666 # Extension to String class to convert a move-description string into a Move object.
667 # This is the inverse of the Move#to_s method
668 class String
669 def in_move_format?
670 elements = split
671 return ((elements.length == 3 or elements.length == 4) and
672 elements[0].to_i > 0 and elements[0].to_i < 6 and
673 elements[1].to_i >= 0 and elements[1].to_i <= 37 and
674 elements[2].to_i >= 0 and elements[2].to_i <= 37)
675 end
676
677 def to_move(game, convert_to_zero_based_player_number = false)
678 move_elements = self.downcase.split
679 player = move_elements[0].to_i
680 player = player - 1 if convert_to_zero_based_player_number
681 origin_index = move_elements[1].to_i
682 destination_index = move_elements[2].to_i
683 raise(InvalidMoveError, "Invalid origin #{origin_index} in move read") unless origin_index >= 0 and origin_index < game.board.positions.length
684 raise(InvalidMoveError, "Invalid destination #{destination_index} in move read") unless destination_index > 0 and destination_index <= game.board.positions.length
685 raise(InvalidMoveError, "Player #{player} does not have a piece on position #{origin_index} in move read") unless game.board.positions[origin_index].contains.any? {|p| p.player == player}
686 piece = game.board.positions[origin_index].contains.find {|p| p.player == player}
687 piece_name = piece.number
688 if move_elements.length == 4
689 raise(InvalidMoveError, "Invalid card played: #{move_elements[3]}") unless $SYMBOLS.include?(move_elements[3].chomp.to_sym)
690 Move.new(game.pieces[player][piece_name],
691 game.board.positions[origin_index],
692 game.board.positions[destination_index],
693 move_elements[3].chomp.to_sym)
694 else
695 Move.new(game.pieces[player][piece_name],
696 game.board.positions[origin_index],
697 game.board.positions[destination_index])
698 end
699 end
700 end
701
702
703 # Read a game description file and convert it into a Game object and set of Move objects.
704 # Note that Game.read_game method returns multiple values, so this one does too.
705 class IO
706 def IO.read_game(filename)
707 gamelines = IO.readlines(filename)
708 return Game.read_game(gamelines)
709 end
710 end
711
712 # Extension to the Array class to include the Array#shuffle function
713 class Array
714 def shuffle
715 sort_by { rand }
716 end
717
718 def index_find(&block)
719 found = find(&block)
720 if found
721 index found
722 else
723 found
724 end
725 end
726 end