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 board_location = LocationCard.new_location(
114 choice(deck['cards']).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 location = LocationCard.new_location(choice(self.locations).copy())
138 self.board_locations[position] = location
140 def shift_location_row(self, change, is_vertical):
141 px, py = self.player.position
142 shifted_locations = {}
143 mkpos = lambda i: (px, i) if is_vertical else (i, py)
146 if (px, py) == mkpos(i):
148 new_i = (i + change) % 5
149 if (px, py) == mkpos(new_i):
150 new_i = (new_i + change) % 5
151 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
153 self.board_locations.update(shifted_locations)
155 def shift_locations(self, direction):
156 if BITS[direction] == BITS.NORTH:
157 self.shift_location_row(-1, is_vertical=True)
158 elif BITS[direction] == BITS.SOUTH:
159 self.shift_location_row(1, is_vertical=True)
160 elif BITS[direction] == BITS.EAST:
161 self.shift_location_row(1, is_vertical=False)
162 elif BITS[direction] == BITS.WEST:
163 self.shift_location_row(-1, is_vertical=False)
165 def rotate_locations(self, direction):
166 px, py = self.player.position
167 locations_to_rotate = []
168 rotated_locations = {}
171 for i in range(max(0, px - 1), min(5, px + 2)):
172 locations_to_rotate.append((i, py - 1))
175 locations_to_rotate.append((px + 1, py))
178 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
179 locations_to_rotate.append((i, py + 1))
182 locations_to_rotate.append((px - 1, py))
184 print "rotating", direction
186 if ROTATION[direction] == ROTATION.CLOCKWISE:
188 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
189 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
190 print "ANTICLOCKWISE"
191 new_positions = ([locations_to_rotate[-1]] + locations_to_rotate[:-1])
193 for old, new in zip(locations_to_rotate, new_positions):
194 rotated_locations[new] = self.board_locations[old]
196 self.board_locations.update(rotated_locations)
198 def allow_chess_move(self, chesspiece):
199 self.player.allow_chess_move(chesspiece)
201 def change_mode(self, new_mode):
202 """Advance to the next mode"""
203 if new_mode == self.player_mode:
204 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
206 elif new_mode in (ACT, EXAMINE):
207 self.player_mode = new_mode
209 raise RuntimeError("Illegal player mode %s" % self.player_mode)
211 def end_game(self, win):
212 # TODO: Find a way to not have UI stuff in game logic stuff.
213 from naja.events import SceneChangeEvent
214 from naja.scenes.lose import LoseScene
215 from naja.scenes.win import WinScene
218 SceneChangeEvent.post(WinScene)
220 SceneChangeEvent.post(LoseScene)
223 class LocationCard(object):
225 A particular set of options available on a location.
228 def __init__(self, bitwise_operand, location_actions):
229 self.bitwise_operand = bitwise_operand
230 self.actions = location_actions
234 def import_location(cls, state):
236 cls.build_action(definition) for definition in state['actions']]
237 return cls(state['bitwise_operand'], location_actions)
240 def build_action(cls, definition):
241 action_class = getattr(actions, definition['action_class'])
242 required_bits = cls.parse_bits(definition['required_bits'])
243 data = definition.get('data', {})
244 return action_class(required_bits, **data)
247 def new_location(cls, definition):
248 if 'bits' in definition:
249 bits = cls.parse_bits(definition['bits'])
251 bits = cls.generate_bitwise_operand()
252 return cls.import_location({
253 'bitwise_operand': bits,
254 'actions': definition['actions'],
258 def parse_bits(self, bit_list):
259 # Convert names to numbers if applicable.
260 return frozenset(BITS.get(bit, bit) for bit in bit_list)
264 'bitwise_operand': sorted(self.bitwise_operand),
265 'actions': [action.export() for action in self.actions],
268 def check_actions(self):
270 print "Warning: Location has no actions."
271 self.insert_default_default_action()
272 if self.actions[0].required_bits:
273 self.insert_default_default_action()
275 def insert_default_default_action(self):
276 self.actions.insert(0, self.build_action({
277 'action_class': 'DoNothing',
282 def generate_bitwise_operand():
284 Generate a set of two or three bits. At least one direction and one
285 condition bit will be included. There is a low probability of choosing
286 a third bit from the complete set.
289 bits.add(choice(DIRECTION_BITS.values()))
290 bits.add(choice(CONDITION_BITS.values()))
291 # One in three chance of adding a third bit, with a further one in four
292 # chance that it will match a bit already chosen.
293 if choice(range(3)) == 0:
294 bits.add(choice(BITS.values()))
295 return frozenset(bits)