def player_score_fn(
      current_scene_type: str,
      activity_proportions: Mapping[str, Mapping[str, float]],
      player_name: str,
      game_master_memory: associative_memory.AssociativeMemory,
      timepoint: str,
  ) -> tuple[float, Sequence[str]]:
    """Get the individual part of the score."""
    score = 0.0
    events = []
    if current_scene_type != DEBRIEF_SCENE_TYPE:
      return score, events

    # Shared part of the score (common defense) is calculated first.
    defense_per_village = _get_overall_activity_product_per_village(
        activity_proportions, config.warrior_training_activity)
    # Overall defense is the mean defense over villages because barbarians
    # may attack randomly near any village equiprobably.
    raw_overall_defense = np.mean(list(defense_per_village.values()))
    if raw_overall_defense < config.defense_threshold:
      defense = 0.0
      events.append(config.sample_event_of_failing_to_repel_barbarians())
    else:
      defense = 1.0
      events.append(config.sample_event_of_success_repelling_barbarians())

    # Individual part of the score (farming and free time) is calculated next.
    if player_name in village_by_representative:
      this_village = village_by_representative[player_name]
    else:
      if player_name in  [name for name, _ in config.supporting_characters.a]:
        this_village = config.village_a_name
      elif player_name in [name for name, _ in config.supporting_characters.b]:
        this_village = config.village_b_name
      else:
        raise ValueError(f'Player {player_name} not found in any '
                         f'village: {village_by_representative} -- '
                         f'{config.supporting_characters.a} -- '
                         f'{config.supporting_characters.b}')

    farming_per_village = _get_overall_activity_product_per_village(
        activity_proportions, config.farming_activity)
    if _are_agricultural_resources_pooled(game_master_memory,
                                          timepoint):
      # If villagers have agreed to resupply whichever village has less food
      # then all villages receive the max of their agricultural production, so
      # starvation is unlikely unless all villages starve together.
      raw_agriculture = np.max(list(farming_per_village.values()))
      events.append(config.sample_event_treaty_in_effect())
    else:
      # If villagers did not agree to pool agricultural resources then each
      # village is on its own and villages that do not farm enough starve.
      raw_agriculture = farming_per_village[this_village]
      events.append(config.sample_event_no_treaty_in_effect())

    if raw_agriculture < config.starvation_threshold:
      agriculture = 0.0
      events.append(config.sample_event_of_failing_to_grow_food())
    else:
      agriculture = 1.0
      events.append(config.sample_event_of_success_growing_food())

    # Villager free time contributes positively to the overall score.
    free_time_per_village = _get_overall_activity_product_per_village(
        activity_proportions, config.free_time_activity)
    free_time = free_time_per_village[this_village]

    # The overall score is the product of the activity scores. Note that two of
    # them are constrained to be binary (defense and agriculture) so the only
    # effect they can have is to gate the free time score.
    score = defense * agriculture * free_time

    return score, events
Get the individual part of the score.
examples/deprecated/modular/environment/state_formation.py