diff --git a/Cargo.lock b/Cargo.lock index 6ac9000..ef50587 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,7 @@ dependencies = [ "serde_json", "strum", "syn 1.0.109", + "tabled", "textwrap", "thiserror", "tokio", @@ -257,6 +258,12 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytecount" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" + [[package]] name = "byteorder" version = "1.5.0" @@ -1200,6 +1207,17 @@ dependencies = [ "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]] name = "parking_lot" version = "0.12.1" @@ -1379,6 +1397,30 @@ dependencies = [ "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]] name = "proc-macro2" version = "1.0.69" @@ -2060,6 +2102,30 @@ dependencies = [ "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]] name = "tap" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index d0f667b..2e3029b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ itertools = "0.12.0" crossterm = "0.27.0" textwrap = "0.16.0" config = "0.13.4" +tabled = "0.15.0" [build-dependencies] prettyplease = "0.1.25" diff --git a/src/ai/coherence.rs b/src/ai/coherence.rs index a2dbffe..9671b9c 100644 --- a/src/ai/coherence.rs +++ b/src/ai/coherence.rs @@ -79,6 +79,9 @@ fn is_duplicate_recorded(failures: &[CoherenceFailure], exit: &Exit) -> bool { 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 { generator: Rc, } diff --git a/src/ai/generator.rs b/src/ai/generator.rs index 2ed9a0d..9cc1a7d 100644 --- a/src/ai/generator.rs +++ b/src/ai/generator.rs @@ -65,6 +65,8 @@ impl AiGenerator { }; let mut cmds: ParsedCommands = self.parsing_convo.execute(&prompt).await?; + cmds.original = cmd.to_owned(); + let verbs = self.find_verbs(cmd).await?; self.check_coherence(&verbs, &mut cmds).await?; Ok(cmds) @@ -102,8 +104,18 @@ impl AiGenerator { Ok(()) } - pub async fn execute_raw(&self, stage: &Stage, cmd: &ParsedCommand) -> Result { - let prompt = execution_prompts::execution_prompt(stage, &cmd); + pub async fn execute_raw( + &self, + stage: &Stage, + parsed_cmds: &ParsedCommands, + ) -> Result { + //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?; Ok(raw_exec) } diff --git a/src/ai/logic.rs b/src/ai/logic.rs index 38f45ec..2999ab5 100644 --- a/src/ai/logic.rs +++ b/src/ai/logic.rs @@ -54,20 +54,7 @@ impl AiLogic { stage: &Stage, parsed_cmd: &ParsedCommands, ) -> Result { - //TODO handle multiple commands in list - 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 - + let raw_exec: RawCommandExecution = self.generator.execute_raw(stage, parsed_cmd).await?; self.generator.reset_commands(); Ok(raw_exec) } diff --git a/src/ai/prompts/execution_prompts.rs b/src/ai/prompts/execution_prompts.rs index 5f7841c..a046361 100644 --- a/src/ai/prompts/execution_prompts.rs +++ b/src/ai/prompts/execution_prompts.rs @@ -1,7 +1,76 @@ use crate::ai::convo::AiPrompt; -use crate::models::commands::{ParsedCommand, CommandEvent, EventConversionFailures}; -use crate::models::world::scenes::{Scene, Stage}; +use crate::models::commands::{CommandEvent, EventConversionFailures, ParsedCommand}; +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 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#" root ::= CommandExecution @@ -51,6 +120,9 @@ The following events can be generated: - `change_scene`: The player's current scene is changed. - `appliesTo` must be set to `player`. - `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. - `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. @@ -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. - `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} **Player Command**: - - Action: `{}` - - Target: `{}` - - Location: `{}` - - Using: `{}` + - Action: `{ACTION}` + - Target: `{TARGET}` + - Location: `{LOCATION}` + - Using: `{USING}` [/INST] "#; @@ -110,6 +188,12 @@ const SCENE_EXIT_INFO: &'static str = r#" - 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 { let valid_events = CommandEvent::VARIANTS .iter() @@ -123,10 +207,9 @@ fn unrecognized_event_solution(event_name: &str) -> String { } fn stage_info(stage: &Stage) -> String { - let scene_description = "**Scene Description:** ".to_string() + &stage.scene.description; - - let mut info = "**Scene Information:**\n".to_string(); + let mut info = "# SCENE INFORMATION\n\n".to_string(); + info.push_str("## CURRENT SCENE INFORMATION\n\n"); info.push_str(" - Key: "); info.push_str(&format!("`{}`", stage.key)); info.push_str("\n"); @@ -137,72 +220,38 @@ fn stage_info(stage: &Stage) -> String { info.push_str(" - Location: "); info.push_str(&stage.scene.region); - info.push_str("\n"); - - let people: String = stage - .people - .iter() - .map(|p| format!(" - {}", p.name)) - .collect::>() - .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::>() - .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::>() - .join("\n"); - - info.push_str("**Props:**\n"); - info.push_str(&props); info.push_str("\n\n"); - let exits: String = stage - .scene - .exits - .iter() - .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::>() - .join("\n\n"); + let people = stage.people.iter().map_into::(); + let items = stage.items.iter().map_into::(); + let props = stage.scene.props.iter().map_into::(); + let entities = people.chain(items).chain(props); - 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::()); + exits.with(Style::markdown()); + info.push_str("## EXITS\n\n"); + info.push_str(&exits.to_string()); 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 prompt = COMMAND_EXECUTION_PROMPT .replacen("{SCENE_INFO}", &scene_info, 1) - .replacen("{}", &cmd.verb, 1) - .replacen("{}", &cmd.target, 1) - .replacen("{}", &cmd.location, 1) - .replacen("{}", &cmd.using, 1); + .replacen("{ORIGINAL_COMMAND}", &original_cmd, 1) + .replacen("{ACTION}", &cmd.verb, 1) + .replacen("{TARGET}", &cmd.target, 1) + .replacen("{LOCATION}", &cmd.location, 1) + .replacen("{USING}", &cmd.using, 1); AiPrompt::new_with_grammar_and_size(&prompt, COMMAND_EXECUTION_BNF, 512) } diff --git a/src/commands/builtins.rs b/src/commands/builtins.rs index 3562ee0..0b10dfa 100644 --- a/src/commands/builtins.rs +++ b/src/commands/builtins.rs @@ -9,5 +9,5 @@ pub fn check_builtin_command(stage: &Stage, cmd: &str) -> Option } fn look_command(_stage: &Stage) -> Option { - Some(BuiltinCommand::Look) + Some(BuiltinCommand::LookAtScene) } diff --git a/src/commands/coherence.rs b/src/commands/coherence.rs new file mode 100644 index 0000000..7a92e61 --- /dev/null +++ b/src/commands/coherence.rs @@ -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; +type OptionalCoherenceResult = Result, EventCoherenceFailure>; + +pub struct CommandCoherence<'a> { + logic: Rc, + db: Rc, + stage: &'a Stage, +} + +impl CommandCoherence<'_> { + pub fn new<'a>( + logic: &Rc, + db: &Rc, + stage: &'a Stage, + ) -> CommandCoherence<'a> { + CommandCoherence { + logic: logic.clone(), + db: db.clone(), + stage, + } + } + + pub async fn fix_incoherent_events( + &self, + failures: Vec, + ) -> 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 { + Uuid::parse_str(uuid_str.replace("-", "").as_ref()) + .ok() + .map(|parsed| parsed.as_hyphenated().to_string()) +} diff --git a/src/commands/converter.rs b/src/commands/converter.rs index 815fb46..0aba2a5 100644 --- a/src/commands/converter.rs +++ b/src/commands/converter.rs @@ -1,7 +1,7 @@ use crate::{ db::Database, models::commands::{ - CommandEvent, AiCommand, EventCoherenceFailure, EventConversionError, + AiCommand, CommandEvent, EventCoherenceFailure, EventConversionError, EventConversionFailures, ExecutionConversionResult, RawCommandEvent, RawCommandExecution, }, }; @@ -9,6 +9,7 @@ use anyhow::Result; use futures::stream::{self, StreamExt, TryStreamExt}; use itertools::{Either, Itertools}; use std::convert::TryFrom; +use super::coherence::strip_prefixes; use strum::VariantNames; @@ -50,10 +51,10 @@ fn from_raw_success(raw: Narrative, events: Vec) -> AiCommand { Some(reason) if !raw.valid && reason.is_empty() => { Some("invalid for unknown reason".to_string()) } - Some(_) if !raw.valid => raw.reason.clone(), + Some(_) if !raw.valid => raw.reason, _ => None, }, - narration: raw.narration.clone(), + narration: raw.narration, } } @@ -118,36 +119,35 @@ fn deserialize_recognized_event( let event_name = event_name.as_str(); 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 "change_scene" => Ok(CommandEvent::ChangeScene { - scene_key: raw_event - .parameter - .strip_prefix("scenes/") // Mini coherence check - .map(String::from) - .unwrap_or(raw_event.parameter) - .clone(), + scene_key: strip_prefixes(raw_event.parameter), }), // bodily position-related "stand" => Ok(CommandEvent::Stand { - target: raw_event.applies_to, + target: strip_prefixes(raw_event.applies_to), }), "sit" => Ok(CommandEvent::Sit { - target: raw_event.applies_to, + target: strip_prefixes(raw_event.applies_to), }), "prone" => Ok(CommandEvent::Prone { - target: raw_event.applies_to, + target: strip_prefixes(raw_event.applies_to), }), "crouch" => Ok(CommandEvent::Crouch { - target: raw_event.applies_to, + target: strip_prefixes(raw_event.applies_to), }), // combat-related "take_damage" => deserialize_take_damage(raw_event), - // miscellaneous - "narration" => Ok(CommandEvent::Narration(raw_event.parameter)), - // unrecognized _ => Err(EventConversionError::UnrecognizedEvent(raw_event)), } @@ -158,18 +158,28 @@ fn deserialize_take_damage( ) -> Result { match raw_event.parameter.parse::() { Ok(dmg) => Ok(CommandEvent::TakeDamage { - target: raw_event.applies_to, + target: strip_prefixes(raw_event.applies_to), amount: dmg, }), Err(_) => Err(EventConversionError::InvalidParameter(raw_event)), } } -async fn validate_event_coherence<'a>( +pub(super) async fn validate_event_coherence<'a>( db: &Database, event: CommandEvent, ) -> std::result::Result { 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 { Ok(exists) => match exists { true => Ok(event), @@ -185,6 +195,7 @@ async fn validate_event_coherence<'a>( /// information contained in the response is not valid. fn invalid_converted_event(event: CommandEvent) -> Option { match event { + CommandEvent::LookAtEntity { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)), CommandEvent::ChangeScene { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)), _ => None, } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 1443304..063d102 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,16 +3,17 @@ use crate::{ db::Database, models::{ commands::{ - AiCommand, BuiltinCommand, CommandExecution, ExecutionConversionResult, ParsedCommand, - ParsedCommands, RawCommandExecution, + AiCommand, CommandEvent, CommandExecution, EventCoherenceFailure, + ExecutionConversionResult, ParsedCommand, ParsedCommands, RawCommandExecution, }, world::scenes::Stage, }, }; -use anyhow::{anyhow, Result}; +use anyhow::Result; use std::rc::Rc; pub mod builtins; +pub mod coherence; pub mod converter; fn directional_command(direction: &str) -> ParsedCommand { @@ -45,10 +46,7 @@ fn translate(cmd: &str) -> Option { _ => None, }; - cmd.map(|c| ParsedCommands { - commands: vec![c], - count: 1, - }) + cmd.map(|c| ParsedCommands::single(&format!("go {}", c.target), c)) } pub struct CommandExecutor { @@ -79,8 +77,6 @@ impl CommandExecutor { } pub async fn execute(&self, stage: &Stage, cmd: &str) -> Result { - CommandExecution::AiCommand(AiCommand::empty()); - if let Some(builtin) = builtins::check_builtin_command(stage, cmd) { return Ok(CommandExecution::Builtin(builtin)); } @@ -91,25 +87,63 @@ impl CommandExecutor { } else { let (cmds_to_cache, execution) = self.logic.execute(stage, cmd).await?; - self.db - .cache_command(cmd, &stage.scene, &cmds_to_cache) - .await?; + if execution.valid && cmds_to_cache.commands.len() > 0 { + self.db + .cache_command(cmd, &stage.scene, &cmds_to_cache) + .await?; + } execution }; let converted = converter::convert_raw_execution(raw_exec, &self.db).await; - //TODO handle the errored events aside from getting rid of them - let execution: AiCommand = match converted { + let execution: Result = match converted { ExecutionConversionResult::Success(execution) => Ok(execution), - ExecutionConversionResult::PartialSuccess(execution, _) => Ok(execution), - ExecutionConversionResult::Failure(failures) => Err(anyhow!( - "unhandled command execution failure: {:?}", - failures - )), - }?; + ExecutionConversionResult::PartialSuccess(mut execution, failures) => { + // TODO also deal with conversion failures + // TODO deal with failures to fix incoherent events. + // 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)) } + + async fn fix_incoherence( + &self, + stage: &Stage, + failures: Vec, + ) -> Result> { + 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) + } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 5cc9bb7..a260db4 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,6 +1,6 @@ use crate::models::commands::{CachedParsedCommand, ParsedCommand, ParsedCommands}; 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 arangors::document::options::InsertOptions; use arangors::graph::{EdgeDefinition, Graph}; @@ -384,6 +384,34 @@ impl Database { Ok(stage_count > 0) } + pub async fn entity_exists(&self, scene_key: &str, entity_key: &str) -> Result { + 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::(queries::LOAD_ENTITY, vars) + .await? + .len(); + + Ok(entity_count > 0) + } + + pub async fn load_entity(&self, scene_key: &str, entity_key: &str) -> Result> { + 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( &self, raw_cmd: &str, diff --git a/src/db/queries.rs b/src/db/queries.rs index 85b2385..9138386 100644 --- a/src/db/queries.rs +++ b/src/db/queries.rs @@ -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#" UPSERT { _key: @scene_key } INSERT diff --git a/src/game_loop.rs b/src/game_loop.rs index 6670709..5577e00 100644 --- a/src/game_loop.rs +++ b/src/game_loop.rs @@ -1,5 +1,7 @@ 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::{commands::CommandExecutor, db::Database}; use anyhow::Result; @@ -51,26 +53,29 @@ impl GameLoop { // TODO this will probably eventually be moved to its own file. async fn handle_builtin(&mut self, builtin: BuiltinCommand) -> Result<()> { match builtin { - BuiltinCommand::Look => display!("{}", self.state.current_scene), + BuiltinCommand::LookAtScene => display!("{}", self.state.current_scene), }; Ok(()) } - async fn handle_execution(&mut self, execution: CommandExecution) -> Result<()> { - match execution { - CommandExecution::Builtin(builtin) => self.handle_builtin(builtin).await?, - CommandExecution::AiCommand(exec) => self.handle_ai_command(exec).await?, - }; + async fn handle_execution(&mut self, execution: Result) -> Result<()> { + if let Ok(execution) = execution { + match execution { + CommandExecution::Builtin(builtin) => self.handle_builtin(builtin).await?, + CommandExecution::AiCommand(exec) => self.handle_ai_command(exec).await?, + }; + } else { + display!("{}", execution.unwrap_err()); + } Ok(()) } async fn handle_input(&mut self, cmd: &str) -> Result<()> { if !cmd.is_empty() { - //let execution = self.execute_command(cmd).await?; 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?; } diff --git a/src/main.rs b/src/main.rs index e8925ba..19ab6b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use game_loop::GameLoop; use ai::logic::AiLogic; use models::world::scenes::{root_scene_id, Stage}; use state::GameState; -use std::{io::stdout, rc::Rc, time::Duration}; +use std::{io::stdout, rc::Rc, time::Duration, str::FromStr}; use arangors::Connection; diff --git a/src/models/commands.rs b/src/models/commands.rs index 824cec5..b346245 100644 --- a/src/models/commands.rs +++ b/src/models/commands.rs @@ -1,9 +1,9 @@ +use std::fmt::Display; + use serde::{Deserialize, Serialize}; use strum::{EnumString, EnumVariantNames}; use thiserror::Error; -use super::world::scenes::Stage; - /// Stored in the database to bypass AI 'parsing' when possible. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct CachedParsedCommand { @@ -14,10 +14,22 @@ pub struct CachedParsedCommand { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ParsedCommands { + #[serde(default)] + pub original: String, // The original text entered by the player, set by code. pub commands: Vec, 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)] pub struct ParsedCommand { pub verb: String, @@ -74,14 +86,23 @@ pub struct RawCommandEvent { #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum CommandEvent { + // Informational + Narration(String), + LookAtEntity { + entity_key: String, + scene_key: String, + }, + + // Movement-related ChangeScene { scene_key: String, }, + + // Player character state TakeDamage { target: String, amount: u32, }, - Narration(String), Stand { target: String, }, @@ -105,10 +126,12 @@ pub enum CommandEvent { /// builtin command is only created directly via checking for builtin /// commands. These commands may have little or no parameters, as they /// are meant for simple, direct commands like looking, movement, etc. +#[derive(Debug)] pub enum BuiltinCommand { - Look, + LookAtScene, } +#[derive(Debug)] pub enum CommandExecution { Builtin(BuiltinCommand), AiCommand(AiCommand), @@ -143,6 +166,15 @@ impl AiCommand { events: vec![], } } + + pub fn from_events(events: Vec) -> AiCommand { + AiCommand { + valid: true, + reason: None, + narration: "".to_string(), + events, + } + } } #[derive(Clone, Debug)] @@ -152,7 +184,13 @@ pub enum ExecutionConversionResult { 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 conversion_failures: Vec, pub coherence_failures: Vec, @@ -170,6 +208,21 @@ impl EventConversionFailures { } } +impl From> for EventConversionFailures { + fn from(value: Vec) -> 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)] pub enum EventConversionError { #[error("invalid parameter for {0:?}")] @@ -187,3 +240,19 @@ pub enum EventCoherenceFailure { #[error("uncategorized coherence failure: {1}")] 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 for CommandEvent { + fn from(value: EventCoherenceFailure) -> Self { + value.as_event() + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 73c5581..bfcd7ee 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,6 +1,7 @@ use self::world::items::Item; use self::world::people::Person; use self::world::scenes::{Scene, SceneStub}; +use serde::{Deserialize, Serialize}; use uuid::Uuid; // Has to come before any module declarations! @@ -38,9 +39,9 @@ macro_rules! impl_insertable { }; } +pub mod coherence; pub mod commands; pub mod world; -pub mod coherence; pub fn new_uuid_string() -> String { let uuid = Uuid::now_v7(); @@ -191,3 +192,56 @@ impl Insertable for Content { 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 { + match self { + Entity::Person(person) => person.set_id(id), + Entity::Item(item) => item.set_id(id), + } + } + + fn set_key(&mut self, key: String) -> Option { + match self { + Entity::Person(person) => person.set_key(key), + Entity::Item(item) => item.set_key(key), + } + } + + fn take_id(&mut self) -> Option { + match self { + Entity::Person(person) => person.take_id(), + Entity::Item(item) => item.take_id(), + } + } + + fn take_key(&mut self) -> Option { + match self { + Entity::Person(person) => person.take_key(), + Entity::Item(item) => item.take_key(), + } + } +} diff --git a/src/models/world/items.rs b/src/models/world/items.rs index 962da6e..4bc5070 100644 --- a/src/models/world/items.rs +++ b/src/models/world/items.rs @@ -1,6 +1,7 @@ use crate::models::new_uuid_string; use serde::{Deserialize, Serialize}; -use strum::{EnumString, EnumVariantNames, Display}; +use strum::{Display, EnumString, EnumVariantNames}; +use tabled::Tabled; use super::super::Insertable; diff --git a/src/models/world/people.rs b/src/models/world/people.rs index eac3787..f44aaca 100644 --- a/src/models/world/people.rs +++ b/src/models/world/people.rs @@ -1,4 +1,5 @@ use crate::models::new_uuid_string; +use tabled::Tabled; use super::super::Insertable; use serde::{Deserialize, Serialize}; diff --git a/src/models/world/scenes.rs b/src/models/world/scenes.rs index b443967..2c04eaf 100644 --- a/src/models/world/scenes.rs +++ b/src/models/world/scenes.rs @@ -1,8 +1,9 @@ +use crate::models::world::items::Item; use crate::models::world::people::Person; use crate::models::{new_uuid_string, Insertable}; -use crate::{db::Key, models::world::items::Item}; use serde::{Deserialize, Serialize}; use std::sync::OnceLock; +use tabled::Tabled; use super::raw::{ExitSeed, PropSeed}; diff --git a/src/state.rs b/src/state.rs index 3114d0b..747c521 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,4 +1,5 @@ -use crate::models::Insertable; +use crate::io::display; +use crate::models::{Entity, Insertable}; use crate::{ ai::logic::AiLogic, db::Database, @@ -24,6 +25,7 @@ impl GameState { match event { CommandEvent::ChangeScene { scene_key } => self.change_scene(&scene_key).await?, 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(()) } + + 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(()) + } }