Initial commit
[cartagena.git] / lib / .svn / tmp / tempfile.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.symbol}\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 # puts "Checking positions #{destination_position} to #{origin_position}"
285 intervening_target_position = @board.positions[(destination_position + 1)...origin_position].index_find do |p|
286 # puts "Examining postition #{p} at location #{@board.positions.index(p)} which contains #{p.contains.length} pieces"
287 p.contains.length > 0 and
288 p.contains.length < $MAX_PIECES_PER_POSITION
289 end
290 raise(InvalidMoveError, "Retreat move #{move.show(@board)}: location #{intervening_target_position} is a viable target") if intervening_target_position
291 end
292 end
293
294 # Apply a single move to a game.
295 def apply_move!(move, player = @current_player)
296
297 validate_move(move, player)
298
299 # Apply this move
300 move.origin.contains.delete move.piece
301 move.destination.contains << move.piece
302 move.piece.position = move.destination
303
304 # Update cards
305 if @board.positions.index(move.destination) > @board.positions.index(move.origin)
306 # Advance move
307 @players_cards[player].delete_at(@players_cards[player].index(move.destination.symbol))
308 else
309 # Retreat move
310 deal_cards!(move.destination.contains.length - 1, player)
311 end
312
313 # Record the new stae
314 # this_game_state = GameState.new(move, player, @pieces)
315 # @history << this_game_state
316
317 # If this player has all their pieces in the boat, declare a win.
318 if @pieces[player].all? {|pc| pc.position == :boat}
319 raise(GameWonNotice, "Game won by #{player}")
320 end
321 end
322
323 # Undo a move
324 def undo_move!
325 if @history.length > 1
326 # general case
327 state_to_restore = @history[-2]
328 @current_player = @history[-1].player
329 @pieces.each do |name, piece|
330 copy_piece = state_to_restore.pieces_after_move[name]
331 piece.position = copy_piece.position
332 end
333 @history.pop
334 elsif @history.length == 1
335 # reset to start
336 @current_player = @players[1]
337 @pieces.each do |name, piece|
338 piece.position = @board.positions[piece.colour]
339 end
340 @history.pop
341 end
342 end
343
344 # Apply a list of moves in order
345 def apply_moves!(moves)
346 moves.each do |move|
347 if move.via_base?
348 moved_distance = board.distance_between[move.piece.position.place][@current_player] +
349 board.distance_between[@current_player][move.destination.place]
350 else
351 moved_distance = board.distance_between[move.piece.position.place][move.destination.place]
352 end
353 self.apply_move!(move, @current_player)
354 next_player! unless moved_distance == 6
355 end
356 end
357
358
359 # Set the current player to be the next player
360 def next_player!
361 original_player = @current_player
362 if @current_player == @players[-1]
363 @current_player = @players[1]
364 else
365 @current_player = @players[@players.index(@current_player) + 1]
366 end
367 @current_player
368 end
369
370
371 # Return an array of all possible moves from this state, given the active player
372 def possible_moves(player = @current_player)
373 moves = []
374 @pieces[player].each do |piece|
375 # Do a forward move for each card held
376 unless piece.position == @board.positions[-1]
377 @players_cards[player].each do |card|
378 destination = @board.positions[@board.positions.index(piece.position)..-1].find do |pos|
379 (pos.symbol == card and pos.contains == []) or
380 pos.symbol == :boat
381 end
382 <<<<<<< .mine
383 puts "Player #{player}, card #{card}, piece #{piece.number} at position #{@board.positions.index(piece.position)} moving to #{@board.positions.index(destination)}, a #{destination.symbol}"
384 =======
385 # puts "Player #{player}, card #{card}, piece #{piece.number} at position #{@board.positions.index(piece.position)} to #{@board.positions.index(destination)}, a #{destination.symbol}"
386 >>>>>>> .r34
387 moves << Move.new(piece, piece.position, destination)
388 puts "Added move #{piece.number}: #{piece.position.symbol} -> #{destination.symbol}"
389 end
390 end
391 # Do a reverse move for the piece
392 unless piece.position == board.positions[0]
393 destination = @board.positions[0...@board.positions.index(piece.position)].reverse.find do |pos|
394 pos.contains.length == 1 or pos.contains.length == 2
395 end
396 if destination
397 # 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"
398 moves << Move.new(piece, piece.position, destination)
399 end
400 end
401 end
402 # moves.each {|m| puts m.show(@board)}
403 moves
404 end
405
406 def build_state_string
407 outstr = "Current player = #{@current_player}\n"
408 0.upto((@board.positions.length)-1) do |i|
409 outstr << "#{i}: #{@board.positions[i].symbol}: "
410 @board.positions[i].contains.each do |piece|
411 outstr << "P#{piece.player}:#{piece.number} "
412 end
413 outstr << "\n"
414 end
415 0.upto((@players.length)-1) do |i|
416 outstr << "Player #{i} holds " << (@players_cards[i].sort_by {|c| c.to_s}).join(', ') << "\n"
417 end
418 outstr << "Deck holds " << @deck.join(', ') << "\n"
419 outstr
420 end
421
422 # Show the state of the board
423 def show_state
424 puts build_state_string
425 # @pieces.keys.sort.each do |piece_name|
426 # if @pieces[piece_name].captured
427 # puts "Piece #{piece_name} captured, at #{@pieces[piece_name].position}"
428 # else
429 # puts "Piece #{piece_name} is at #{@pieces[piece_name].position}, holds #{(@pieces[piece_name].contains.collect{|c| c.name}).join(' ')}"
430 # end
431 # end
432 end
433
434 def to_s
435 show_state
436 end
437
438 def to_str
439 to_s
440 end
441
442
443 def set_testing_game!
444 srand 1234
445 @board = nil
446 initialize(6, 6, 6)
447 end
448
449 # Given a set of lines from an input file, turn them into a Game object and
450 # a set of Move objects.
451 # Note the multiple return values.
452 # Class method
453 def Game.read_game(gamelines)
454 gamelines.each {|l| l.chomp!}
455 game = Game.new(gamelines[0].to_i, 6, 6, 6, 6)
456 moves = []
457 gamelines[1..-2].each {|m| moves << m.to_move(game)}
458 return game, moves, gamelines[-1].to_i
459 end
460
461
462 end
463
464
465 # Extension to String class to convert a move-description string into a Move object.
466 # This is the inverse of the Move#to_s method
467 class String
468 def to_move(game)
469 move_elements = self.downcase.split
470 piece_name = move_elements[0]
471 destination_name = move_elements[-1]
472 if destination_name.length > 2 and
473 destination_name[-2,2] == game.board.centre.place[-2,2]
474 destination_name = game.board.centre.place
475 end
476 raise(InvalidMoveError, "Invalid piece in move read") unless game.pieces.has_key?(piece_name)
477 raise(InvalidMoveError, "Invalid destination in move read") unless game.board.positions.has_key?(destination_name)
478 # Deal with the synonyms for the centre position
479 via_base = (destination_name.length == 1 or move_elements.length > 2)
480 Move.new(game.pieces[piece_name], game.board.positions[destination_name], via_base)
481 end
482 end
483
484
485 # Read a game description file and convert it into a Game object and set of Move objects.
486 # Note that Game.read_game method returns multiple values, so this one does too.
487 class IO
488 def IO.read_game(filename)
489 gamelines = IO.readlines(filename)
490 return Game.read_game(gamelines)
491 end
492 end
493
494 # Extension to the Array class to include the Array#shuffle function
495 class Array
496 def shuffle
497 sort_by { rand }
498 end
499
500 def index_find(&block)
501 found = find(&block)
502 if found
503 index found
504 else
505 found
506 end
507 end
508 end