forked from Nekojimi/JackIt
242 lines
8.2 KiB
GDScript3
242 lines
8.2 KiB
GDScript3
|
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
|
||
|
|