4
0
Fork 0
WolfBox/scripts/werewolf/bot.gd

242 lines
8.2 KiB
GDScript

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