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.get('puzzle', False)
24 self.board_locations = board_locations
25 self.player_mode = state.get('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(),
66 'player_mode': self.player_mode,
70 def import_locations(cls, locations_definition):
72 LocationCard.import_location(definition)
73 for definition in locations_definition]
75 def export_board_locations(self):
77 (position, location.export())
78 for position, location in self.board_locations.iteritems())
81 def import_board_locations(cls, board_locations_definition):
83 (tuple(position), LocationCard.import_location(definition))
84 for position, definition in board_locations_definition)
87 def generate_board(cls, deck):
88 if deck.get('puzzle', False):
89 return cls.generate_puzzle_board(deck)
91 return cls.generate_random_board(deck)
94 def generate_puzzle_board(cls, deck):
95 assert len(deck['cards']) == 5 * 5
98 LocationCard.new_location(card.copy()).export()]
99 for i, card in enumerate(deck['cards'])
101 return board_locations
104 def generate_random_board(cls, deck):
108 board_location = LocationCard.new_location(
109 choice(deck['cards']).copy())
110 board_locations.append([(x, y), board_location.export()])
111 return board_locations
113 def lose_health(self):
116 self.end_game(win=False)
118 def gain_health(self):
119 if self.health < self.max_health:
122 def acquire_win_token(self):
124 if self.wins >= self.wins_required:
125 self.end_game(win=True)
127 def card_used(self, position):
129 self.replace_card(position)
131 def replace_card(self, position):
132 location = LocationCard.new_location(choice(self.locations).copy())
133 self.board_locations[position] = location
135 def shift_location_row(self, change, is_vertical):
136 px, py = self.player.position
137 shifted_locations = {}
138 mkpos = lambda i: (px, i) if is_vertical else (i, py)
141 if (px, py) == mkpos(i):
143 new_i = (i + change) % 5
144 if (px, py) == mkpos(new_i):
145 new_i = (new_i + change) % 5
146 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
148 self.board_locations.update(shifted_locations)
150 def shift_locations(self, direction):
151 if BITS[direction] == BITS.NORTH:
152 self.shift_location_row(-1, is_vertical=True)
153 elif BITS[direction] == BITS.SOUTH:
154 self.shift_location_row(1, is_vertical=True)
155 elif BITS[direction] == BITS.EAST:
156 self.shift_location_row(1, is_vertical=False)
157 elif BITS[direction] == BITS.WEST:
158 self.shift_location_row(-1, is_vertical=False)
160 def rotate_locations(self, direction):
161 px, py = self.player.position
162 locations_to_rotate = []
163 rotated_locations = {}
166 for i in range(max(0, px - 1), min(5, px + 2)):
167 locations_to_rotate.append((i, py - 1))
170 locations_to_rotate.append((px + 1, py))
173 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
174 locations_to_rotate.append((i, py + 1))
177 locations_to_rotate.append((px - 1, py))
179 if ROTATION[direction] == ROTATION.CLOCKWISE:
180 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
181 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
183 [locations_to_rotate[-1]] + locations_to_rotate[:-1])
185 for old, new in zip(locations_to_rotate, new_positions):
186 rotated_locations[old] = self.board_locations[new]
188 self.board_locations.update(rotated_locations)
190 def allow_chess_move(self, chesspiece):
191 self.player.allow_chess_move(chesspiece)
193 def change_mode(self, new_mode):
194 """Advance to the next mode"""
195 if new_mode == self.player_mode:
196 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
198 elif new_mode in (ACT, EXAMINE):
199 self.player_mode = new_mode
201 raise RuntimeError("Illegal player mode %s" % self.player_mode)
203 def end_game(self, win):
204 # TODO: Find a way to not have UI stuff in game logic stuff.
205 from naja.events import SceneChangeEvent
206 from naja.scenes.lose import LoseScene
207 from naja.scenes.win import WinScene
209 SceneChangeEvent.post(WinScene)
211 SceneChangeEvent.post(LoseScene)
214 class LocationCard(object):
216 A particular set of options available on a location.
219 def __init__(self, bitwise_operand, location_actions):
220 self.bitwise_operand = bitwise_operand
221 self.actions = location_actions
225 def import_location(cls, state):
227 cls.build_action(definition) for definition in state['actions']]
228 return cls(state['bitwise_operand'], location_actions)
231 def build_action(cls, definition):
232 action_class = getattr(actions, definition['action_class'])
233 required_bits = cls.parse_bits(definition['required_bits'])
234 data = definition.get('data', {})
235 return action_class(required_bits, **data)
238 def new_location(cls, definition):
239 if 'bits' in definition:
240 bits = cls.parse_bits(definition['bits'])
242 bits = cls.generate_bitwise_operand()
243 return cls.import_location({
244 'bitwise_operand': bits,
245 'actions': definition['actions'],
249 def parse_bits(self, bit_list):
250 # Convert names to numbers if applicable.
251 return frozenset(BITS.get(bit, bit) for bit in bit_list)
255 'bitwise_operand': sorted(self.bitwise_operand),
256 'actions': [action.export() for action in self.actions],
259 def check_actions(self):
261 print "Warning: Location has no actions."
262 self.insert_default_default_action()
263 if self.actions[0].required_bits:
264 self.insert_default_default_action()
266 def insert_default_default_action(self):
267 self.actions.insert(0, self.build_action({
268 'action_class': 'DoNothing',
273 def generate_bitwise_operand():
275 Generate a set of two or three bits. At least one direction and one
276 condition bit will be included. There is a low probability of choosing
277 a third bit from the complete set.
280 bits.add(choice(DIRECTION_BITS.values()))
281 bits.add(choice(CONDITION_BITS.values()))
282 # One in three chance of adding a third bit, with a further one in four
283 # chance that it will match a bit already chosen.
284 if choice(range(3)) == 0:
285 bits.add(choice(BITS.values()))
286 return frozenset(bits)