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 board_location = LocationCard.new_location(
124 choice(deck['cards']).copy(), replacement_params)
125 board_locations.append([(x, y), board_location.export()])
126 return board_locations
128 def lose_health(self):
131 self.end_game(win=False)
133 def gain_health(self):
134 if self.health < self.max_health:
137 def acquire_win_token(self):
139 if self.wins >= self.wins_required:
140 self.end_game(win=True)
142 def card_used(self, position):
144 self.replace_card(position)
146 def replace_card(self, position):
147 location = LocationCard.new_location(choice(self.locations).copy(),
148 self.replacement_params)
149 self.board_locations[position] = location
151 def shift_location_row(self, change, is_vertical):
152 px, py = self.player.position
153 shifted_locations = {}
154 mkpos = lambda i: (px, i) if is_vertical else (i, py)
157 if (px, py) == mkpos(i):
159 new_i = (i + change) % 5
160 if (px, py) == mkpos(new_i):
161 new_i = (new_i + change) % 5
162 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
164 self.board_locations.update(shifted_locations)
166 def shift_locations(self, direction):
167 if BITS[direction] == BITS.NORTH:
168 self.shift_location_row(-1, is_vertical=True)
169 elif BITS[direction] == BITS.SOUTH:
170 self.shift_location_row(1, is_vertical=True)
171 elif BITS[direction] == BITS.EAST:
172 self.shift_location_row(1, is_vertical=False)
173 elif BITS[direction] == BITS.WEST:
174 self.shift_location_row(-1, is_vertical=False)
176 def rotate_locations(self, direction):
177 px, py = self.player.position
178 locations_to_rotate = []
179 rotated_locations = {}
182 for i in range(max(0, px - 1), min(5, px + 2)):
183 locations_to_rotate.append((i, py - 1))
186 locations_to_rotate.append((px + 1, py))
189 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
190 locations_to_rotate.append((i, py + 1))
193 locations_to_rotate.append((px - 1, py))
195 if ROTATION[direction] == ROTATION.CLOCKWISE:
196 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
197 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
198 new_positions = ([locations_to_rotate[-1]] + locations_to_rotate[:-1])
200 for old, new in zip(locations_to_rotate, new_positions):
201 rotated_locations[new] = self.board_locations[old]
203 self.board_locations.update(rotated_locations)
205 def allow_chess_move(self, chesspiece):
206 self.player.allow_chess_move(chesspiece)
208 def change_mode(self, new_mode):
209 """Advance to the next mode"""
210 if new_mode == self.player_mode:
211 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
213 elif new_mode in (ACT, EXAMINE):
214 self.player_mode = new_mode
215 if new_mode is EXAMINE:
218 raise RuntimeError("Illegal player mode %s" % self.player_mode)
220 def board_update(self):
221 self.clock_count += 1
222 for position, location in self.board_locations.iteritems():
223 location.timer_action(position, self)
225 def end_game(self, win):
226 # TODO: Find a way to not have UI stuff in game logic stuff.
227 from naja.events import SceneChangeEvent
228 from naja.scenes.lose import LoseScene
229 from naja.scenes.win import WinScene
232 SceneChangeEvent.post(WinScene)
234 SceneChangeEvent.post(LoseScene)
237 class LocationCard(object):
239 A particular set of options available on a location.
242 def __init__(self, card_name, bitwise_operand, location_actions,
244 self.card_name = card_name
245 self.bitwise_operand = bitwise_operand
246 self.actions = location_actions
248 self.replacement_time = replacement_time
251 def import_location(cls, state):
253 cls.build_action(definition) for definition in state['actions']]
254 return cls(state['card_name'], state['bitwise_operand'],
255 location_actions, state['replacement_time'])
258 def build_action(cls, definition):
259 action_class = getattr(actions, definition['action_class'])
260 required_bits = cls.parse_bits(definition['required_bits'])
261 data = definition.get('data', {})
262 return action_class(required_bits, **data)
265 def new_location(cls, definition, replacement_params):
266 if 'bits' in definition:
267 bits = cls.parse_bits(definition['bits'])
269 bits = cls.generate_bitwise_operand()
271 if 'replacement_time' in definition:
272 replacement_time = definition['replacement_time']
274 replacement_time = cls.generate_replacement_time(
277 card_name = definition['card_name']
278 return cls.import_location({
279 'bitwise_operand': bits,
280 'actions': definition['actions'],
281 'card_name': card_name,
282 'replacement_time': replacement_time,
286 def parse_bits(self, bit_list):
287 # Convert names to numbers if applicable.
288 return frozenset(BITS.get(bit, bit) for bit in bit_list)
292 'bitwise_operand': sorted(self.bitwise_operand),
293 'actions': [action.export() for action in self.actions],
294 'card_name': self.card_name,
295 'replacement_time': self.replacement_time,
298 def check_actions(self):
300 print "Warning: Location has no actions."
301 self.insert_default_default_action()
302 if self.actions[0].required_bits:
303 self.insert_default_default_action()
305 def insert_default_default_action(self):
306 self.actions.insert(0, self.build_action({
307 'action_class': 'DoNothing',
312 def generate_bitwise_operand():
314 Generate a set of two or three bits. At least one direction and one
315 condition bit will be included. There is a low probability of choosing
316 a third bit from the complete set.
319 bits.add(choice(DIRECTION_BITS.values()))
320 bits.add(choice(CONDITION_BITS.values()))
321 # One in three chance of adding a third bit, with a further one in four
322 # chance that it will match a bit already chosen.
323 if choice(range(3)) == 0:
324 bits.add(choice(BITS.values()))
325 return frozenset(bits)
328 def generate_replacement_time(replacement_params):
329 if replacement_params is None:
332 return random.randint(replacement_params[0], replacement_params[1])
334 def timer_action(self, position, board):
335 if self.replacement_time is not None:
336 self.replacement_time -= 1
337 if self.replacement_time <= 0:
338 board.replace_card(position)