1 from random import choice
3 from naja.constants import(
4 BITS, DIRECTION_BITS, CONDITION_BITS, PLAYER_DEFAULTS,
5 ACT, EXAMINE, ROTATION)
6 from naja.options import options
7 from naja.player import Player
8 from naja import actions
9 from naja.sound import sound
10 from naja.utils import parse_bits
14 class GameBoard(object):
16 A representation of the game board.
19 def __init__(self, state, player, board_locations):
20 self.max_health = state['max_health']
21 self.wins_required = state['wins_required']
22 self.health = state['health']
23 self.wins = state['wins']
24 self.locations = [item.copy() for item in state['locations']]
25 self.puzzle = state.get('puzzle', False)
27 self.board_locations = board_locations
28 self.player_mode = state.get('player_mode', EXAMINE)
29 self.has_cheated = state.get('cheater', options.cheat_enabled)
30 self.clock_count = state.get('clock_count', 0)
31 self.replacement_params = state.get('replacement_params', None)
34 def new_game(cls, deck, initial_bits=None, initial_pos=None,
35 max_health=None, wins_required=None):
38 'initial_bits': PLAYER_DEFAULTS.INITIAL_BITS,
39 'initial_pos': PLAYER_DEFAULTS.INITIAL_POS,
40 'max_health': PLAYER_DEFAULTS.MAX_HEALTH,
41 'wins_required': PLAYER_DEFAULTS.WINS_REQUIRED,
44 deck_defaults = deck.get('defaults', {})
45 for k, v in deck_defaults.iteritems():
46 if isinstance(v, list):
47 deck_defaults[k] = tuple(v)
48 defaults.update(deck_defaults)
50 if initial_bits is None:
51 initial_bits = defaults['initial_bits']
52 if initial_pos is None:
53 initial_pos = defaults['initial_pos']
54 if max_health is None:
55 max_health = defaults['max_health']
56 if wins_required is None:
57 wins_required = defaults['wins_required']
59 assert wins_required + max_health == 8
61 # Overriden by command line
62 if options.initial_bits:
63 initial_bits = options.initial_bits
66 'max_health': max_health,
68 'wins_required': wins_required,
70 'locations': deck['cards'],
71 'puzzle': deck.get('puzzle', False),
73 'replacement_params': deck.get('replacement_params', None),
75 player = Player(initial_bits, initial_pos)
76 board_locations = cls.import_board_locations(
77 cls.generate_board(deck))
78 board = cls(state, player, board_locations)
79 player.set_gameboard(board)
83 def import_game(cls, definition):
84 state = definition.copy()
85 player = Player.import_player(state.pop('player'))
86 board_locations = cls.import_board_locations(
87 state.pop('board_locations'))
88 board = cls(state, player, board_locations)
89 player.set_gameboard(board)
94 'max_health': self.max_health,
95 'health': self.health,
96 'wins_required': self.wins_required,
98 'locations': [item.copy() for item in self.locations],
99 'puzzle': self.puzzle,
100 'player': self.player.export(),
101 'board_locations': self.export_board_locations(),
102 'player_mode': self.player_mode,
103 'clock_count': self.clock_count,
104 'replacement_params': self.replacement_params,
106 if options.cheat_enabled:
107 self.has_cheated = True
109 data['cheater'] = True
113 def import_locations(cls, locations_definition):
115 LocationCard.import_location(definition)
116 for definition in locations_definition]
118 def export_board_locations(self):
120 (position, location.export())
121 for position, location in self.board_locations.iteritems())
124 def import_board_locations(cls, board_locations_definition):
126 (tuple(position), LocationCard.import_location(definition))
127 for position, definition in board_locations_definition)
130 def generate_board(cls, deck):
131 if deck.get('puzzle', False):
132 return cls.generate_puzzle_board(deck)
134 return cls.generate_random_board(deck)
137 def generate_puzzle_board(cls, deck):
138 assert len(deck['cards']) == 5 * 5
139 replacement_params = deck.get('replacement_params', None)
142 LocationCard.new_location(
143 card.copy(), replacement_params, puzzle=True).export()]
144 for i, card in enumerate(deck['cards'])
146 return board_locations
149 def generate_random_board(cls, deck):
151 replacement_params = deck.get('replacement_params', None)
154 new_choice = cls.choose_card(deck['cards'], board_locations)
155 board_location = LocationCard.new_location(
156 new_choice.copy(), replacement_params)
157 board_locations.append([(x, y), board_location.export()])
158 return board_locations
160 def lose_health(self):
163 self.end_game(win=False)
165 def gain_health(self):
166 if self.health < self.max_health:
169 def acquire_win_token(self):
171 if self.wins >= self.wins_required:
172 self.end_game(win=True)
174 def card_used(self, position):
176 self.replace_card(position)
178 def replace_card(self, position):
179 new_choice = self.choose_card(self.locations,
180 self.board_locations.items(),
182 location = LocationCard.new_location(new_choice.copy(),
183 self.replacement_params)
184 self.board_locations[position] = location
187 def choose_card(cls, cards, board_locations, position=None):
188 # Find which cards are at their maximum and exclude them from
191 choices = {card['card_name']: card for card in cards}
192 for pos, card in board_locations:
194 # skip the card we're replacing if appropriate
196 if isinstance(card, LocationCard):
198 max_num = card.max_number
200 key = card['card_name']
201 max_num = card.get('max_number', 25)
202 counts.setdefault(key, 0)
204 if counts[key] >= max_num:
207 return choice(choices.values())
209 def shift_location_row(self, change, is_vertical):
210 px, py = self.player.position
211 shifted_locations = {}
212 mkpos = lambda i: (px, i) if is_vertical else (i, py)
215 if (px, py) == mkpos(i):
217 new_i = (i + change) % 5
218 if (px, py) == mkpos(new_i):
219 new_i = (new_i + change) % 5
220 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
222 self.board_locations.update(shifted_locations)
224 def shift_locations(self, direction):
225 if BITS[direction] == BITS.NORTH:
226 self.shift_location_row(-1, is_vertical=True)
227 elif BITS[direction] == BITS.SOUTH:
228 self.shift_location_row(1, is_vertical=True)
229 elif BITS[direction] == BITS.EAST:
230 self.shift_location_row(1, is_vertical=False)
231 elif BITS[direction] == BITS.WEST:
232 self.shift_location_row(-1, is_vertical=False)
234 def rotate_locations(self, direction):
235 px, py = self.player.position
236 locations_to_rotate = []
237 rotated_locations = {}
240 for i in range(max(0, px - 1), min(5, px + 2)):
241 locations_to_rotate.append((i, py - 1))
244 locations_to_rotate.append((px + 1, py))
247 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
248 locations_to_rotate.append((i, py + 1))
251 locations_to_rotate.append((px - 1, py))
253 if ROTATION[direction] == ROTATION.CLOCKWISE:
254 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
255 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
257 [locations_to_rotate[-1]] + locations_to_rotate[:-1])
259 for old, new in zip(locations_to_rotate, new_positions):
260 rotated_locations[new] = self.board_locations[old]
262 self.board_locations.update(rotated_locations)
264 def allow_chess_move(self, chesspiece):
265 self.player.allow_chess_move(chesspiece)
267 def change_mode(self, new_mode):
268 """Advance to the next mode"""
269 if new_mode == self.player_mode:
270 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
272 elif new_mode in (ACT, EXAMINE):
273 self.player_mode = new_mode
274 if new_mode is EXAMINE:
277 raise RuntimeError("Illegal player mode %s" % self.player_mode)
279 def board_update(self):
280 self.clock_count += 1
281 for position, location in self.board_locations.iteritems():
282 location.timer_action(position, self)
284 def end_game(self, win):
285 # TODO: Find a way to not have UI stuff in game logic stuff.
286 from naja.events import SceneChangeEvent
287 from naja.scenes.lose import LoseScene
288 from naja.scenes.win import WinScene
291 SceneChangeEvent.post(WinScene)
293 SceneChangeEvent.post(LoseScene)
296 class LocationCard(object):
298 A particular set of options available on a location.
301 def __init__(self, card_name, bitwise_operand, location_actions,
302 replacement_time=None, max_number=25):
303 self.card_name = card_name
304 self.bitwise_operand = bitwise_operand
305 self.actions = location_actions
306 self.max_number = max_number
307 self.replacement_time = replacement_time
310 def import_location(cls, state):
312 cls.build_action(definition) for definition in state['actions']]
313 return cls(state['card_name'], state['bitwise_operand'],
314 location_actions, state['replacement_time'],
318 def build_action(cls, definition):
319 action_class = getattr(actions, definition['action_class'])
320 required_bits = parse_bits(definition['required_bits'])
321 data = definition.get('data', {})
322 return action_class(required_bits, **data)
325 def new_location(cls, definition, replacement_params=None, puzzle=False):
326 if 'bits' in definition:
327 bits = parse_bits(definition['bits'])
329 bits = cls.generate_bitwise_operand()
331 if 'replacement_time' in definition:
332 replacement_time = definition['replacement_time']
334 replacement_time = cls.generate_replacement_time(
337 max_number = definition.get('max_number', 25)
338 card_name = definition['card_name']
339 location = cls.import_location({
340 'bitwise_operand': bits,
341 'actions': definition['actions'],
342 'max_number': max_number,
343 'card_name': card_name,
344 'replacement_time': replacement_time,
347 location.check_actions()
352 'bitwise_operand': sorted(self.bitwise_operand),
353 'actions': [action.export() for action in self.actions],
354 'max_number': self.max_number,
355 'card_name': self.card_name,
356 'replacement_time': self.replacement_time,
359 def check_actions(self):
361 print "Warning: Location has no actions."
362 self.insert_default_default_action()
363 if self.actions[0].required_bits:
364 self.insert_default_default_action()
366 def insert_default_default_action(self):
367 self.actions.insert(0, self.build_action({
368 'action_class': 'DoNothing',
373 def generate_bitwise_operand():
375 Generate a set of two or three bits. At least one direction and one
376 condition bit will be included. There is a low probability of choosing
377 a third bit from the complete set.
380 bits.add(choice(DIRECTION_BITS.values()))
381 bits.add(choice(CONDITION_BITS.values()))
382 # One in three chance of adding a third bit, with a further one in four
383 # chance that it will match a bit already chosen.
384 if choice(range(3)) == 0:
385 bits.add(choice(BITS.values()))
386 return frozenset(bits)
389 def generate_replacement_time(replacement_params):
390 if replacement_params is None:
393 if replacement_params['chance'] > random.random():
394 return random.randint(replacement_params['min'],
395 replacement_params['max'])
399 def timer_action(self, position, board):
400 if self.replacement_time is not None:
401 self.replacement_time -= 1
402 if self.replacement_time <= 0:
403 board.replace_card(position)