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
12 class GameBoard(object):
14 A representation of the game board.
17 def __init__(self, state, player, board_locations):
18 self.max_health = state['max_health']
19 self.wins_required = state['wins_required']
20 self.health = state['health']
21 self.wins = state['wins']
22 self.locations = [item.copy() for item in state['locations']]
23 self.puzzle = state.get('puzzle', False)
25 self.board_locations = board_locations
26 self.player_mode = state.get('player_mode', EXAMINE)
27 self.has_cheated = state.get('cheater', options.cheat_enabled)
30 def new_game(cls, deck,
31 initial_bits=PLAYER_DEFAULTS.INITIAL_BITS,
32 initial_pos=PLAYER_DEFAULTS.INITIAL_POS,
33 max_health=PLAYER_DEFAULTS.MAX_HEALTH,
34 wins_required=PLAYER_DEFAULTS.WINS_REQUIRED):
35 if options.initial_bits:
36 initial_bits = options.initial_bits
38 'max_health': max_health,
40 'wins_required': wins_required,
42 'locations': deck['cards'],
43 'puzzle': deck.get('puzzle', False),
45 player = Player(initial_bits, initial_pos)
46 board_locations = cls.import_board_locations(
47 cls.generate_board(deck))
48 return cls(state, player, board_locations)
51 def import_game(cls, definition):
52 state = definition.copy()
53 player = Player.import_player(state.pop('player'))
54 board_locations = cls.import_board_locations(
55 state.pop('board_locations'))
56 return cls(state, player, board_locations)
60 'max_health': self.max_health,
61 'health': self.health,
62 'wins_required': self.wins_required,
64 'locations': [item.copy() for item in self.locations],
65 'puzzle': self.puzzle,
66 'player': self.player.export(),
67 'board_locations': self.export_board_locations(),
68 'player_mode': self.player_mode,
71 data['cheater'] = True
75 def import_locations(cls, locations_definition):
77 LocationCard.import_location(definition)
78 for definition in locations_definition]
80 def export_board_locations(self):
82 (position, location.export())
83 for position, location in self.board_locations.iteritems())
86 def import_board_locations(cls, board_locations_definition):
88 (tuple(position), LocationCard.import_location(definition))
89 for position, definition in board_locations_definition)
92 def generate_board(cls, deck):
93 if deck.get('puzzle', False):
94 return cls.generate_puzzle_board(deck)
96 return cls.generate_random_board(deck)
99 def generate_puzzle_board(cls, deck):
100 assert len(deck['cards']) == 5 * 5
103 LocationCard.new_location(card.copy()).export()]
104 for i, card in enumerate(deck['cards'])
106 return board_locations
109 def generate_random_board(cls, deck):
113 new_choice = cls.choose_card(deck['cards'], board_locations)
114 board_location = LocationCard.new_location(new_choice.copy())
115 board_locations.append([(x, y), board_location.export()])
116 return board_locations
118 def lose_health(self):
121 self.end_game(win=False)
123 def gain_health(self):
124 if self.health < self.max_health:
127 def acquire_win_token(self):
129 if self.wins >= self.wins_required:
130 self.end_game(win=True)
132 def card_used(self, position):
134 self.replace_card(position)
136 def replace_card(self, position):
137 new_choice = self.choose_card(self.locations,
138 self.board_locations.items(),
140 location = LocationCard.new_location(new_choice.copy())
141 self.board_locations[position] = location
144 def choose_card(cls, cards, board_locations, position=None):
145 # Find which cards are at their maximum and exclude them from
148 choices = {card['card_name']: card for card in cards}
149 for pos, card in board_locations:
151 # skip the card we're replacing if appropriate
153 if isinstance(card, LocationCard):
155 max_num = card.max_number
157 key = card['card_name']
158 max_num = card.get('max_number', 25)
159 counts.setdefault(key, 0)
161 if counts[key] >= max_num:
164 return choice(choices.values())
166 def shift_location_row(self, change, is_vertical):
167 px, py = self.player.position
168 shifted_locations = {}
169 mkpos = lambda i: (px, i) if is_vertical else (i, py)
172 if (px, py) == mkpos(i):
174 new_i = (i + change) % 5
175 if (px, py) == mkpos(new_i):
176 new_i = (new_i + change) % 5
177 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
179 self.board_locations.update(shifted_locations)
181 def shift_locations(self, direction):
182 if BITS[direction] == BITS.NORTH:
183 self.shift_location_row(-1, is_vertical=True)
184 elif BITS[direction] == BITS.SOUTH:
185 self.shift_location_row(1, is_vertical=True)
186 elif BITS[direction] == BITS.EAST:
187 self.shift_location_row(1, is_vertical=False)
188 elif BITS[direction] == BITS.WEST:
189 self.shift_location_row(-1, is_vertical=False)
191 def rotate_locations(self, direction):
192 px, py = self.player.position
193 locations_to_rotate = []
194 rotated_locations = {}
197 for i in range(max(0, px - 1), min(5, px + 2)):
198 locations_to_rotate.append((i, py - 1))
201 locations_to_rotate.append((px + 1, py))
204 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
205 locations_to_rotate.append((i, py + 1))
208 locations_to_rotate.append((px - 1, py))
210 if ROTATION[direction] == ROTATION.CLOCKWISE:
211 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
212 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
213 new_positions = ([locations_to_rotate[-1]] + locations_to_rotate[:-1])
215 for old, new in zip(locations_to_rotate, new_positions):
216 rotated_locations[new] = self.board_locations[old]
218 self.board_locations.update(rotated_locations)
220 def allow_chess_move(self, chesspiece):
221 self.player.allow_chess_move(chesspiece)
223 def change_mode(self, new_mode):
224 """Advance to the next mode"""
225 if new_mode == self.player_mode:
226 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
228 elif new_mode in (ACT, EXAMINE):
229 self.player_mode = new_mode
231 raise RuntimeError("Illegal player mode %s" % self.player_mode)
233 def end_game(self, win):
234 # TODO: Find a way to not have UI stuff in game logic stuff.
235 from naja.events import SceneChangeEvent
236 from naja.scenes.lose import LoseScene
237 from naja.scenes.win import WinScene
240 SceneChangeEvent.post(WinScene)
242 SceneChangeEvent.post(LoseScene)
245 class LocationCard(object):
247 A particular set of options available on a location.
250 def __init__(self, card_name, bitwise_operand, location_actions,
252 self.card_name = card_name
253 self.bitwise_operand = bitwise_operand
254 self.actions = location_actions
255 self.max_number = max_number
259 def import_location(cls, state):
261 cls.build_action(definition) for definition in state['actions']]
262 return cls(state['card_name'], state['bitwise_operand'],
263 location_actions, state['max_number'])
266 def build_action(cls, definition):
267 action_class = getattr(actions, definition['action_class'])
268 required_bits = cls.parse_bits(definition['required_bits'])
269 data = definition.get('data', {})
270 return action_class(required_bits, **data)
273 def new_location(cls, definition):
274 if 'bits' in definition:
275 bits = cls.parse_bits(definition['bits'])
277 bits = cls.generate_bitwise_operand()
278 max_number = definition.get('max_number', 25)
279 card_name = definition['card_name']
280 return cls.import_location({
281 'bitwise_operand': bits,
282 'actions': definition['actions'],
283 'max_number': max_number,
284 'card_name': card_name,
288 def parse_bits(self, bit_list):
289 # Convert names to numbers if applicable.
290 return frozenset(BITS.get(bit, bit) for bit in bit_list)
294 'bitwise_operand': sorted(self.bitwise_operand),
295 'actions': [action.export() for action in self.actions],
296 'max_number': self.max_number,
297 'card_name': self.card_name,
300 def check_actions(self):
302 print "Warning: Location has no actions."
303 self.insert_default_default_action()
304 if self.actions[0].required_bits:
305 self.insert_default_default_action()
307 def insert_default_default_action(self):
308 self.actions.insert(0, self.build_action({
309 'action_class': 'DoNothing',
314 def generate_bitwise_operand():
316 Generate a set of two or three bits. At least one direction and one
317 condition bit will be included. There is a low probability of choosing
318 a third bit from the complete set.
321 bits.add(choice(DIRECTION_BITS.values()))
322 bits.add(choice(CONDITION_BITS.values()))
323 # One in three chance of adding a third bit, with a further one in four
324 # chance that it will match a bit already chosen.
325 if choice(range(3)) == 0:
326 bits.add(choice(BITS.values()))
327 return frozenset(bits)