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