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