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:
parent
ce89b61c19
commit
1d78d05d7a
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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?;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::models::new_uuid_string;
|
||||
use tabled::Tabled;
|
||||
|
||||
use super::super::Insertable;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
22
src/state.rs
22
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(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue