Look at people and items in a scene. Non-LLM coherence.

Adds the ability to inspect people or items in a scene. Also
technically props, but they are not first class entities (i.e. stored
in DB with an ID), so the LLM will behave oddly if you attempt to look
at a prop.

Also adds the foundation of non-LLM + LLM command coherence to fix up
LLM responses from LLM command execution. CommandEvents that are
recognized as incoherent will attempt to be fixed by the command
coherence layer. Currently only non-LLM coherence checks are done
(normalizing IDs, making sure they are right, etc).

Also many changes to command execution prompt to make it more
intelligent and correct.

Many foundational pieces for building actual game mechanics and
in-depth coherence are also added.
This commit is contained in:
projectmoon 2024-01-30 12:03:32 +01:00
parent ce89b61c19
commit 1d78d05d7a
20 changed files with 686 additions and 138 deletions

66
Cargo.lock generated
View File

@ -73,6 +73,7 @@ dependencies = [
"serde_json", "serde_json",
"strum", "strum",
"syn 1.0.109", "syn 1.0.109",
"tabled",
"textwrap", "textwrap",
"thiserror", "thiserror",
"tokio", "tokio",
@ -257,6 +258,12 @@ version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "bytecount"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@ -1200,6 +1207,17 @@ dependencies = [
"hashbrown 0.12.3", "hashbrown 0.12.3",
] ]
[[package]]
name = "papergrid"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ad43c07024ef767f9160710b3a6773976194758c7919b17e63b863db0bdf7fb"
dependencies = [
"bytecount",
"fnv",
"unicode-width",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -1379,6 +1397,30 @@ dependencies = [
"toml_edit", "toml_edit",
] ]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.69" version = "1.0.69"
@ -2060,6 +2102,30 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "tabled"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c998b0c8b921495196a48aabaf1901ff28be0760136e31604f7967b0792050e"
dependencies = [
"papergrid",
"tabled_derive",
"unicode-width",
]
[[package]]
name = "tabled_derive"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c138f99377e5d653a371cdad263615634cfc8467685dfe8e73e2b8e98f44b17"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "tap" name = "tap"
version = "1.0.1" version = "1.0.1"

View File

@ -27,6 +27,7 @@ itertools = "0.12.0"
crossterm = "0.27.0" crossterm = "0.27.0"
textwrap = "0.16.0" textwrap = "0.16.0"
config = "0.13.4" config = "0.13.4"
tabled = "0.15.0"
[build-dependencies] [build-dependencies]
prettyplease = "0.1.25" prettyplease = "0.1.25"

View File

@ -79,6 +79,9 @@ fn is_duplicate_recorded(failures: &[CoherenceFailure], exit: &Exit) -> bool {
false false
} }
/// This is currently for handling coherence when CREATING stuff in
/// the world. It's not doing coherence to fix things like command
/// execution.
pub(super) struct AiCoherence { pub(super) struct AiCoherence {
generator: Rc<AiGenerator>, generator: Rc<AiGenerator>,
} }

View File

@ -65,6 +65,8 @@ impl AiGenerator {
}; };
let mut cmds: ParsedCommands = self.parsing_convo.execute(&prompt).await?; let mut cmds: ParsedCommands = self.parsing_convo.execute(&prompt).await?;
cmds.original = cmd.to_owned();
let verbs = self.find_verbs(cmd).await?; let verbs = self.find_verbs(cmd).await?;
self.check_coherence(&verbs, &mut cmds).await?; self.check_coherence(&verbs, &mut cmds).await?;
Ok(cmds) Ok(cmds)
@ -102,8 +104,18 @@ impl AiGenerator {
Ok(()) Ok(())
} }
pub async fn execute_raw(&self, stage: &Stage, cmd: &ParsedCommand) -> Result<RawCommandExecution> { pub async fn execute_raw(
let prompt = execution_prompts::execution_prompt(stage, &cmd); &self,
stage: &Stage,
parsed_cmds: &ParsedCommands,
) -> Result<RawCommandExecution> {
//TODO handle multiple commands in list
if parsed_cmds.commands.is_empty() {
return Ok(RawCommandExecution::empty());
}
let cmd = &parsed_cmds.commands[0];
let prompt = execution_prompts::execution_prompt(&parsed_cmds.original, stage, &cmd);
let raw_exec: RawCommandExecution = self.execution_convo.execute(&prompt).await?; let raw_exec: RawCommandExecution = self.execution_convo.execute(&prompt).await?;
Ok(raw_exec) Ok(raw_exec)
} }

View File

@ -54,20 +54,7 @@ impl AiLogic {
stage: &Stage, stage: &Stage,
parsed_cmd: &ParsedCommands, parsed_cmd: &ParsedCommands,
) -> Result<RawCommandExecution> { ) -> Result<RawCommandExecution> {
//TODO handle multiple commands in list let raw_exec: RawCommandExecution = self.generator.execute_raw(stage, parsed_cmd).await?;
if parsed_cmd.commands.is_empty() {
return Ok(RawCommandExecution::empty());
}
let cmd = &parsed_cmd.commands[0];
let raw_exec: RawCommandExecution = self.generator.execute_raw(stage, cmd).await?;
// Coherence check:
// Set aside any events that are not in the enum
// Set aside anything with correct event, but wrong parameters.
// Ask LLM to fix them, if possible
//TODO make a aiclient::fix_execution
self.generator.reset_commands(); self.generator.reset_commands();
Ok(raw_exec) Ok(raw_exec)
} }

View File

@ -1,7 +1,76 @@
use crate::ai::convo::AiPrompt; use crate::ai::convo::AiPrompt;
use crate::models::commands::{ParsedCommand, CommandEvent, EventConversionFailures}; use crate::models::commands::{CommandEvent, EventConversionFailures, ParsedCommand};
use crate::models::world::scenes::{Scene, Stage}; use crate::models::world::items::Item;
use crate::models::world::people::Person;
use crate::models::world::scenes::{Exit, Prop, Scene, Stage};
use crate::models::Insertable;
use itertools::Itertools;
use strum::VariantNames; use strum::VariantNames;
use tabled::settings::Style;
use tabled::{Table, Tabled};
const UNKNOWN: &'static str = "unknown";
const PERSON: &'static str = "person";
const ITEM: &'static str = "item";
const PROP: &'static str = "prop";
const NO_KEY: &'static str = "n/a";
#[derive(Tabled)]
struct EntityTableRow<'a> {
name: &'a str,
#[tabled(rename = "type")]
entity_type: &'a str,
key: &'a str,
}
impl<'a> From<&'a Person> for EntityTableRow<'a> {
fn from(value: &'a Person) -> Self {
EntityTableRow {
name: &value.name,
key: value.key().unwrap_or(UNKNOWN),
entity_type: PERSON,
}
}
}
impl<'a> From<&'a Item> for EntityTableRow<'a> {
fn from(value: &'a Item) -> Self {
EntityTableRow {
name: &value.name,
key: value.key().unwrap_or(UNKNOWN),
entity_type: ITEM,
}
}
}
impl<'a> From<&'a Prop> for EntityTableRow<'a> {
fn from(value: &'a Prop) -> Self {
EntityTableRow {
name: &value.name,
entity_type: PROP,
key: NO_KEY,
}
}
}
#[derive(Tabled)]
pub struct ExitTableRow<'a> {
pub name: &'a str,
pub direction: &'a str,
pub scene_key: &'a str,
pub region: &'a str,
}
impl<'a> From<&'a Exit> for ExitTableRow<'a> {
fn from(value: &'a Exit) -> Self {
ExitTableRow {
name: &value.name,
direction: &value.direction,
scene_key: &value.scene_key,
region: &value.region,
}
}
}
const COMMAND_EXECUTION_BNF: &'static str = r#" const COMMAND_EXECUTION_BNF: &'static str = r#"
root ::= CommandExecution root ::= CommandExecution
@ -51,6 +120,9 @@ The following events can be generated:
- `change_scene`: The player's current scene is changed. - `change_scene`: The player's current scene is changed.
- `appliesTo` must be set to `player`. - `appliesTo` must be set to `player`.
- `parameter` must be the Scene Key of the new scene. - `parameter` must be the Scene Key of the new scene.
- `look_at_entity`: The player is looking at an entity--a person, prop, or item in the scene.
- `appliesTo` is the Scene Key of the current scene.
- `parameter` is the Entity Key of the entity being looked at.
- `take_damage`: The target of the event takes an amount of damage. - `take_damage`: The target of the event takes an amount of damage.
- `appliesTo` must be the target taking damage (player, NPC, item, prop, or other thing in the scene) - `appliesTo` must be the target taking damage (player, NPC, item, prop, or other thing in the scene)
- `parameter` must be the amount of damage taken. This value must be a positive integer. - `parameter` must be the amount of damage taken. This value must be a positive integer.
@ -73,13 +145,19 @@ The following events can be generated:
- `appliesTo` must be the target in the scene that the event would apply to, if it was a valid event. - `appliesTo` must be the target in the scene that the event would apply to, if it was a valid event.
- `parameter` should be a value that theoretically makes sense, if this event was a valid event. - `parameter` should be a value that theoretically makes sense, if this event was a valid event.
Check that the events make sense and are generated correctly, given the original command.
The original command is the raw text entered by the player.
**Original Command:** `{ORIGINAL_COMMAND}`
{SCENE_INFO} {SCENE_INFO}
**Player Command**: **Player Command**:
- Action: `{}` - Action: `{ACTION}`
- Target: `{}` - Target: `{TARGET}`
- Location: `{}` - Location: `{LOCATION}`
- Using: `{}` - Using: `{USING}`
[/INST] [/INST]
"#; "#;
@ -110,6 +188,12 @@ const SCENE_EXIT_INFO: &'static str = r#"
- Scene Location: `{EXIT_LOCATION}` - Scene Location: `{EXIT_LOCATION}`
"#; "#;
const SCENE_PERSON_INFO: &'static str = r#"
**Person:**:
- Name: `{PERSON_NAME}`
- Entity Key: `{PERSON_KEY}`
"#;
fn unrecognized_event_solution(event_name: &str) -> String { fn unrecognized_event_solution(event_name: &str) -> String {
let valid_events = CommandEvent::VARIANTS let valid_events = CommandEvent::VARIANTS
.iter() .iter()
@ -123,10 +207,9 @@ fn unrecognized_event_solution(event_name: &str) -> String {
} }
fn stage_info(stage: &Stage) -> String { fn stage_info(stage: &Stage) -> String {
let scene_description = "**Scene Description:** ".to_string() + &stage.scene.description; let mut info = "# SCENE INFORMATION\n\n".to_string();
let mut info = "**Scene Information:**\n".to_string();
info.push_str("## CURRENT SCENE INFORMATION\n\n");
info.push_str(" - Key: "); info.push_str(" - Key: ");
info.push_str(&format!("`{}`", stage.key)); info.push_str(&format!("`{}`", stage.key));
info.push_str("\n"); info.push_str("\n");
@ -137,72 +220,38 @@ fn stage_info(stage: &Stage) -> String {
info.push_str(" - Location: "); info.push_str(" - Location: ");
info.push_str(&stage.scene.region); info.push_str(&stage.scene.region);
info.push_str("\n");
let people: String = stage
.people
.iter()
.map(|p| format!(" - {}", p.name))
.collect::<Vec<_>>()
.join("\n");
info.push_str("**People:**\n");
info.push_str(&people);
info.push_str("\n");
let items: String = stage
.items
.iter()
.map(|i| format!(" - {} ({})", i.name, i.category))
.collect::<Vec<_>>()
.join("\n");
info.push_str("**Items:**\n");
info.push_str(&items);
info.push_str("\n");
let props: String = stage
.scene
.props
.iter()
.map(|p| format!(" - {}", p.name))
.collect::<Vec<_>>()
.join("\n");
info.push_str("**Props:**\n");
info.push_str(&props);
info.push_str("\n\n"); info.push_str("\n\n");
let exits: String = stage let people = stage.people.iter().map_into::<EntityTableRow>();
.scene let items = stage.items.iter().map_into::<EntityTableRow>();
.exits let props = stage.scene.props.iter().map_into::<EntityTableRow>();
.iter() let entities = people.chain(items).chain(props);
.map(|e| {
SCENE_EXIT_INFO
.replacen("{EXIT_NAME}", &e.name, 1)
.replacen("{DIRECTION}", &e.direction, 1)
.replacen("{SCENE_KEY}", &e.scene_key, 1)
.replacen("{EXIT_LOCATION}", &e.region, 1)
})
.collect::<Vec<_>>()
.join("\n\n");
info.push_str(&exits); let mut entities_table = Table::new(entities);
entities_table.with(Style::markdown());
info.push_str(&scene_description); info.push_str("## ENTITIES\n\n");
info.push_str(&entities_table.to_string());
info.push_str("\n\n");
let mut exits = Table::new(stage.scene.exits.iter().map_into::<ExitTableRow>());
exits.with(Style::markdown());
info.push_str("## EXITS\n\n");
info.push_str(&exits.to_string());
info info
} }
pub fn execution_prompt(stage: &Stage, cmd: &ParsedCommand) -> AiPrompt { pub fn execution_prompt(original_cmd: &str, stage: &Stage, cmd: &ParsedCommand) -> AiPrompt {
let scene_info = stage_info(&stage); let scene_info = stage_info(&stage);
let prompt = COMMAND_EXECUTION_PROMPT let prompt = COMMAND_EXECUTION_PROMPT
.replacen("{SCENE_INFO}", &scene_info, 1) .replacen("{SCENE_INFO}", &scene_info, 1)
.replacen("{}", &cmd.verb, 1) .replacen("{ORIGINAL_COMMAND}", &original_cmd, 1)
.replacen("{}", &cmd.target, 1) .replacen("{ACTION}", &cmd.verb, 1)
.replacen("{}", &cmd.location, 1) .replacen("{TARGET}", &cmd.target, 1)
.replacen("{}", &cmd.using, 1); .replacen("{LOCATION}", &cmd.location, 1)
.replacen("{USING}", &cmd.using, 1);
AiPrompt::new_with_grammar_and_size(&prompt, COMMAND_EXECUTION_BNF, 512) AiPrompt::new_with_grammar_and_size(&prompt, COMMAND_EXECUTION_BNF, 512)
} }

View File

@ -9,5 +9,5 @@ pub fn check_builtin_command(stage: &Stage, cmd: &str) -> Option<BuiltinCommand>
} }
fn look_command(_stage: &Stage) -> Option<BuiltinCommand> { fn look_command(_stage: &Stage) -> Option<BuiltinCommand> {
Some(BuiltinCommand::Look) Some(BuiltinCommand::LookAtScene)
} }

187
src/commands/coherence.rs Normal file
View File

@ -0,0 +1,187 @@
use super::converter::validate_event_coherence;
use crate::{
ai::logic::AiLogic,
db::Database,
models::{
commands::{AiCommand, CommandEvent, EventCoherenceFailure, ExecutionConversionResult},
world::scenes::{root_scene_id, Stage},
},
};
use anyhow::{anyhow, Result as AnyhowResult};
use futures::stream::{self, StreamExt};
use futures::{future, TryFutureExt};
use std::rc::Rc;
use uuid::Uuid;
type CoherenceResult = Result<CommandEvent, EventCoherenceFailure>;
type OptionalCoherenceResult = Result<Option<CommandEvent>, EventCoherenceFailure>;
pub struct CommandCoherence<'a> {
logic: Rc<AiLogic>,
db: Rc<Database>,
stage: &'a Stage,
}
impl CommandCoherence<'_> {
pub fn new<'a>(
logic: &Rc<AiLogic>,
db: &Rc<Database>,
stage: &'a Stage,
) -> CommandCoherence<'a> {
CommandCoherence {
logic: logic.clone(),
db: db.clone(),
stage,
}
}
pub async fn fix_incoherent_events(
&self,
failures: Vec<EventCoherenceFailure>,
) -> ExecutionConversionResult {
let (successes, failures): (Vec<_>, Vec<_>) = stream::iter(failures.into_iter())
.then(|failure| self.cohere_event(failure))
.fold(
(vec![], vec![]),
|(mut successes, mut failures), res| async {
match res {
Ok(event) => successes.push(event),
Err(err) => failures.push(err),
};
(successes, failures)
},
)
.await;
// TODO we need to use LLM on events that have failed non-LLM coherence.
if successes.len() > 0 && failures.len() == 0 {
ExecutionConversionResult::Success(AiCommand::from_events(successes))
} else if successes.len() > 0 && failures.len() > 0 {
ExecutionConversionResult::PartialSuccess(
AiCommand::from_events(successes),
failures.into(),
)
} else {
ExecutionConversionResult::Failure(failures.into())
}
}
async fn cohere_event(&self, failure: EventCoherenceFailure) -> CoherenceResult {
let event = async {
match failure {
EventCoherenceFailure::TargetDoesNotExist(event) => {
self.fix_target_does_not_exist(event).await
}
EventCoherenceFailure::OtherError(event, _) => future::ok(event).await,
}
};
event
.and_then(|e| validate_event_coherence(&self.db, e))
.await
}
async fn fix_target_does_not_exist(&self, mut event: CommandEvent) -> CoherenceResult {
if let CommandEvent::LookAtEntity {
ref mut entity_key,
ref mut scene_key,
} = event
{
let res = cohere_scene_and_entity(&self.db, &self.stage, entity_key, scene_key).await;
match res {
Ok(_) => Ok(event),
Err(err) => Err(EventCoherenceFailure::OtherError(event, err.to_string())),
}
} else {
Ok(event)
}
}
}
/// Directly mutates an entity and scene key to make scene, if
/// possible.
async fn cohere_scene_and_entity(
db: &Database,
stage: &Stage,
entity_key: &mut String,
scene_key: &mut String,
) -> AnyhowResult<()> {
// Normalize UUIDs, assuming that they are proper UUIDs.
normalize_keys(scene_key, entity_key);
// Sometimes scene key is actually the entity key, and the entity
// key is blank.
if !scene_key.is_empty() && scene_key != &stage.key {
// Check if scene key is an entity
if db.entity_exists(&stage.key, &scene_key).await? {
entity_key.clear();
entity_key.push_str(&scene_key);
scene_key.clear();
scene_key.push_str(&stage.key);
}
}
// If scene key isn't valid, override it from known-good
// information.
if !is_valid_scene_key(scene_key) {
scene_key.clear();
scene_key.push_str(&stage.key);
}
// If entity key is not a valid UUID at this point, then we have
// entered a weird situation.
if Uuid::try_parse(&entity_key).is_err() {
return Err(anyhow!("Entity key is not a UUID"));
}
// If the scene key and entity key are the same at this point,
// then we have entered a weird situation.
if scene_key == entity_key {
return Err(anyhow!("Scene key and entity key are the same"));
}
// It is often likely that the scene key and entity key are reversed.
if db.entity_exists(&entity_key, &scene_key).await? {
std::mem::swap(entity_key, scene_key);
}
Ok(())
}
fn is_valid_scene_key(scene_key: &str) -> bool {
scene_key == root_scene_id() || Uuid::try_parse(&scene_key).is_ok()
}
/// Used as basic sanitization for raw command parameters and
/// applies_to.
#[inline]
pub fn strip_prefixes(value: String) -> String {
value
.strip_prefix("scenes/")
.and_then(|s| s.strip_prefix("people/"))
.and_then(|s| s.strip_prefix("items/"))
.map(String::from)
.unwrap_or(value)
}
/// Make sure entity keys are valid UUIDs, and fix them if possible.
fn normalize_keys(scene_key: &mut String, entity_key: &mut String) {
if let Some(normalized) = normalize_uuid(&scene_key) {
scene_key.clear();
scene_key.push_str(&normalized);
}
if let Some(normalized) = normalize_uuid(&entity_key) {
entity_key.clear();
entity_key.push_str(&normalized);
}
}
fn normalize_uuid(uuid_str: &str) -> Option<String> {
Uuid::parse_str(uuid_str.replace("-", "").as_ref())
.ok()
.map(|parsed| parsed.as_hyphenated().to_string())
}

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
db::Database, db::Database,
models::commands::{ models::commands::{
CommandEvent, AiCommand, EventCoherenceFailure, EventConversionError, AiCommand, CommandEvent, EventCoherenceFailure, EventConversionError,
EventConversionFailures, ExecutionConversionResult, RawCommandEvent, RawCommandExecution, EventConversionFailures, ExecutionConversionResult, RawCommandEvent, RawCommandExecution,
}, },
}; };
@ -9,6 +9,7 @@ use anyhow::Result;
use futures::stream::{self, StreamExt, TryStreamExt}; use futures::stream::{self, StreamExt, TryStreamExt};
use itertools::{Either, Itertools}; use itertools::{Either, Itertools};
use std::convert::TryFrom; use std::convert::TryFrom;
use super::coherence::strip_prefixes;
use strum::VariantNames; use strum::VariantNames;
@ -50,10 +51,10 @@ fn from_raw_success(raw: Narrative, events: Vec<CommandEvent>) -> AiCommand {
Some(reason) if !raw.valid && reason.is_empty() => { Some(reason) if !raw.valid && reason.is_empty() => {
Some("invalid for unknown reason".to_string()) Some("invalid for unknown reason".to_string())
} }
Some(_) if !raw.valid => raw.reason.clone(), Some(_) if !raw.valid => raw.reason,
_ => None, _ => None,
}, },
narration: raw.narration.clone(), narration: raw.narration,
} }
} }
@ -118,36 +119,35 @@ fn deserialize_recognized_event(
let event_name = event_name.as_str(); let event_name = event_name.as_str();
match event_name { match event_name {
// informational-related
"narration" => Ok(CommandEvent::Narration(raw_event.parameter)),
"look_at_entity" => Ok(CommandEvent::LookAtEntity {
entity_key: strip_prefixes(raw_event.parameter),
scene_key: strip_prefixes(raw_event.applies_to),
}),
// scene-related // scene-related
"change_scene" => Ok(CommandEvent::ChangeScene { "change_scene" => Ok(CommandEvent::ChangeScene {
scene_key: raw_event scene_key: strip_prefixes(raw_event.parameter),
.parameter
.strip_prefix("scenes/") // Mini coherence check
.map(String::from)
.unwrap_or(raw_event.parameter)
.clone(),
}), }),
// bodily position-related // bodily position-related
"stand" => Ok(CommandEvent::Stand { "stand" => Ok(CommandEvent::Stand {
target: raw_event.applies_to, target: strip_prefixes(raw_event.applies_to),
}), }),
"sit" => Ok(CommandEvent::Sit { "sit" => Ok(CommandEvent::Sit {
target: raw_event.applies_to, target: strip_prefixes(raw_event.applies_to),
}), }),
"prone" => Ok(CommandEvent::Prone { "prone" => Ok(CommandEvent::Prone {
target: raw_event.applies_to, target: strip_prefixes(raw_event.applies_to),
}), }),
"crouch" => Ok(CommandEvent::Crouch { "crouch" => Ok(CommandEvent::Crouch {
target: raw_event.applies_to, target: strip_prefixes(raw_event.applies_to),
}), }),
// combat-related // combat-related
"take_damage" => deserialize_take_damage(raw_event), "take_damage" => deserialize_take_damage(raw_event),
// miscellaneous
"narration" => Ok(CommandEvent::Narration(raw_event.parameter)),
// unrecognized // unrecognized
_ => Err(EventConversionError::UnrecognizedEvent(raw_event)), _ => Err(EventConversionError::UnrecognizedEvent(raw_event)),
} }
@ -158,18 +158,28 @@ fn deserialize_take_damage(
) -> Result<CommandEvent, EventConversionError> { ) -> Result<CommandEvent, EventConversionError> {
match raw_event.parameter.parse::<u32>() { match raw_event.parameter.parse::<u32>() {
Ok(dmg) => Ok(CommandEvent::TakeDamage { Ok(dmg) => Ok(CommandEvent::TakeDamage {
target: raw_event.applies_to, target: strip_prefixes(raw_event.applies_to),
amount: dmg, amount: dmg,
}), }),
Err(_) => Err(EventConversionError::InvalidParameter(raw_event)), Err(_) => Err(EventConversionError::InvalidParameter(raw_event)),
} }
} }
async fn validate_event_coherence<'a>( pub(super) async fn validate_event_coherence<'a>(
db: &Database, db: &Database,
event: CommandEvent, event: CommandEvent,
) -> std::result::Result<CommandEvent, EventCoherenceFailure> { ) -> std::result::Result<CommandEvent, EventCoherenceFailure> {
match event { match event {
CommandEvent::LookAtEntity {
ref entity_key,
ref scene_key,
} => match db.entity_exists(&scene_key, &entity_key).await {
Ok(exists) => match exists {
true => Ok(event),
false => Err(invalid_converted_event(event).unwrap()),
},
Err(err) => Err(invalid_converted_event_because_err(event, err)),
},
CommandEvent::ChangeScene { ref scene_key } => match db.stage_exists(&scene_key).await { CommandEvent::ChangeScene { ref scene_key } => match db.stage_exists(&scene_key).await {
Ok(exists) => match exists { Ok(exists) => match exists {
true => Ok(event), true => Ok(event),
@ -185,6 +195,7 @@ async fn validate_event_coherence<'a>(
/// information contained in the response is not valid. /// information contained in the response is not valid.
fn invalid_converted_event(event: CommandEvent) -> Option<EventCoherenceFailure> { fn invalid_converted_event(event: CommandEvent) -> Option<EventCoherenceFailure> {
match event { match event {
CommandEvent::LookAtEntity { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)),
CommandEvent::ChangeScene { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)), CommandEvent::ChangeScene { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)),
_ => None, _ => None,
} }

View File

@ -3,16 +3,17 @@ use crate::{
db::Database, db::Database,
models::{ models::{
commands::{ commands::{
AiCommand, BuiltinCommand, CommandExecution, ExecutionConversionResult, ParsedCommand, AiCommand, CommandEvent, CommandExecution, EventCoherenceFailure,
ParsedCommands, RawCommandExecution, ExecutionConversionResult, ParsedCommand, ParsedCommands, RawCommandExecution,
}, },
world::scenes::Stage, world::scenes::Stage,
}, },
}; };
use anyhow::{anyhow, Result}; use anyhow::Result;
use std::rc::Rc; use std::rc::Rc;
pub mod builtins; pub mod builtins;
pub mod coherence;
pub mod converter; pub mod converter;
fn directional_command(direction: &str) -> ParsedCommand { fn directional_command(direction: &str) -> ParsedCommand {
@ -45,10 +46,7 @@ fn translate(cmd: &str) -> Option<ParsedCommands> {
_ => None, _ => None,
}; };
cmd.map(|c| ParsedCommands { cmd.map(|c| ParsedCommands::single(&format!("go {}", c.target), c))
commands: vec![c],
count: 1,
})
} }
pub struct CommandExecutor { pub struct CommandExecutor {
@ -79,8 +77,6 @@ impl CommandExecutor {
} }
pub async fn execute(&self, stage: &Stage, cmd: &str) -> Result<CommandExecution> { pub async fn execute(&self, stage: &Stage, cmd: &str) -> Result<CommandExecution> {
CommandExecution::AiCommand(AiCommand::empty());
if let Some(builtin) = builtins::check_builtin_command(stage, cmd) { if let Some(builtin) = builtins::check_builtin_command(stage, cmd) {
return Ok(CommandExecution::Builtin(builtin)); return Ok(CommandExecution::Builtin(builtin));
} }
@ -91,25 +87,63 @@ impl CommandExecutor {
} else { } else {
let (cmds_to_cache, execution) = self.logic.execute(stage, cmd).await?; let (cmds_to_cache, execution) = self.logic.execute(stage, cmd).await?;
self.db if execution.valid && cmds_to_cache.commands.len() > 0 {
.cache_command(cmd, &stage.scene, &cmds_to_cache) self.db
.await?; .cache_command(cmd, &stage.scene, &cmds_to_cache)
.await?;
}
execution execution
}; };
let converted = converter::convert_raw_execution(raw_exec, &self.db).await; let converted = converter::convert_raw_execution(raw_exec, &self.db).await;
//TODO handle the errored events aside from getting rid of them let execution: Result<AiCommand> = match converted {
let execution: AiCommand = match converted {
ExecutionConversionResult::Success(execution) => Ok(execution), ExecutionConversionResult::Success(execution) => Ok(execution),
ExecutionConversionResult::PartialSuccess(execution, _) => Ok(execution), ExecutionConversionResult::PartialSuccess(mut execution, failures) => {
ExecutionConversionResult::Failure(failures) => Err(anyhow!( // TODO also deal with conversion failures
"unhandled command execution failure: {:?}", // TODO deal with failures to fix incoherent events.
failures // right now we just drop them.
)), let mut fixed_events = self
}?; .fix_incoherence(stage, failures.coherence_failures)
.await
.ok()
.unwrap_or(vec![]);
execution.events.append(&mut fixed_events);
Ok(execution)
}
ExecutionConversionResult::Failure(failures) => {
// TODO also deal with conversion failures
// For a complete failure, we want to make sure all
// events become coherent.
Ok(AiCommand::from_events(
self.fix_incoherence(stage, failures.coherence_failures)
.await?,
))
}
};
let execution = execution?;
Ok(CommandExecution::AiCommand(execution)) Ok(CommandExecution::AiCommand(execution))
} }
async fn fix_incoherence(
&self,
stage: &Stage,
failures: Vec<EventCoherenceFailure>,
) -> Result<Vec<CommandEvent>> {
println!("Attempting to fix {} incoherent events", failures.len());
let fixer = coherence::CommandCoherence::new(&self.logic, &self.db, stage);
// TODO should do something w/ partial failures.
let events = match fixer.fix_incoherent_events(failures).await {
ExecutionConversionResult::Success(AiCommand { events, .. }) => Ok(events),
ExecutionConversionResult::PartialSuccess(AiCommand { events, .. }, _) => Ok(events),
ExecutionConversionResult::Failure(errs) => Err(errs),
}?;
Ok(events)
}
} }

View File

@ -1,6 +1,6 @@
use crate::models::commands::{CachedParsedCommand, ParsedCommand, ParsedCommands}; use crate::models::commands::{CachedParsedCommand, ParsedCommand, ParsedCommands};
use crate::models::world::scenes::{Scene, Stage, StageOrStub}; use crate::models::world::scenes::{Scene, Stage, StageOrStub};
use crate::models::{Content, ContentContainer, Insertable}; use crate::models::{Content, ContentContainer, Insertable, Entity};
use anyhow::Result; use anyhow::Result;
use arangors::document::options::InsertOptions; use arangors::document::options::InsertOptions;
use arangors::graph::{EdgeDefinition, Graph}; use arangors::graph::{EdgeDefinition, Graph};
@ -384,6 +384,34 @@ impl Database {
Ok(stage_count > 0) Ok(stage_count > 0)
} }
pub async fn entity_exists(&self, scene_key: &str, entity_key: &str) -> Result<bool> {
let mut vars = HashMap::new();
vars.insert("@scene_collection", SCENE_COLLECTION.into());
vars.insert("scene_key", to_json_value(scene_key).unwrap());
vars.insert("entity_key", to_json_value(entity_key).unwrap());
let db = self.db().await?;
let entity_count = db
.aql_bind_vars::<JsonValue>(queries::LOAD_ENTITY, vars)
.await?
.len();
Ok(entity_count > 0)
}
pub async fn load_entity(&self, scene_key: &str, entity_key: &str) -> Result<Option<Entity>> {
let aql = AqlQuery::builder()
.query(queries::LOAD_ENTITY)
.bind_var("@scene_collection", SCENE_COLLECTION)
.bind_var("scene_key", to_json_value(scene_key)?)
.bind_var("entity_key", to_json_value(entity_key)?)
.build();
let results = self.db().await?.aql_query(aql).await?;
Ok(take_first(results))
}
pub async fn cache_command( pub async fn cache_command(
&self, &self,
raw_cmd: &str, raw_cmd: &str,

View File

@ -24,6 +24,25 @@ pub const LOAD_STAGE: &'static str = r#"
} }
"#; "#;
pub const LOAD_ENTITY: &'static str = r#"
LET entities = (
FOR scene IN @@scene_collection
FILTER scene._key == @scene_key
LET occupants = (FOR v, edge IN OUTBOUND scene._id GRAPH 'world'
FILTER edge.relation == "scene-has-person" and v._key == @entity_key
RETURN MERGE({ "type": "Person"}, v))
LET items = (FOR v, edge IN OUTBOUND scene._id GRAPH 'world'
FILTER edge.relation == "item-located-at" and v._key == @entity_key
RETURN MERGE({ "type": "Item" }, v ))
RETURN FIRST(APPEND(occupants, items)))
FOR ent in entities
FILTER ent != null
RETURN ent
"#;
pub const UPSERT_SCENE: &'static str = r#" pub const UPSERT_SCENE: &'static str = r#"
UPSERT { _key: @scene_key } UPSERT { _key: @scene_key }
INSERT <SCENE_JSON> INSERT <SCENE_JSON>

