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
13 class GameBoard(object):
15 A representation of the game board.
18 def __init__(self, state, player, board_locations):
19 self.max_health = state['max_health']
20 self.wins_required = state['wins_required']
21 self.health = state['health']
22 self.wins = state['wins']
23 self.locations = [item.copy() for item in state['locations']]
24 self.puzzle = state.get('puzzle', False)
26 self.board_locations = board_locations
27 self.player_mode = state.get('player_mode', EXAMINE)
28 self.has_cheated = state.get('cheater', options.cheat_enabled)
29 self.clock_count = state.get('clock_count', 0)
30 self.replacement_params = state.get('replacement_params', None)
33 def new_game(cls, deck,
34 initial_bits=PLAYER_DEFAULTS.INITIAL_BITS,
35 initial_pos=PLAYER_DEFAULTS.INITIAL_POS,
36 max_health=PLAYER_DEFAULTS.MAX_HEALTH,
37 wins_required=PLAYER_DEFAULTS.WINS_REQUIRED):
38 if options.initial_bits:
39 initial_bits = options.initial_bits
41 'max_health': max_health,
43 'wins_required': wins_required,
45 'locations': deck['cards'],
46 'puzzle': deck.get('puzzle', False),
48 'replacement_params': deck.get('replacement_params', None),
50 player = Player(initial_bits, initial_pos)
51 board_locations = cls.import_board_locations(
52 cls.generate_board(deck))
53 return cls(state, player, board_locations)
56 def import_game(cls, definition):
57 state = definition.copy()
58 player = Player.import_player(state.pop('player'))
59 board_locations = cls.import_board_locations(
60 state.pop('board_locations'))
61 return cls(state, player, board_locations)
65 'max_health': self.max_health,
66 'health': self.health,
67 'wins_required': self.wins_required,
69 'locations': [item.copy() for item in self.locations],
70 'puzzle': self.puzzle,
71 'player': self.player.export(),
72 'board_locations': self.export_board_locations(),
73 'player_mode': self.player_mode,
74 'clock_count': self.clock_count,
75 'replacement_params': self.replacement_params,
78 data['cheater'] = True
82 def import_locations(cls, locations_definition):
84 LocationCard.import_location(definition)
85 for definition in locations_definition]
87 def export_board_locations(self):
89 (position, location.export())
90 for position, location in self.board_locations.iteritems())
93 def import_board_locations(cls, board_locations_definition):
95 (tuple(position), LocationCard.import_location(definition))
96 for position, definition in board_locations_definition)
99 def generate_board(cls, deck):
100 if deck.get('puzzle', False):
101 return cls.generate_puzzle_board(deck)
103 return cls.generate_random_board(deck)
106 def generate_puzzle_board(cls, deck):
107 assert len(deck['cards']) == 5 * 5
108 replacement_params = deck.get('replacement_params', None)
111 LocationCard.new_location(
112 card.copy(), replacement_params).export()]
113 for i, card in enumerate(deck['cards'])
115 return board_locations
118 def generate_random_board(cls, deck):
120 replacement_params = deck.get('replacement_params', None)
123 new_choice = cls.choose_card(deck['cards'], board_locations)
124 board_location = LocationCard.new_location(
125 new_choice.copy(), replacement_params)
126 board_locations.append([(x, y), board_location.export()])
127 return board_locations
129 def lose_health(self):
132 self.end_game(win=False)
134 def gain_health(self):
135 if self.health < self.max_health:
138 def acquire_win_token(self):
140 if self.wins >= self.wins_required:
141 self.end_game(win=True)
143 def card_used(self, position):
145 self.replace_card(position)
147 def replace_card(self, position):
148 new_choice = self.choose_card(self.locations,
149 self.board_locations.items(),
151 location = LocationCard.new_location(new_choice.copy(),
152 self.replacement_params)
153 self.board_locations[position] = location
156 def choose_card(cls, cards, board_locations, position=None):
157 # Find which cards are at their maximum and exclude them from
160 choices = {card['card_name']: card for card in cards}
161 for pos, card in board_locations:
163 # skip the card we're replacing if appropriate
165 if isinstance(card, LocationCard):
167 max_num = card.max_number
169 key = card['card_name']
170 max_num = card.get('max_number', 25)
171 counts.setdefault(key, 0)
173 if counts[key] >= max_num:
176 return choice(choices.values())
178 def shift_location_row(self, change, is_vertical):
179 px, py = self.player.position
180 shifted_locations = {}
181 mkpos = lambda i: (px, i) if is_vertical else (i, py)
184 if (px, py) == mkpos(i):
186 new_i = (i + change) % 5
187 if (px, py) == mkpos(new_i):
188 new_i = (new_i + change) % 5
189 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
191 self.board_locations.update(shifted_locations)
193 def shift_locations(self, direction):
194 if BITS[direction] == BITS.NORTH:
195 self.shift_location_row(-1, is_vertical=True)
196 elif BITS[direction] == BITS.SOUTH:
197 self.shift_location_row(1, is_vertical=True)
198 elif BITS[direction] == BITS.EAST:
199 self.shift_location_row(1, is_vertical=False)
200 elif BITS[direction] == BITS.WEST:
201 self.shift_location_row(-1, is_vertical=False)
203 def rotate_locations(self, direction):
204 px, py = self.player.position
205 locations_to_rotate = []
206 rotated_locations = {}
209 for i in range(max(0, px - 1), min(5, px + 2)):
210 locations_to_rotate.append((i, py - 1))
213 locations_to_rotate.append((px + 1, py))
216 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
217 locations_to_rotate.append((i, py + 1))
220 locations_to_rotate.append((px - 1, py))
222 if ROTATION[direction] == ROTATION.CLOCKWISE:
223 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
224 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
225 new_positions = ([locations_to_rotate[-1]] + locations_to_rotate[:-1])
227 for old, new in zip(locations_to_rotate, new_positions):
228 rotated_locations[new] = self.board_locations[old]
230 self.board_locations.update(rotated_locations)
232 def allow_chess_move(self, chesspiece):
233 self.player.allow_chess_move(chesspiece)
235 def change_mode(self, new_mode):
236 """Advance to the next mode"""
237 if new_mode == self.player_mode:
238 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
240 elif new_mode in (ACT, EXAMINE):
241 self.player_mode = new_mode
242 if new_mode is EXAMINE:
245 raise RuntimeError("Illegal player mode %s" % self.player_mode)
247 def board_update(self):
248 self.clock_count += 1
249 for position, location in self.board_locations.iteritems():
250 location.timer_action(position, self)
252 def end_game(self, win):
253 # TODO: Find a way to not have UI stuff in game logic stuff.
254 from naja.events import SceneChangeEvent
255 from naja.scenes.lose import LoseScene
256 from naja.scenes.win import WinScene
259 SceneChangeEvent.post(WinScene)
261 SceneChangeEvent.post(LoseScene)
264 class LocationCard(object):
266 A particular set of options available on a location.
269 def __init__(self, card_name, bitwise_operand, location_actions,
270 replacement_time, max_number=25):
271 self.card_name = card_name
272 self.bitwise_operand = bitwise_operand
273 self.actions = location_actions
274 self.max_number = max_number
276 self.replacement_time = replacement_time
279 def import_location(cls, state):
281 cls.build_action(definition) for definition in state['actions']]
282 return cls(state['card_name'], state['bitwise_operand'],
283 location_actions, state['replacement_time'],
287 def build_action(cls, definition):
288 action_class = getattr(actions, definition['action_class'])
289 required_bits = cls.parse_bits(definition['required_bits'])
290 data = definition.get('data', {})
291 return action_class(required_bits, **data)
294 def new_location(cls, definition, replacement_params):
295 if 'bits' in definition:
296 bits = cls.parse_bits(definition['bits'])
298 bits = cls.generate_bitwise_operand()
300 if 'replacement_time' in definition:
301 replacement_time = definition['replacement_time']
303 replacement_time = cls.generate_replacement_time(
306 max_number = definition.get('max_number', 25)
307 card_name = definition['card_name']
308 return cls.import_location({
309 'bitwise_operand': bits,
310 'actions': definition['actions'],
311 'max_number': max_number,
312 'card_name': card_name,
313 'replacement_time': replacement_time,
317 def parse_bits(self, bit_list):
318 # Convert names to numbers if applicable.
319 return frozenset(BITS.get(bit, bit) for bit in bit_list)
323 'bitwise_operand': sorted(self.bitwise_operand),
324 'actions': [action.export() for action in self.actions],
325 'max_number': self.max_number,
326 'card_name': self.card_name,
327 'replacement_time': self.replacement_time,
330 def check_actions(self):
332 print "Warning: Location has no actions."
333 self.insert_default_default_action()
334 if self.actions[0].required_bits:
335 self.insert_default_default_action()
337 def insert_default_default_action(self):
338 self.actions.insert(0, self.build_action({
339 'action_class': 'DoNothing',
344 def generate_bitwise_operand():
346 Generate a set of two or three bits. At least one direction and one
347 condition bit will be included. There is a low probability of choosing
348 a third bit from the complete set.
351 bits.add(choice(DIRECTION_BITS.values()))
352 bits.add(choice(CONDITION_BITS.values()))
353 # One in three chance of adding a third bit, with a further one in four
354 # chance that it will match a bit already chosen.
355 if choice(range(3)) == 0:
356 bits.add(choice(BITS.values()))
357 return frozenset(bits)
360 def generate_replacement_time(replacement_params):
361 if replacement_params is None:
364 if replacement_params['chance'] > random.random():
365 return random.randint(replacement_params['min'],
366 replacement_params['max'])
370 def timer_action(self, position, board):
371 if self.replacement_time is not None:
372 self.replacement_time -= 1
373 if self.replacement_time <= 0:
374 board.replace_card(position)