Initial commit
[cartagena.git] / lib / .svn / tmp / tempfile.2.tmp
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 # Errors for a game
38
39 # Moves can only be [1..6] spaces
40 class InvalidMoveError < StandardError
41 end
42
43 # Game is won when only one player has uncaptured pieces
44 class GameWonNotice < StandardError
45 end
46
47 # A position on the board.
48 class Position
49 attr_reader :symbol, :contains
50
51 def initialize(symbol)
52 @symbol = symbol
53 @contains = []
54 end
55 end
56
57 # A tile that makes up the board. Each tile has two sides, each with six
58 # positions. Tiles can be either way up, and either way round.
59 class Tile
60 attr_reader :exposed, :front, :back
61
62 def initialize(front, back)
63 @front = front.collect {|s| Position.new(s)}
64 @back = back.collect {|s| Position.new(s)}
65 @exposed = @front
66 @exposed_side = :front
67 end
68
69 def flip!
70 if @exposed_side == :front
71 @exposed = @back
72 @exposed_side = :back
73 else
74 @exposed = @front
75 @exposed_side = :front
76 end
77 end
78
79 def reverse!
80 @exposed = @exposed.reverse
81 end
82 end
83
84
85 # The game board
86 class Board
87
88 attr_reader :positions
89 attr_reader :tiles
90
91 # A laborious procedure to create all the positions and tie them all together
92 def initialize(tiles_used)
93 # A hash of all positions, indexed by position names
94 @tiles = [Tile.new([:hat, :keys, :gun, :bottle, :skull, :dagger],
95 [:hat, :keys, :gun, :bottle, :skull, :dagger]),
96 Tile.new([:gun, :hat, :dagger, :skull, :bottle, :keys],
97 [:gun, :hat, :dagger, :skull, :bottle, :keys]),
98 Tile.new([:skull, :gun, :bottle, :keys, :dagger, :hat],
99 [:skull, :gun, :bottle, :keys, :dagger, :hat]),
100 Tile.new([:dagger, :bottle, :keys, :gun, :hat, :skull],
101 [:dagger, :bottle, :keys, :gun, :hat, :skull]),
102 Tile.new([:keys, :dagger, :skull, :hat, :gun, :bottle],
103 [:keys, :dagger, :skull, :hat, :gun, :bottle]),
104 Tile.new([:bottle, :skull, :hat, :dagger, :keys, :gun],
105 [:bottle, :skull, :hat, :dagger, :keys, :gun])
106 ].shuffle[0...tiles_used]
107 @tiles = @tiles.each do |t|
108 if rand < 0.5
109 t.reverse!
110 else
111 t
112 end
113 end
114
115 @positions = [Position.new(:cell)]
116 @tiles.each {|t| t.exposed.each {|p| @positions << p}}
117 @positions << Position.new(:boat)
118 end # def
119
120 def to_s
121 layout
122 end
123
124 def to_str
125 to_s
126 end
127
128
129 # For each position, show its name and what it touches
130 def layout
131 out_string = ""
132 @positions.each {|position| out_string << "#{position}\n"}
133 out_string
134 end
135
136 end
137
138
139 # Each piece on the board is an object
140 class Piece
141 attr_reader :player, :number
142 attr_accessor :position
143
144 def initialize(position, player, number)
145 @position = position
146 @player = player
147 @number = number
148 end
149
150 def to_s
151 "#{@player}:#{@number}"
152 end
153
154 def to_str
155 to_s
156 end
157
158 def move_to(new_position)
159 @position = new_position
160 end
161
162 end
163
164
165 # A move in a game
166 class Move
167 attr_reader :piece, :origin, :destination
168
169 def initialize(piece, origin, destination)
170 @piece = piece
171 @origin = origin
172 @destination = destination
173 end
174
175 def show(board)
176 "#{@piece.to_s}: #{board.positions.index(@origin)} -> #{board.positions.index(@destination)}"
177 end
178
179 # Write a move to a string
180 # Note the inverse, String#to_move, is defined below
181 def to_s
182 "#{@piece.player}: #{@origin.to_s} -> #{@destination.to_s}"
183 end
184
185 def to_str
186 to_s
187 end
188
189 end
190
191
192
193 # A class to record each of the states previously found in a game.
194 # Note that this is a deep copy of the pieces and what they've captured, so
195 # you'll need to convert back
196 class GameState
197 attr_accessor :move, :player, :pieces_after_move
198
199 def initialize(move, player, pieces)
200 @move = move
201 @player = player
202 @pieces_after_move = Hash.new
203 pieces.each {|k, p| @pieces_after_move[k] = p.dup}
204 end
205
206 def ==(other)
207 @move.to_s == other.move.to_s and
208 @player == other.player and
209 @piece_after_move == other.pieces_after_move
210 end
211
212 end
213
214
215 # A game of Cartagena. It keeps a history of all previous states.
216 class Game
217
218 attr_reader :history
219 attr_reader :current_player, :players, :players_cards
220 attr_reader :board
221 attr_reader :pieces
222 attr_reader :deck
223
224 # Create a new game
225 def initialize(players = 6, number_of_tiles = 6, pieces_each = 6)
226 @board = Board.new(number_of_tiles)
227 @history = []
228 @pieces = []
229 @players = [].fill(0, players) {|i| i}
230 @players_cards = []
231 @current_player = 0
232 @cards = []
233 1.upto($CARDS_PER_SYMBOL) {|x| @cards.concat($SYMBOLS)}
234 @deck = @cards.shuffle
235 @players.each do |p|
236 @pieces[p] = []
237 0.upto(pieces_each - 1) do |count|
238 piece = Piece.new(@board.positions[0], p, count)
239 @pieces[p][count] = piece
240 @board.positions[0].contains << piece
241 end
242 @players_cards[p] = []
243 deal_cards!($INITIAL_CARDS_PER_PLAYER, p)
244 end
245 end
246
247 def deal_cards!(number_of_cards, player = @current_player)
248 1.upto(number_of_cards) do
249 @deck = @cards.shuffle if @deck.empty?
250 @players_cards[player] << @deck.pop
251 end
252 end
253
254 # Check that a move is valid. Throw an exception if it's invalid
255 def validate_move(move, player = @current_player)
256 # Check the move is a valid one
257 raise(InvalidMoveError, "Move #{move}: Player #{player} does not exist") unless @players.include?(player)
258 raise(InvalidMoveError, "Move #{move}: None of player #{player}'s pieces on position #{move.origin}") unless move.origin.contains.find {|pc| pc.player == player}
259 raise(InvalidMoveError, "Move #{move}: Origin and destination are the same") if move.origin == move.destination
260
261 origin_position = @board.positions.index(move.origin)
262 destination_position = @board.positions.index(move.destination)
263 # Is this move an advance or a retreat?
264 if destination_position > origin_position
265 # Advancing a piece
266 unless @players_cards[player].find {|c| c == move.destination.symbol}
267 raise(InvalidMoveError, "Player #{player} does not have a card to move a piece to a #{move.destination.symbol} square")
268 end
269 # Check target square is vacant
270 raise(InvalidMoveError, "Advance move #{move}: destination occupied") unless move.destination.contains.empty?
271 # Check all the intervening squares with this symbol are occupied
272 intervening_empty_position = @board.positions[origin_position...destination_position].index_find do |p|
273 p.symbol == move.destination.symbol and
274 p.contains.empty?
275 end
276 raise(InvalidMoveError, "Advance move #{move}: location #{intervening_empty_position} is empty") if intervening_empty_position
277 else
278 # Retreating a piece
279 # Check target position has one or two pieces already on it
280 destination_count = move.destination.contains.length
281 raise(InvalidMoveError, "Retreat move #{move}: destination has no pieces already on it") if destination_count == 0
282 raise(InvalidMoveError, "Retreat move #{move}: destination has too many (#{destination_count}) pieces already on it") if destination_count >= $MAX_PIECES_PER_POSITION
283 # Check none of the intervening squares have any pieces on them
284 intervening_target_position = @board.positions[destination_position...origin_position].index_find do |p|
285 p.contains.length > 0 and
286 p.contains.length < $MAX_PIECES_PER_POSITION
287 end
288 raise(InvalidMoveError, "Retreat move #{move}: location #{intervening_target_position} is a viable target") if intervening_target_position
289 end
290 end
291
292 # Apply a single move to a game.
293 def apply_move!(move, player = @current_player)
294
295 validate_move(move, player)
296
297 # Apply this move
298 move.origin.contains.delete move.piece
299 move.destination.contains << move.piece
300 move.piece.position = move.destination
301
302 # Update cards
303 if @board.positions.index(move.destination) > @board.positions.index(move.origin)
304 # Advance move
305 @players_cards[player].delete_at(@players_cards[player].index(move.destination.symbol))
306 else
307 # Retreat move
308 deal_cards!(move.destination.contains.length, player)
309 end
310
311 # Record the new stae
312 # this_game_state = GameState.new(move, player, @pieces)
313 # @history << this_game_state
314
315 # If this player has all their pieces in the boat, declare a win.
316 if @pieces[player].all? {|pc| pc.position == :boat}
317 raise(GameWonNotice, "Game won by #{player}")
318 end
319 end
320
321 # Undo a move
322 def undo_move!
323 if @history.length > 1
324 # general case
325 state_to_restore = @history[-2]
326 @current_player = @history[-1].player
327 @pieces.each do |name, piece|
328 copy_piece = state_to_restore.pieces_after_move[name]
329 piece.position = copy_piece.position
330 end
331 @history.pop
332 elsif @history.length == 1
333 # reset to start
334 @current_player = @players[1]
335 @pieces.each do |name, piece|
336 piece.position = @board.positions[piece.colour]
337 end
338 @history.pop
339 end
340 end
341
342 # Apply a list of moves in order
343 def apply_moves!(moves)
344 moves.each do |move|
345 if move.via_base?
346 moved_distance = board.distance_between[move.piece.position.place][@current_player] +
347 board.distance_between[@current_player][move.destination.place]
348 else
349 moved_distance = board.distance_between[move.piece.position.place][move.destination.place]
350 end
351 self.apply_move!(move, @current_player)
352 next_player! unless moved_distance == 6
353 end
354 end
355
356
357 # Set the current player to be the next player
358 def next_player!
359 original_player = @current_player
360 if @current_player == @players[-1]
361 @current_player = @players[1]
362 else
363 @current_player = @players[@players.index(@current_player) + 1]
364 end
365 @current_player
366 end
367
368
369 # Return an array of all possible moves from this state, given the active player
370 def possible_moves(player = @current_player)
371 moves = []
372 @pieces[player].each do |piece|
373 # Do a forward move for each card held
374 unless piece.position == @board.positions[-1]
375 @players_cards[player].each do |card|
376 puts "Player #{player}, card #{card}, piece #{piece.number} at position #{@board.positions.index(piece.position)}"
377 destination = @board.positions[@board.positions.index(piece.position)..-1].find do |pos|
378 (pos.symbol == card and pos.contains == []) or
379 pos.symbol == :boat
380 end
381 moves << Move.new(piece, piece.position, destination)
382 end
383 end
384 # Do a reverse move for the piece
385 unless piece.position == board.positions[0]
386 destination = @board.positions[0...@board.positions.index(piece.position)].reverse.find do |pos|
387 pos.contains.length == 1 or pos.contains.length == 2
388 end
389 moves << Move.new(piece, piece.position, destination)
390 end
391 end
392 moves
393 end
394
395
396 def build_state_string
397 outstr = "Current player = #{@current_player}\n"
398 0.upto((@board.positions.length)-1) do |i|
399 outstr << "#{i}: #{@board.positions[i].symbol}: "
400 @board.positions[i].contains.each do |piece|
401 outstr << "P#{piece.player}:#{piece.number} "
402 end
403 outstr << "\n"
404 end
405 0.upto((@players.length)-1) do |i|
406 outstr << "Player #{i} holds " << (@players_cards[i].sort_by {|c| c.to_s}).join(', ') << "\n"
407 end
408 outstr << "Deck holds " << @deck.join(', ') << "\n"
409 outstr
410 end
411
412 # Show the state of the board
413 def show_state
414 puts build_state_string
415 # @pieces.keys.sort.each do |piece_name|
416 # if @pieces[piece_name].captured
417 # puts "Piece #{piece_name} captured, at #{@pieces[piece_name].position}"
418 # else
419 # puts "Piece #{piece_name} is at #{@pieces[piece_name].position}, holds #{(@pieces[piece_name].contains.collect{|c| c.name}).join(' ')}"
420 # end
421 # end
422 end
423
424 def to_s
425 show_state
426 end
427
428 def to_str
429 to_s
430 end
431
432
433 # Given a set of lines from an input file, turn them into a Game object and
434 # a set of Move objects.
435 # Note the multiple return values.
436 # Class method
437 def Game.read_game(gamelines)
438 gamelines.each {|l| l.chomp!}
439 game = Game.new(gamelines[0].to_i, 6, 6, 6, 6)
440 moves = []
441 gamelines[1..-2].each {|m| moves << m.to_move(game)}
442 return game, moves, gamelines[-1].to_i
443 end
444
445
446 end
447
448
449 # Extension to String class to convert a move-description string into a Move object.
450 # This is the inverse of the Move#to_s method
451 class String
452 def to_move(game)
453 move_elements = self.downcase.split
454 piece_name = move_elements[0]
455 destination_name = move_elements[-1]
456 if destination_name.length > 2 and
457 destination_name[-2,2] == game.board.centre.place[-2,2]
458 destination_name = game.board.centre.place
459 end
460 raise(InvalidMoveError, "Invalid piece in move read") unless game.pieces.has_key?(piece_name)
461 raise(InvalidMoveError, "Invalid destination in move read") unless game.board.positions.has_key?(destination_name)
462 # Deal with the synonyms for the centre position
463 via_base = (destination_name.length == 1 or move_elements.length > 2)
464 Move.new(game.pieces[piece_name], game.board.positions[destination_name], via_base)
465 end
466 end
467
468
469 # Read a game description file and convert it into a Game object and set of Move objects.
470 # Note that Game.read_game method returns multiple values, so this one does too.
471 class IO
472 def IO.read_game(filename)
473 gamelines = IO.readlines(filename)
474 return Game.read_game(gamelines)
475 end
476 end
477
478 # Extension to the Array class to include the Array#shuffle function
479 class Array
480 def shuffle
481 sort_by { rand }
482 end
483
484 <<<<<<< .mine
485 def index_find(&proc)
486 self.index(self.find &proc) # added some additional comments
487 =======
488 def index_find(&block)
489 found = find(&block)
490 if found
491 index found
492 else
493 found
494 end
495 >>>>>>> .r32
496 end
497 end