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",
"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"

View File

@ -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"

View File

@ -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<AiGenerator>,
}

View File

@ -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<RawCommandExecution> {
let prompt = execution_prompts::execution_prompt(stage, &cmd);
pub async fn execute_raw(
&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?;
Ok(raw_exec)
}

View File

@ -54,20 +54,7 @@ impl AiLogic {
stage: &Stage,
parsed_cmd: &ParsedCommands,
) -> Result<RawCommandExecution> {
//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)
}

View File

@ -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::<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");
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::<Vec<_>>()
.join("\n\n");
let people = stage.people.iter().map_into::<EntityTableRow>();
let items = stage.items.iter().map_into::<EntityTableRow>();
let props = stage.scene.props.iter().map_into::<EntityTableRow>();
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::<ExitTableRow>());
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)
}

View File

@ -9,5 +9,5 @@ pub fn check_builtin_command(stage: &Stage, cmd: &str) -> 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::{
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<CommandEvent>) -> 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<CommandEvent, EventConversionError> {
match raw_event.parameter.parse::<u32>() {
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<CommandEvent, EventCoherenceFailure> {
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<EventCoherenceFailure> {
match event {
CommandEvent::LookAtEntity { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)),
CommandEvent::ChangeScene { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)),
_ => None,
}

View File

@ -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<ParsedCommands> {
_ => 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> {
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<AiCommand> = 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<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::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<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(
&self,
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#"
UPSERT { _key: @scene_key }
INSERT <SCENE_JSON>

View File

@ -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<CommandExecution>) -> 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?;
}

View File

@ -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;

View File

@ -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<ParsedCommand>,
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<CommandEvent>) -> 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<EventConversionError>,
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)]
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<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::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<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 serde::{Deserialize, Serialize};
use strum::{EnumString, EnumVariantNames, Display};
use strum::{Display, EnumString, EnumVariantNames};
use tabled::Tabled;
use super::super::Insertable;

View File

@ -1,4 +1,5 @@
use crate::models::new_uuid_string;
use tabled::Tabled;
use super::super::Insertable;
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::{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};

View File

@ -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(())
}
}