extends WolfPlayer class_name Bot enum TargetingStrategy { ENEMIES = 1, FRIENDS = 2, POWERFUL = 4, VULNERABLE = 8, UNKNOWN = 16, KNOWN = ENEMIES | FRIENDS, ALLOW_SELF = 32, RANDOM = 64, } const strategies: Dictionary = { "block": TargetingStrategy.POWERFUL | TargetingStrategy.ENEMIES, "investigate": TargetingStrategy.UNKNOWN, "jail": TargetingStrategy.VULNERABLE, "protect": TargetingStrategy.POWERFUL | TargetingStrategy.VULNERABLE | TargetingStrategy.FRIENDS, "steal": TargetingStrategy.POWERFUL | TargetingStrategy.ENEMIES, "murder": TargetingStrategy.POWERFUL | TargetingStrategy.ENEMIES, "vote": TargetingStrategy.POWERFUL | TargetingStrategy.VULNERABLE | TargetingStrategy.ENEMIES, } const verb_aliases: Dictionary = { "distract": "block", "roleblock": "block", "kill": "murder", "execute": "vote", "defend": "protect", "heal": "protect", "imprison": "jail", } # handles both investigation results and player statements const claim_noun_trustworthiness : Dictionary = { "harmless": +0.5, "what it takes to kill": -0.5, "town": +1.0, "townie": +1.0, "innocent": +1.0, "wolf": -1.0, "wolves": -1.0, "killer": -1.0, } const CLAIM_CONFIRMATION_TRUST_CHANGE: float = 0.3 class PlayerKnowledge: var trustworthiness: float = randfn(0.0, 0.1) var power: float = absf(randfn(1.0, 0.1)) var role: Role = null var role_confidence: float = 0.0 var outed: bool = false var talked_about: bool = false var claims: Dictionary = {} func change_trust(delta: float): if trustworthiness >= 1.0 or trustworthiness <= -1.0: return # no change if we just fuckin KNOW var old: float = trustworthiness trustworthiness = clamp(lerp(trustworthiness, signf(delta), abs(delta)), -1.0, 1.0) print("Adjusting trust %f by %f, result %f" % [old, delta, trustworthiness]) var player_knowledge: Dictionary = {} var test_mode: bool = false func _init() -> void: #super._init() connect("prompt_changed", _on_prompt_changed) connect("team_changed", _on_team_changed) func game_reset() -> void: super.game_reset() player_knowledge.clear() init_trust() func _on_team_changed(_team:Team) -> void: if team == null: return init_trust() func init_trust(): #var town_members: int = game_state.count_team_members(team) #var living_players: int = game_state.count_alive_players() #var starting_trust: float = town_members / float(living_players) #starting_trust = remap(starting_trust, 0.0, 1.0, -1.0, 1.0) for player in Players.instance.get_active_players(): var wp: WolfPlayer = player as WolfPlayer var trust: float = 0.0 if player == self: trust = 1.0 elif team.know_each_other: if wp.team == team: trust = 1.0 else: trust = -1.0 else: trust = randfn(0.0,0.2) get_player_knowledge(wp).trustworthiness = trust func _on_prompt_changed(prompt: Prompt) -> void: if prompt == null: return var answer: String = "" match prompt.type: Prompt.PromptType.PLAYER: var targeting_strategy: TargetingStrategy = TargetingStrategy.RANDOM var words: PackedStringArray = prompt.text.to_lower().split(" ") var verb: String = find_verb(words) if !verb.is_empty(): targeting_strategy = get_verb_strategy(verb) answer = choose_player_by_strategy(prompt.options, targeting_strategy).player_name Prompt.PromptType.MULTIPLE_CHOICE, Prompt.PromptType.COLOUR: if !prompt.options.is_empty(): var random_option: String = prompt.options.pick_random() answer = random_option Prompt.PromptType.CONFIRMATION: process_message(prompt.format_text()) answer = "confirmed" Prompt.PromptType.SHOW_TEAM, Prompt.PromptType.SHOW_ROLE: answer = "confirmed" Prompt.PromptType.TEXT, Prompt.PromptType.LONG_TEXT: answer = player_name if !test_mode: await get_tree().create_timer(randf_range(0,10)).timeout submit_prompt_answer(prompt.id, answer) func get_player_knowledge(player: Player) -> PlayerKnowledge: if !player_knowledge.has(player): player_knowledge[player] = PlayerKnowledge.new() return player_knowledge[player] func calculate_player_value_with_strategy(player: Player, strategy: TargetingStrategy) -> float: var knowledge: PlayerKnowledge = get_player_knowledge(player) if player == self and !(strategy & TargetingStrategy.ALLOW_SELF): return 0.0 var value: float = 0.0 if strategy & TargetingStrategy.RANDOM: value += randf() if strategy & TargetingStrategy.FRIENDS: value += max(knowledge.trustworthiness, 0.0) if strategy & TargetingStrategy.ENEMIES: value += max(-knowledge.trustworthiness, 0.0) if strategy & TargetingStrategy.UNKNOWN: value += 1.0-abs(knowledge.trustworthiness) if strategy & TargetingStrategy.POWERFUL: value *= knowledge.power if strategy & TargetingStrategy.VULNERABLE: value *= 2.0 if knowledge.outed else 1.0 return value func choose_player_by_strategy(options: Array[String], strategy: TargetingStrategy) -> Player: var current_choice: Player = null var current_best_value: float = 0.0 for option in options: var player: Player = Players.instance.find_player_by_name(option) if player == null: continue var value: float = calculate_player_value_with_strategy(player, strategy) if value >= current_best_value: current_best_value = value current_choice = player return current_choice func get_verb_strategy(verb: String) -> TargetingStrategy: return strategies.get(verb, TargetingStrategy.RANDOM) func find_verb(words: PackedStringArray) -> String: for word in words: word = word.trim_suffix(",").trim_suffix(".") if strategies.has(word): return word elif verb_aliases.has(word): return verb_aliases[word] return "" func process_message(message: String, source: Player = null) -> void: var message_lower: String = message.to_lower() var source_knowledge: PlayerKnowledge = null if source != null: source_knowledge = get_player_knowledge(source) var trustworthiness: float = 1.0 if source_knowledge != null: trustworthiness = source_knowledge.trustworthiness var players_named: Array[Player] = [] for player in Players.instance.get_active_players(): if message_lower.contains(player.player_name.to_lower()): players_named.append(player) var modification_weight: float = 1.0 if players_named.is_empty() else 1.0/players_named.size() # TODO: if message contains "i" or "me" as whole word add source to players named # for now, just handle trustworthiness for term in claim_noun_trustworthiness.keys(): if message_lower.contains(term): var trust_adjustment: float = trustworthiness * modification_weight * claim_noun_trustworthiness[term] for player in players_named: if player == source: continue # ignore statements about your own innocence because they're pointless get_player_knowledge(player).change_trust(trust_adjustment) if source_knowledge != null: source_knowledge.claims[player] = source_knowledge.claims.get(player, 0.0) + claim_noun_trustworthiness[term] if message.contains("dead") and source == null and players_named.size() == 1: # this is a death flip, so let's see what everyone else said about them var dead_player: Player = players_named[0] var dead_player_knowledge: PlayerKnowledge = get_player_knowledge(dead_player) var players: Array = player_knowledge.keys() players.shuffle() for player in players: var knowledge: PlayerKnowledge = player_knowledge[player] if knowledge.claims.has(dead_player): var claimed_trust: float = knowledge.claims[dead_player] var our_trust: float = dead_player_knowledge.trustworthiness knowledge.change_trust(claimed_trust * our_trust * CLAIM_CONFIRMATION_TRUST_CHANGE) # this will be +ve if the signs agree (they were right), -ve otherwise (they were wrong) func chatter() -> void: # say something about someone var message: String = "" var players: Array = player_knowledge.keys() players.shuffle() for player in players: if player == self or !player.alive: continue # don't make statements about yourself or dead people because they're pointless var knowledge: PlayerKnowledge = player_knowledge[player] if knowledge.trustworthiness >= 0.5: message = player.player_name + " innocent" elif knowledge.trustworthiness <= -0.5: message = player.player_name + " werewolf" elif knowledge.role_confidence >= 0.5: message = player.player_name + " " + knowledge.role.name.to_lower() if !message.is_empty(): speak(message) return