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 = dict((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, skip_player=True):
210 px, py = self.player.position
211 shifted_locations = {}
212 mkpos = lambda i: (px, i) if is_vertical else (i, py)
215 if skip_player and (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, skip_player=True):
225 if BITS[direction] == BITS.NORTH:
226 self.shift_location_row(-1, is_vertical=True,
227 skip_player=skip_player)
228 elif BITS[direction] == BITS.SOUTH:
229 self.shift_location_row(1, is_vertical=True,
230 skip_player=skip_player)
231 elif BITS[direction] == BITS.EAST:
232 self.shift_location_row(1, is_vertical=False,
233 skip_player=skip_player)
234 elif BITS[direction] == BITS.WEST:
235 self.shift_location_row(-1, is_vertical=False,
236 skip_player=skip_player)
238 def rotate_locations(self, direction):
239 px, py = self.player.position
240 locations_to_rotate = []
241 rotated_locations = {}
244 for i in range(max(0, px - 1), min(5, px + 2)):
245 locations_to_rotate.append((i, py - 1))
248 locations_to_rotate.append((px + 1, py))
251 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
252 locations_to_rotate.append((i, py + 1))
255 locations_to_rotate.append((px - 1, py))
257 if ROTATION[direction] == ROTATION.CLOCKWISE:
258 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
259 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
261 [locations_to_rotate[-1]] + locations_to_rotate[:-1])
263 for old, new in zip(locations_to_rotate, new_positions):
264 rotated_locations[new] = self.board_locations[old]
266 self.board_locations.update(rotated_locations)
268 def allow_chess_move(self, chesspiece):
269 self.player.allow_chess_move(chesspiece)
271 def change_mode(self, new_mode):
272 """Advance to the next mode"""
273 if new_mode == self.player_mode:
274 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
276 elif new_mode in (ACT, EXAMINE):
277 self.player_mode = new_mode
278 if new_mode is EXAMINE:
281 raise RuntimeError("Illegal player mode %s" % self.player_mode)
283 def board_update(self):
284 self.clock_count += 1
285 for position, location in self.board_locations.iteritems():
286 location.timer_action(position, self)
288 def end_game(self, win):
289 # TODO: Find a way to not have UI stuff in game logic stuff.
290 from naja.events import SceneChangeEvent
291 from naja.scenes.lose import LoseScene
292 from naja.scenes.win import WinScene
295 SceneChangeEvent.post(WinScene)
297 SceneChangeEvent.post(LoseScene)
300 class LocationCard(object):
302 A particular set of options available on a location.
305 def __init__(self, card_name, bitwise_operand, location_actions,
306 replacement_time=None, max_number=25):
307 self.card_name = card_name
308 self.bitwise_operand = bitwise_operand
309 self.actions = location_actions
310 self.max_number = max_number
311 self.replacement_time = replacement_time
313 for action in self.actions:
314 action.sanity_check(self)
317 def import_location(cls, state):
319 cls.build_action(definition) for definition in state['actions']]
320 return cls(state['card_name'], state['bitwise_operand'],
321 location_actions, state['replacement_time'],
325 def build_action(cls, definition):
326 action_class = getattr(actions, definition['action_class'])
327 required_bits = parse_bits(definition['required_bits'])
328 data = definition.get('data', {})
329 return action_class(required_bits, **data)
332 def new_location(cls, definition, replacement_params=None, puzzle=False):
333 if 'bits' in definition:
334 bits = parse_bits(definition['bits'])
336 bits = cls.generate_bitwise_operand()
338 if 'replacement_time' in definition:
339 replacement_time = definition['replacement_time']
341 replacement_time = cls.generate_replacement_time(
344 max_number = definition.get('max_number', 25)
345 card_name = definition['card_name']
346 location = cls.import_location({
347 'bitwise_operand': bits,
348 'actions': definition['actions'],
349 'max_number': max_number,
350 'card_name': card_name,
351 'replacement_time': replacement_time,
354 location.check_actions()
359 'bitwise_operand': sorted(self.bitwise_operand),
360 'actions': [action.export() for action in self.actions],
361 'max_number': self.max_number,
362 'card_name': self.card_name,
363 'replacement_time': self.replacement_time,
366 def check_actions(self):
368 print "Warning: Location has no actions."
369 self.insert_default_default_action()
370 if self.actions[0].required_bits:
371 self.insert_default_default_action()
373 def insert_default_default_action(self):
374 self.actions.insert(0, self.build_action({
375 'action_class': 'DoNothing',
380 def generate_bitwise_operand():
382 Generate a set of two or three bits. At least one direction and one
383 condition bit will be included. There is a low probability of choosing
384 a third bit from the complete set.
387 bits.add(choice(DIRECTION_BITS.values()))
388 bits.add(choice(CONDITION_BITS.values()))
389 # One in three chance of adding a third bit, with a further one in four
390 # chance that it will match a bit already chosen.
391 if choice(range(3)) == 0:
392 bits.add(choice(BITS.values()))
393 return frozenset(bits)
396 def generate_replacement_time(replacement_params):
397 if replacement_params is None:
400 if replacement_params['chance'] > random.random():
401 return random.randint(replacement_params['min'],
402 replacement_params['max'])
406 def timer_action(self, position, board):
407 if self.replacement_time is not None:
408 self.replacement_time -= 1
409 if self.replacement_time <= 0:
410 board.replace_card(position)