View File

@ -1,5 +1,7 @@
use crate::io::display; use crate::io::display;
use crate::models::commands::{AiCommand, BuiltinCommand, CommandExecution}; use crate::models::commands::{
AiCommand, BuiltinCommand, CommandExecution, ExecutionConversionResult, EventConversionFailures,
};
use crate::state::GameState; use crate::state::GameState;
use crate::{commands::CommandExecutor, db::Database}; use crate::{commands::CommandExecutor, db::Database};
use anyhow::Result; use anyhow::Result;
@ -51,26 +53,29 @@ impl GameLoop {
// TODO this will probably eventually be moved to its own file. // TODO this will probably eventually be moved to its own file.
async fn handle_builtin(&mut self, builtin: BuiltinCommand) -> Result<()> { async fn handle_builtin(&mut self, builtin: BuiltinCommand) -> Result<()> {
match builtin { match builtin {
BuiltinCommand::Look => display!("{}", self.state.current_scene), BuiltinCommand::LookAtScene => display!("{}", self.state.current_scene),
}; };
Ok(()) Ok(())
} }
async fn handle_execution(&mut self, execution: CommandExecution) -> Result<()> { async fn handle_execution(&mut self, execution: Result<CommandExecution>) -> Result<()> {
match execution { if let Ok(execution) = execution {
CommandExecution::Builtin(builtin) => self.handle_builtin(builtin).await?, match execution {
CommandExecution::AiCommand(exec) => self.handle_ai_command(exec).await?, CommandExecution::Builtin(builtin) => self.handle_builtin(builtin).await?,
}; CommandExecution::AiCommand(exec) => self.handle_ai_command(exec).await?,
};
} else {
display!("{}", execution.unwrap_err());
}
Ok(()) Ok(())
} }
async fn handle_input(&mut self, cmd: &str) -> Result<()> { async fn handle_input(&mut self, cmd: &str) -> Result<()> {
if !cmd.is_empty() { if !cmd.is_empty() {
//let execution = self.execute_command(cmd).await?;
let mut stage = &self.state.current_scene; let mut stage = &self.state.current_scene;
let execution = self.executor.execute(&mut stage, cmd).await?; let execution = self.executor.execute(&mut stage, cmd).await;
self.handle_execution(execution).await?; self.handle_execution(execution).await?;
} }

View File

@ -4,7 +4,7 @@ use game_loop::GameLoop;
use ai::logic::AiLogic; use ai::logic::AiLogic;
use models::world::scenes::{root_scene_id, Stage}; use models::world::scenes::{root_scene_id, Stage};
use state::GameState; use state::GameState;
use std::{io::stdout, rc::Rc, time::Duration}; use std::{io::stdout, rc::Rc, time::Duration, str::FromStr};
use arangors::Connection; use arangors::Connection;

View File

@ -1,9 +1,9 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{EnumString, EnumVariantNames}; use strum::{EnumString, EnumVariantNames};
use thiserror::Error; use thiserror::Error;
use super::world::scenes::Stage;
/// Stored in the database to bypass AI 'parsing' when possible. /// Stored in the database to bypass AI 'parsing' when possible.
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CachedParsedCommand { pub struct CachedParsedCommand {
@ -14,10 +14,22 @@ pub struct CachedParsedCommand {
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ParsedCommands { pub struct ParsedCommands {
#[serde(default)]
pub original: String, // The original text entered by the player, set by code.
pub commands: Vec<ParsedCommand>, pub commands: Vec<ParsedCommand>,
pub count: usize, pub count: usize,
} }
impl ParsedCommands {
pub fn single(original: &str, cmd: ParsedCommand) -> ParsedCommands {
ParsedCommands {
original: original.to_owned(),
commands: vec![cmd],
count: 1,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ParsedCommand { pub struct ParsedCommand {
pub verb: String, pub verb: String,
@ -74,14 +86,23 @@ pub struct RawCommandEvent {
#[strum(serialize_all = "snake_case")] #[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum CommandEvent { pub enum CommandEvent {
// Informational
Narration(String),
LookAtEntity {
entity_key: String,
scene_key: String,
},
// Movement-related
ChangeScene { ChangeScene {
scene_key: String, scene_key: String,
}, },
// Player character state
TakeDamage { TakeDamage {
target: String, target: String,
amount: u32, amount: u32,
}, },
Narration(String),
Stand { Stand {
target: String, target: String,
}, },
@ -105,10 +126,12 @@ pub enum CommandEvent {
/// builtin command is only created directly via checking for builtin /// builtin command is only created directly via checking for builtin
/// commands. These commands may have little or no parameters, as they /// commands. These commands may have little or no parameters, as they
/// are meant for simple, direct commands like looking, movement, etc. /// are meant for simple, direct commands like looking, movement, etc.
#[derive(Debug)]
pub enum BuiltinCommand { pub enum BuiltinCommand {
Look, LookAtScene,
} }
#[derive(Debug)]
pub enum CommandExecution { pub enum CommandExecution {
Builtin(BuiltinCommand), Builtin(BuiltinCommand),
AiCommand(AiCommand), AiCommand(AiCommand),
@ -143,6 +166,15 @@ impl AiCommand {
events: vec![], events: vec![],
} }
} }
pub fn from_events(events: Vec<CommandEvent>) -> AiCommand {
AiCommand {
valid: true,
reason: None,
narration: "".to_string(),
events,
}
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -152,7 +184,13 @@ pub enum ExecutionConversionResult {
Failure(EventConversionFailures), Failure(EventConversionFailures),
} }
#[derive(Clone, Debug)] impl Display for ExecutionConversionResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Error, Clone, Debug)]
pub struct EventConversionFailures { pub struct EventConversionFailures {
pub conversion_failures: Vec<EventConversionError>, pub conversion_failures: Vec<EventConversionError>,
pub coherence_failures: Vec<EventCoherenceFailure>, pub coherence_failures: Vec<EventCoherenceFailure>,
@ -170,6 +208,21 @@ impl EventConversionFailures {
} }
} }
impl From<Vec<EventCoherenceFailure>> for EventConversionFailures {
fn from(value: Vec<EventCoherenceFailure>) -> Self {
EventConversionFailures {
coherence_failures: value,
conversion_failures: vec![],
}
}
}
impl Display for EventConversionFailures {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Error, Clone, Debug)] #[derive(Error, Clone, Debug)]
pub enum EventConversionError { pub enum EventConversionError {
#[error("invalid parameter for {0:?}")] #[error("invalid parameter for {0:?}")]
@ -187,3 +240,19 @@ pub enum EventCoherenceFailure {
#[error("uncategorized coherence failure: {1}")] #[error("uncategorized coherence failure: {1}")]
OtherError(CommandEvent, String), OtherError(CommandEvent, String),
} }
impl EventCoherenceFailure {
/// Consume self to extract the CommandEvent wrapped in this enum.
pub fn as_event(self) -> CommandEvent {
match self {
EventCoherenceFailure::OtherError(event, _) => event,
Self::TargetDoesNotExist(event) => event,
}
}
}
impl From<EventCoherenceFailure> for CommandEvent {
fn from(value: EventCoherenceFailure) -> Self {
value.as_event()
}
}

View File

@ -1,6 +1,7 @@
use self::world::items::Item; use self::world::items::Item;
use self::world::people::Person; use self::world::people::Person;
use self::world::scenes::{Scene, SceneStub}; use self::world::scenes::{Scene, SceneStub};
use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
// Has to come before any module declarations! // Has to come before any module declarations!
@ -38,9 +39,9 @@ macro_rules! impl_insertable {
}; };
} }
pub mod coherence;
pub mod commands; pub mod commands;
pub mod world; pub mod world;
pub mod coherence;
pub fn new_uuid_string() -> String { pub fn new_uuid_string() -> String {
let uuid = Uuid::now_v7(); let uuid = Uuid::now_v7();
@ -191,3 +192,56 @@ impl Insertable for Content {
old_key old_key
} }
} }
/// An entity in a scene that can be loaded from the database. Similar
/// to but different from Content/ContentRelation.
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Entity {
Person(world::people::Person),
Item(world::items::Item),
}
impl Insertable for Entity {
fn id(&self) -> Option<&str> {
match self {
Entity::Person(person) => person.id(),
Entity::Item(item) => item.id(),
}
}
fn key(&self) -> Option<&str> {
match self {
Entity::Person(person) => person.key(),
Entity::Item(item) => item.key(),
}
}
fn set_id(&mut self, id: String) -> Option<String> {
match self {
Entity::Person(person) => person.set_id(id),
Entity::Item(item) => item.set_id(id),
}
}
fn set_key(&mut self, key: String) -> Option<String> {
match self {
Entity::Person(person) => person.set_key(key),
Entity::Item(item) => item.set_key(key),
}
}
fn take_id(&mut self) -> Option<String> {
match self {
Entity::Person(person) => person.take_id(),
Entity::Item(item) => item.take_id(),
}
}
fn take_key(&mut self) -> Option<String> {
match self {
Entity::Person(person) => person.take_key(),
Entity::Item(item) => item.take_key(),
}
}
}

View File

@ -1,6 +1,7 @@
use crate::models::new_uuid_string; use crate::models::new_uuid_string;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{EnumString, EnumVariantNames, Display}; use strum::{Display, EnumString, EnumVariantNames};
use tabled::Tabled;
use super::super::Insertable; use super::super::Insertable;

View File

@ -1,4 +1,5 @@
use crate::models::new_uuid_string; use crate::models::new_uuid_string;
use tabled::Tabled;
use super::super::Insertable; use super::super::Insertable;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -1,8 +1,9 @@
use crate::models::world::items::Item;
use crate::models::world::people::Person; use crate::models::world::people::Person;
use crate::models::{new_uuid_string, Insertable}; use crate::models::{new_uuid_string, Insertable};
use crate::{db::Key, models::world::items::Item};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::OnceLock; use std::sync::OnceLock;
use tabled::Tabled;
use super::raw::{ExitSeed, PropSeed}; use super::raw::{ExitSeed, PropSeed};

View File

@ -1,4 +1,5 @@
use crate::models::Insertable; use crate::io::display;
use crate::models::{Entity, Insertable};
use crate::{ use crate::{
ai::logic::AiLogic, ai::logic::AiLogic,
db::Database, db::Database,
@ -24,6 +25,7 @@ impl GameState {
match event { match event {
CommandEvent::ChangeScene { scene_key } => self.change_scene(&scene_key).await?, CommandEvent::ChangeScene { scene_key } => self.change_scene(&scene_key).await?,
CommandEvent::Narration(narration) => println!("\n\n{}\n\n", narration), CommandEvent::Narration(narration) => println!("\n\n{}\n\n", narration),
CommandEvent::LookAtEntity { ref entity_key, .. } => self.look_at(entity_key).await?,
_ => (), _ => (),
} }
@ -60,4 +62,22 @@ impl GameState {
Ok(()) Ok(())
} }
async fn look_at(&mut self, entity_key: &str) -> Result<()> {
let maybe_entity = self
.db
.load_entity(&self.current_scene.key, entity_key)
.await?;
if let Some(entity) = maybe_entity {
match entity {
Entity::Item(item) => display!(item.description),
Entity::Person(person) => display!(person.description),
}
display!("\n");
}
Ok(())
}
} }