Remove player_mode from game state.
[naja.git] / naja / gameboard.py
index ed8d74f4657974079b3978ece51edbb0a30b3499..0c0d922c959899cf809b45ca483e9137ffe5f763 100644 (file)
@@ -2,11 +2,12 @@ from random import choice
 
 from naja.constants import(
     BITS, DIRECTION_BITS, CONDITION_BITS, PLAYER_DEFAULTS,
 
 from naja.constants import(
     BITS, DIRECTION_BITS, CONDITION_BITS, PLAYER_DEFAULTS,
-    ACT, EXAMINE, ROTATION)
+    ROTATION)
 from naja.options import options
 from naja.player import Player
 from naja import actions
 from naja.sound import sound
 from naja.options import options
 from naja.player import Player
 from naja import actions
 from naja.sound import sound
+from naja.utils import parse_bits
 import random
 
 
 import random
 
 
@@ -24,19 +25,42 @@ class GameBoard(object):
         self.puzzle = state.get('puzzle', False)
         self.player = player
         self.board_locations = board_locations
         self.puzzle = state.get('puzzle', False)
         self.player = player
         self.board_locations = board_locations
-        self.player_mode = state.get('player_mode', EXAMINE)
         self.has_cheated = state.get('cheater', options.cheat_enabled)
         self.clock_count = state.get('clock_count', 0)
         self.replacement_params = state.get('replacement_params', None)
 
     @classmethod
         self.has_cheated = state.get('cheater', options.cheat_enabled)
         self.clock_count = state.get('clock_count', 0)
         self.replacement_params = state.get('replacement_params', None)
 
     @classmethod
-    def new_game(cls, deck,
-                 initial_bits=PLAYER_DEFAULTS.INITIAL_BITS,
-                 initial_pos=PLAYER_DEFAULTS.INITIAL_POS,
-                 max_health=PLAYER_DEFAULTS.MAX_HEALTH,
-                 wins_required=PLAYER_DEFAULTS.WINS_REQUIRED):
+    def new_game(cls, deck, initial_bits=None, initial_pos=None,
+                 max_health=None, wins_required=None):
+
+        defaults = {
+            'initial_bits': PLAYER_DEFAULTS.INITIAL_BITS,
+            'initial_pos': PLAYER_DEFAULTS.INITIAL_POS,
+            'max_health': PLAYER_DEFAULTS.MAX_HEALTH,
+            'wins_required': PLAYER_DEFAULTS.WINS_REQUIRED,
+        }
+
+        deck_defaults = deck.get('defaults', {})
+        for k, v in deck_defaults.iteritems():
+            if isinstance(v, list):
+                deck_defaults[k] = tuple(v)
+        defaults.update(deck_defaults)
+
+        if initial_bits is None:
+            initial_bits = defaults['initial_bits']
+        if initial_pos is None:
+            initial_pos = defaults['initial_pos']
+        if max_health is None:
+            max_health = defaults['max_health']
+        if wins_required is None:
+            wins_required = defaults['wins_required']
+
+        assert wins_required + max_health == 8
+
+        # Overriden by command line
         if options.initial_bits:
             initial_bits = options.initial_bits
         if options.initial_bits:
             initial_bits = options.initial_bits
+
         state = {
             'max_health': max_health,
             'health': max_health,
         state = {
             'max_health': max_health,
             'health': max_health,
@@ -50,7 +74,9 @@ class GameBoard(object):
         player = Player(initial_bits, initial_pos)
         board_locations = cls.import_board_locations(
             cls.generate_board(deck))
         player = Player(initial_bits, initial_pos)
         board_locations = cls.import_board_locations(
             cls.generate_board(deck))
-        return cls(state, player, board_locations)
+        board = cls(state, player, board_locations)
+        player.set_gameboard(board)
+        return board
 
     @classmethod
     def import_game(cls, definition):
 
     @classmethod
     def import_game(cls, definition):
@@ -58,7 +84,9 @@ class GameBoard(object):
         player = Player.import_player(state.pop('player'))
         board_locations = cls.import_board_locations(
             state.pop('board_locations'))
         player = Player.import_player(state.pop('player'))
         board_locations = cls.import_board_locations(
             state.pop('board_locations'))
-        return cls(state, player, board_locations)
+        board = cls(state, player, board_locations)
+        player.set_gameboard(board)
+        return board
 
     def export(self):
         data = {
 
     def export(self):
         data = {
@@ -70,10 +98,11 @@ class GameBoard(object):
             'puzzle': self.puzzle,
             'player': self.player.export(),
             'board_locations': self.export_board_locations(),
             'puzzle': self.puzzle,
             'player': self.player.export(),
             'board_locations': self.export_board_locations(),
-            'player_mode': self.player_mode,
             'clock_count': self.clock_count,
             'replacement_params': self.replacement_params,
         }
             'clock_count': self.clock_count,
             'replacement_params': self.replacement_params,
         }
+        if options.cheat_enabled:
+            self.has_cheated = True
         if self.has_cheated:
             data['cheater'] = True
         return data
         if self.has_cheated:
             data['cheater'] = True
         return data
@@ -109,7 +138,7 @@ class GameBoard(object):
         board_locations = [
             [(i % 5, i // 5),
              LocationCard.new_location(
         board_locations = [
             [(i % 5, i // 5),
              LocationCard.new_location(
-                 card.copy(), replacement_params).export()]
+                 card.copy(), replacement_params, puzzle=True).export()]
             for i, card in enumerate(deck['cards'])
         ]
         return board_locations
             for i, card in enumerate(deck['cards'])
         ]
         return board_locations
@@ -120,8 +149,9 @@ class GameBoard(object):
         replacement_params = deck.get('replacement_params', None)
         for x in range(5):
             for y in range(5):
         replacement_params = deck.get('replacement_params', None)
         for x in range(5):
             for y in range(5):
+                new_choice = cls.choose_card(deck['cards'], board_locations)
                 board_location = LocationCard.new_location(
                 board_location = LocationCard.new_location(
-                    choice(deck['cards']).copy(), replacement_params)
+                    new_choice.copy(), replacement_params)
                 board_locations.append([(x, y), board_location.export()])
         return board_locations
 
                 board_locations.append([(x, y), board_location.export()])
         return board_locations
 
@@ -144,34 +174,64 @@ class GameBoard(object):
             self.replace_card(position)
 
     def replace_card(self, position):
             self.replace_card(position)
 
     def replace_card(self, position):
-        location = LocationCard.new_location(choice(self.locations).copy(),
+        new_choice = self.choose_card(self.locations,
+                                      self.board_locations.items(),
+                                      position)
+        location = LocationCard.new_location(new_choice.copy(),
                                              self.replacement_params)
         self.board_locations[position] = location
 
                                              self.replacement_params)
         self.board_locations[position] = location
 
-    def shift_location_row(self, change, is_vertical):
+    @classmethod
+    def choose_card(cls, cards, board_locations, position=None):
+        # Find which cards are at their maximum and exclude them from
+        # the choice list
+        counts = {}
+        choices = dict((card['card_name'], card) for card in cards)
+        for pos, card in board_locations:
+            if pos == position:
+                # skip the card we're replacing if appropriate
+                continue
+            if isinstance(card, LocationCard):
+                key = card.card_name
+                max_num = card.max_number
+            else:
+                key = card['card_name']
+                max_num = card.get('max_number', 25)
+            counts.setdefault(key, 0)
+            counts[key] += 1
+            if counts[key] >= max_num:
+                if key in choices:
+                    del choices[key]
+        return choice(choices.values())
+
+    def shift_location_row(self, change, is_vertical, skip_player=True):
         px, py = self.player.position
         shifted_locations = {}
         mkpos = lambda i: (px, i) if is_vertical else (i, py)
 
         for i in range(5):
         px, py = self.player.position
         shifted_locations = {}
         mkpos = lambda i: (px, i) if is_vertical else (i, py)
 
         for i in range(5):
-            if (px, py) == mkpos(i):
+            if skip_player and (px, py) == mkpos(i):
                 continue
             new_i = (i + change) % 5
                 continue
             new_i = (i + change) % 5
-            if (px, py) == mkpos(new_i):
+            if skip_player and (px, py) == mkpos(new_i):
                 new_i = (new_i + change) % 5
             shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
 
         self.board_locations.update(shifted_locations)
 
                 new_i = (new_i + change) % 5
             shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
 
         self.board_locations.update(shifted_locations)
 
-    def shift_locations(self, direction):
+    def shift_locations(self, direction, skip_player=True):
         if BITS[direction] == BITS.NORTH:
         if BITS[direction] == BITS.NORTH:
-            self.shift_location_row(-1, is_vertical=True)
+            self.shift_location_row(-1, is_vertical=True,
+                                    skip_player=skip_player)
         elif BITS[direction] == BITS.SOUTH:
         elif BITS[direction] == BITS.SOUTH:
-            self.shift_location_row(1, is_vertical=True)
+            self.shift_location_row(1, is_vertical=True,
+                                    skip_player=skip_player)
         elif BITS[direction] == BITS.EAST:
         elif BITS[direction] == BITS.EAST:
-            self.shift_location_row(1, is_vertical=False)
+            self.shift_location_row(1, is_vertical=False,
+                                    skip_player=skip_player)
         elif BITS[direction] == BITS.WEST:
         elif BITS[direction] == BITS.WEST:
-            self.shift_location_row(-1, is_vertical=False)
+            self.shift_location_row(-1, is_vertical=False,
+                                    skip_player=skip_player)
 
     def rotate_locations(self, direction):
         px, py = self.player.position
 
     def rotate_locations(self, direction):
         px, py = self.player.position
@@ -195,7 +255,8 @@ class GameBoard(object):
         if ROTATION[direction] == ROTATION.CLOCKWISE:
             new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
         elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
         if ROTATION[direction] == ROTATION.CLOCKWISE:
             new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
         elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
-            new_positions = ([locations_to_rotate[-1]] + locations_to_rotate[:-1])
+            new_positions = (
+                [locations_to_rotate[-1]] + locations_to_rotate[:-1])
 
         for old, new in zip(locations_to_rotate, new_positions):
             rotated_locations[new] = self.board_locations[old]
 
         for old, new in zip(locations_to_rotate, new_positions):
             rotated_locations[new] = self.board_locations[old]
@@ -205,18 +266,6 @@ class GameBoard(object):
     def allow_chess_move(self, chesspiece):
         self.player.allow_chess_move(chesspiece)
 
     def allow_chess_move(self, chesspiece):
         self.player.allow_chess_move(chesspiece)
 
-    def change_mode(self, new_mode):
-        """Advance to the next mode"""
-        if new_mode == self.player_mode:
-            raise RuntimeError("Inconsistent state. Setting mode %s to itself"
-                               % self.player_mode)
-        elif new_mode in (ACT, EXAMINE):
-            self.player_mode = new_mode
-            if new_mode is EXAMINE:
-                self.board_update()
-        else:
-            raise RuntimeError("Illegal player mode %s" % self.player_mode)
-
     def board_update(self):
         self.clock_count += 1
         for position, location in self.board_locations.iteritems():
     def board_update(self):
         self.clock_count += 1
         for position, location in self.board_locations.iteritems():
@@ -240,31 +289,35 @@ class LocationCard(object):
     """
 
     def __init__(self, card_name, bitwise_operand, location_actions,
     """
 
     def __init__(self, card_name, bitwise_operand, location_actions,
-                 replacement_time):
+                 replacement_time=None, max_number=25):
         self.card_name = card_name
         self.bitwise_operand = bitwise_operand
         self.actions = location_actions
         self.card_name = card_name
         self.bitwise_operand = bitwise_operand
         self.actions = location_actions
-        self.check_actions()
+        self.max_number = max_number
         self.replacement_time = replacement_time
         self.replacement_time = replacement_time
+        if options.debug:
+            for action in self.actions:
+                action.sanity_check(self)
 
     @classmethod
     def import_location(cls, state):
         location_actions = [
             cls.build_action(definition) for definition in state['actions']]
         return cls(state['card_name'], state['bitwise_operand'],
 
     @classmethod
     def import_location(cls, state):
         location_actions = [
             cls.build_action(definition) for definition in state['actions']]
         return cls(state['card_name'], state['bitwise_operand'],
-                   location_actions, state['replacement_time'])
+                   location_actions, state['replacement_time'],
+                   state['max_number'])
 
     @classmethod
     def build_action(cls, definition):
         action_class = getattr(actions, definition['action_class'])
 
     @classmethod
     def build_action(cls, definition):
         action_class = getattr(actions, definition['action_class'])
-        required_bits = cls.parse_bits(definition['required_bits'])
+        required_bits = parse_bits(definition['required_bits'])
         data = definition.get('data', {})
         return action_class(required_bits, **data)
 
     @classmethod
         data = definition.get('data', {})
         return action_class(required_bits, **data)
 
     @classmethod
-    def new_location(cls, definition, replacement_params):
+    def new_location(cls, definition, replacement_params=None, puzzle=False):
         if 'bits' in definition:
         if 'bits' in definition:
-            bits = cls.parse_bits(definition['bits'])
+            bits = parse_bits(definition['bits'])
         else:
             bits = cls.generate_bitwise_operand()
 
         else:
             bits = cls.generate_bitwise_operand()
 
@@ -274,23 +327,24 @@ class LocationCard(object):
             replacement_time = cls.generate_replacement_time(
                 replacement_params)
 
             replacement_time = cls.generate_replacement_time(
                 replacement_params)
 
+        max_number = definition.get('max_number', 25)
         card_name = definition['card_name']
         card_name = definition['card_name']
-        return cls.import_location({
+        location = cls.import_location({
             'bitwise_operand': bits,
             'actions': definition['actions'],
             'bitwise_operand': bits,
             'actions': definition['actions'],
+            'max_number': max_number,
             'card_name': card_name,
             'replacement_time': replacement_time,
         })
             'card_name': card_name,
             'replacement_time': replacement_time,
         })
-
-    @classmethod
-    def parse_bits(self, bit_list):
-        # Convert names to numbers if applicable.
-        return frozenset(BITS.get(bit, bit) for bit in bit_list)
+        if not puzzle:
+            location.check_actions()
+        return location
 
     def export(self):
         return {
             'bitwise_operand': sorted(self.bitwise_operand),
             'actions': [action.export() for action in self.actions],
 
     def export(self):
         return {
             'bitwise_operand': sorted(self.bitwise_operand),
             'actions': [action.export() for action in self.actions],
+            'max_number': self.max_number,
             'card_name': self.card_name,
             'replacement_time': self.replacement_time,
         }
             'card_name': self.card_name,
             'replacement_time': self.replacement_time,
         }
@@ -329,11 +383,14 @@ class LocationCard(object):
         if replacement_params is None:
             return None
         else:
         if replacement_params is None:
             return None
         else:
-            return random.randint(replacement_params[0], replacement_params[1])
+            if replacement_params['chance'] > random.random():
+                return random.randint(replacement_params['min'],
+                                      replacement_params['max'])
+            else:
+                return None
 
     def timer_action(self, position, board):
         if self.replacement_time is not None:
             self.replacement_time -= 1
             if self.replacement_time <= 0:
                 board.replace_card(position)
 
     def timer_action(self, position, board):
         if self.replacement_time is not None:
             self.replacement_time -= 1
             if self.replacement_time <= 0:
                 board.replace_card(position)
-