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
11 class GameBoard(object):
13 A representation of the game board.
16 def __init__(self, state, player, board_locations):
17 self.max_health = state['max_health']
18 self.wins_required = state['wins_required']
19 self.health = state['health']
20 self.wins = state['wins']
21 self.locations = [item.copy() for item in state['locations']]
22 self.puzzle = state['puzzle']
24 self.board_locations = board_locations
25 self.player_mode = EXAMINE
28 def new_game(cls, deck,
29 initial_bits=PLAYER_DEFAULTS.INITIAL_BITS,
30 initial_pos=PLAYER_DEFAULTS.INITIAL_POS,
31 max_health=PLAYER_DEFAULTS.MAX_HEALTH,
32 wins_required=PLAYER_DEFAULTS.WINS_REQUIRED):
33 if options.initial_bits:
34 initial_bits = options.initial_bits
36 'max_health': max_health,
38 'wins_required': wins_required,
40 'locations': deck['cards'],
41 'puzzle': deck.get('puzzle', False),
43 player = Player(initial_bits, initial_pos)
44 board_locations = cls.import_board_locations(
45 cls.generate_board(deck))
46 return cls(state, player, board_locations)
49 def import_game(cls, definition):
50 state = definition.copy()
51 player = Player.import_player(state.pop('player'))
52 board_locations = cls.import_board_locations(
53 state.pop('board_locations'))
54 return cls(state, player, board_locations)
58 'max_health': self.max_health,
59 'health': self.health,
60 'wins_required': self.wins_required,
62 'locations': [item.copy() for item in self.locations],
63 'puzzle': self.puzzle,
64 'player': self.player.export(),
65 'board_locations': self.export_board_locations(),
69 def import_locations(cls, locations_definition):
71 LocationCard.import_location(definition)
72 for definition in locations_definition]
74 def export_board_locations(self):
76 (position, location.export())
77 for position, location in self.board_locations.iteritems())
80 def import_board_locations(cls, board_locations_definition):
82 (tuple(position), LocationCard.import_location(definition))
83 for position, definition in board_locations_definition)
86 def generate_board(cls, deck):
87 if deck.get('puzzle', False):
88 return cls.generate_puzzle_board(deck)
90 return cls.generate_random_board(deck)
93 def generate_puzzle_board(cls, deck):
94 assert len(deck['cards']) == 5 * 5
97 LocationCard.new_location(card.copy()).export()]
98 for i, card in enumerate(deck['cards'])
100 return board_locations
103 def generate_random_board(cls, deck):
107 board_location = LocationCard.new_location(
108 choice(deck['cards']).copy())
109 board_locations.append([(x, y), board_location.export()])
110 return board_locations
112 def lose_health(self):
115 self.end_game(win=False)
117 def gain_health(self):
118 if self.health < self.max_health:
121 def acquire_win_token(self):
123 if self.wins >= self.wins_required:
124 self.end_game(win=True)
126 def card_used(self, position):
128 self.replace_card(position)
130 def replace_card(self, position):
131 location = LocationCard.new_location(choice(self.locations).copy())
132 self.board_locations[position] = location
134 def shift_location_row(self, change, is_vertical):
135 px, py = self.player.position
136 shifted_locations = {}
137 mkpos = lambda i: (px, i) if is_vertical else (i, py)
140 if (px, py) == mkpos(i):
142 new_i = (i + change) % 5
143 if (px, py) == mkpos(new_i):
144 new_i = (new_i + change) % 5
145 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
147 self.board_locations.update(shifted_locations)
149 def shift_locations(self, direction):
150 if BITS[direction] == BITS.NORTH:
151 self.shift_location_row(-1, is_vertical=True)
152 elif BITS[direction] == BITS.SOUTH:
153 self.shift_location_row(1, is_vertical=True)
154 elif BITS[direction] == BITS.EAST:
155 self.shift_location_row(1, is_vertical=False)
156 elif BITS[direction] == BITS.WEST:
157 self.shift_location_row(-1, is_vertical=False)
159 def rotate_locations(self, direction):
160 px, py = self.player.position
161 locations_to_rotate = []
162 rotated_locations = {}
165 for i in range(max(0, px - 1), min(5, px + 2)):
166 locations_to_rotate.append((i, py - 1))
169 locations_to_rotate.append((px + 1, py))
172 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
173 locations_to_rotate.append((i, py + 1))
176 locations_to_rotate.append((px - 1, py))
178 if ROTATION[direction] == ROTATION.CLOCKWISE:
179 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
180 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
182 [locations_to_rotate[-1]] + locations_to_rotate[:-1])
184 for old, new in zip(locations_to_rotate, new_positions):
185 rotated_locations[old] = self.board_locations[new]
187 self.board_locations.update(rotated_locations)
189 def allow_chess_move(self, chesspiece):
190 self.player.allow_chess_move(chesspiece)
192 def change_mode(self, new_mode):
193 """Advance to the next mode"""
194 if new_mode == self.player_mode:
195 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
197 elif new_mode in (ACT, EXAMINE):
198 self.player_mode = new_mode
200 raise RuntimeError("Illegal player mode %s" % self.player_mode)
202 def end_game(self, win):
203 # TODO: Find a way to not have UI stuff in game logic stuff.
204 from naja.events import SceneChangeEvent
205 from naja.scenes.lose import LoseScene
206 from naja.scenes.win import WinScene
208 SceneChangeEvent.post(WinScene)
210 SceneChangeEvent.post(LoseScene)
213 class LocationCard(object):
215 A particular set of options available on a location.
218 def __init__(self, bitwise_operand, location_actions):
219 self.bitwise_operand = bitwise_operand
220 self.actions = location_actions
224 def import_location(cls, state):
226 cls.build_action(definition) for definition in state['actions']]
227 return cls(state['bitwise_operand'], location_actions)
230 def build_action(cls, definition):
231 action_class = getattr(actions, definition['action_class'])
232 required_bits = cls.parse_bits(definition['required_bits'])
233 data = definition.get('data', {})
234 return action_class(required_bits, **data)
237 def new_location(cls, definition):
238 if 'bits' in definition:
239 bits = cls.parse_bits(definition['bits'])
241 bits = cls.generate_bitwise_operand()
242 return cls.import_location({
243 'bitwise_operand': bits,
244 'actions': definition['actions'],
248 def parse_bits(self, bit_list):
249 # Convert names to numbers if applicable.
250 return frozenset(BITS.get(bit, bit) for bit in bit_list)
254 'bitwise_operand': sorted(self.bitwise_operand),
255 'actions': [action.export() for action in self.actions],
258 def check_actions(self):
260 print "Warning: Location has no actions."
261 self.insert_default_default_action()
262 if self.actions[0].required_bits:
263 self.insert_default_default_action()
265 def insert_default_default_action(self):
266 self.actions.insert(0, self.build_action({
267 'action_class': 'DoNothing',
272 def generate_bitwise_operand():
274 Generate a set of two or three bits. At least one direction and one
275 condition bit will be included. There is a low probability of choosing
276 a third bit from the complete set.
279 bits.add(choice(DIRECTION_BITS.values()))
280 bits.add(choice(CONDITION_BITS.values()))
281 # One in three chance of adding a third bit, with a further one in four
282 # chance that it will match a bit already chosen.
283 if choice(range(3)) == 0:
284 bits.add(choice(BITS.values()))
285 return frozenset(bits)