Editing State Machine - lil-lab/cb2 GitHub Wiki

Editing the State Machine

Modifying turn durations, score rewards, time limits, etc.

These can all be modified quite easily by changing the constants in src/cb2game/server/state_utils.py:


LEADER_MOVES_PER_TURN = 5
FOLLOWER_MOVES_PER_TURN = 10

LEADER_SECONDS_PER_TURN = 50
FOLLOWER_SECONDS_PER_TURN = 15

FOLLOWER_TURN_END_DELAY_SECONDS = 1

The same file also contains the constants for the score rewards:

def turn_reward(score):
    """Calculates the turn reward (# of turns added) for a given score."""
    if score == 0:
        return 5
    elif score in [1, 2]:
        return 4
    elif score in [3, 4]:
        return 3
    elif score in [5, 6]:
        return 2
    elif score in [7, 8]:
        return 1
    else:
        return 0

Modifying the game rules

More complex modifications to the game rules will require editing the state machine. The state machine is currently an unorganized mess of code located in server/state.py. We provide an example here of modifying the game state machine, to aid in understanding the code. In the future, hopefully a refactor can be done to simplify this.

Adding delayed live feedback.

We recently introduced a new kind of feedback mechanism between the leader and follower called delayed feedback. Delayed feedback is similar to live feedback, however it is only delivered after the follower has completed an instruction. Implementing this feature was further complicated as it needed to co-exist with the existing live-feedback feature. The commit which introduced this feature can be seen here.

Note that the commit includes changes to the Unity client as well as other parts of the backend. But we will only discuss the backend state machine changes here.

The first step was to make it so that each instruction object can keep a count of feedback messages that have been sent to the follower. This is done by adding a new field to the Instruction object:

@dataclass
class ObjectiveMessage(DataClassJSONMixin):
    sender: Role = Role.NONE
    text: str = ""
    uuid: str = ""
    completed: bool = False
    cancelled: bool = False
    ...
+   # New fields:
+   feedback_text: str = ""
+   pos_feedback: int = 0
+   neg_feedback: int = 0
+
+   def update_feedback_text(self):
+       ...

Then, we modified State's update() function to accumulate feedback messages for each instruction:

        while len(self._live_feedback_queue) > 0:
            (id, feedback) = self._live_feedback_queue.popleft()
            for actor_id in self._actors:
                self._live_feedback[actor_id] = feedback.signal
            send_tick = True
            active_instruction = None
            if len(self._instructions) > 0:
                active_instruction = self._instructions[0]
+           if feedback.signal == live_feedback.FeedbackType.POSITIVE:
+               active_instruction.pos_feedback += 1
+           elif feedback.signal == live_feedback.FeedbackType.NEGATIVE:
+               active_instruction.neg_feedback += 1
+           active_instruction.update_feedback_text()
            self._game_recorder.record_live_feedback(
                feedback, self._follower, active_instruction
            )

Changing Set Completion Criteria.

Set completion criteria are defined in a utility class called MapProvider. The MapProvider is used for map generation, card state tracking, card generation, and set completion criteria. The function which determines when a valid set has been collected is called selected_valid_set() and is located here. You can modify it and accompanying function selected_cards_collide() here to change the set completion criteria.

    def selected_cards_collide(self):
        """Determines whether any invalid cards are selected."""
        shapes = set()
        colors = set()
        counts = set()
        for card in self.selected_cards():
            shapes.add(card.shape)
            colors.add(card.color)
            counts.add(card.count)
        num_cards = len(self.selected_cards())
        return (
            not (len(shapes) == len(colors) == len(counts) == num_cards)
            or len(self.selected_cards()) > 3
        )

    def selected_valid_set(self):
        return len(self.selected_cards()) == 3 and not self.selected_cards_collide()