260 lines
6.7 KiB
Rust
260 lines
6.7 KiB
Rust
use std::fmt::Display;
|
|
|
|
use gbnf::prelude::*;
|
|
use gbnf_derive::{Gbnf};
|
|
use serde::{Deserialize, Serialize};
|
|
use strum::{EnumString, EnumVariantNames};
|
|
use thiserror::Error;
|
|
|
|
/// Stored in the database to bypass AI 'parsing' when possible.
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct CachedParsedCommand {
|
|
pub raw: String,
|
|
pub scene_key: String,
|
|
pub commands: ParsedCommands,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Gbnf)]
|
|
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, Gbnf)]
|
|
pub struct ParsedCommand {
|
|
pub verb: String,
|
|
pub target: String,
|
|
pub location: String,
|
|
pub using: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone, Gbnf)]
|
|
pub struct VerbsResponse {
|
|
pub verbs: Vec<String>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
pub struct VerbsAndTargets {
|
|
pub entries: Vec<VerbAndTargetEntry>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
pub struct VerbAndTargetEntry {
|
|
pub verb: String,
|
|
pub target: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Gbnf)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RawCommandExecution {
|
|
pub valid: bool,
|
|
pub reason: Option<String>,
|
|
pub narration: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[gbnf_limit]
|
|
pub event: Option<RawCommandEvent>,
|
|
}
|
|
|
|
impl RawCommandExecution {
|
|
pub fn empty() -> RawCommandExecution {
|
|
RawCommandExecution {
|
|
valid: true,
|
|
reason: None,
|
|
narration: "".to_string(),
|
|
event: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Gbnf)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RawCommandEvent {
|
|
pub event_name: String,
|
|
#[gbnf_limit]
|
|
pub applies_to: String,
|
|
#[gbnf_limit]
|
|
pub parameter: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, EnumString, EnumVariantNames)]
|
|
#[strum(serialize_all = "snake_case")]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum CommandEvent {
|
|
// Informational
|
|
Narration(String),
|
|
LookAtEntity(String),
|
|
|
|
// Movement-related
|
|
ChangeScene {
|
|
scene_key: String,
|
|
},
|
|
|
|
// Player character state
|
|
TakeDamage {
|
|
target: String,
|
|
amount: u32,
|
|
},
|
|
Stand {
|
|
target: String,
|
|
},
|
|
Sit {
|
|
target: String,
|
|
},
|
|
Prone {
|
|
target: String,
|
|
},
|
|
Crouch {
|
|
target: String,
|
|
},
|
|
Unrecognized {
|
|
event_name: String,
|
|
narration: String,
|
|
},
|
|
}
|
|
|
|
/// A builtin command has more immediate access to necessary
|
|
/// information, so we can be a bit more loose with what we give it. A
|
|
/// 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 {
|
|
LookAtScene,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum CommandExecution {
|
|
Builtin(BuiltinCommand),
|
|
AiCommand(AiCommand),
|
|
}
|
|
|
|
/// Simple struct to hold the narrative parts of the
|
|
/// RawCommandExecution to minimize clones.
|
|
pub struct Narrative {
|
|
pub valid: bool,
|
|
pub reason: Option<String>,
|
|
pub narration: String,
|
|
}
|
|
|
|
/// An "AI Command" is a command execution generated by the LLM and
|
|
/// run through coherence validation/fixing, and (assuming it is
|
|
/// valid) contains an event to apply to the game state.
|
|
//TODO rename to AiCommandExecution
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct AiCommand {
|
|
pub valid: bool,
|
|
pub reason: Option<String>,
|
|
pub narration: String,
|
|
pub event: Option<CommandEvent>,
|
|
}
|
|
|
|
impl AiCommand {
|
|
fn from_narrative_and_event(narrative: Narrative, event: CommandEvent) -> AiCommand {
|
|
AiCommand {
|
|
event: Some(event),
|
|
valid: narrative.valid,
|
|
reason: match &narrative.reason {
|
|
Some(reason) if !narrative.valid && reason.is_empty() => {
|
|
Some("invalid for unknown reason".to_string())
|
|
}
|
|
Some(_) if !narrative.valid => narrative.reason,
|
|
_ => None,
|
|
},
|
|
narration: narrative.narration,
|
|
}
|
|
}
|
|
|
|
pub fn empty() -> AiCommand {
|
|
AiCommand {
|
|
valid: true,
|
|
reason: None,
|
|
narration: "".to_string(),
|
|
event: None,
|
|
}
|
|
}
|
|
|
|
pub fn from_raw_invalid(raw: RawCommandExecution) -> AiCommand {
|
|
AiCommand {
|
|
valid: raw.valid,
|
|
reason: raw.reason,
|
|
narration: "".to_string(),
|
|
event: None,
|
|
}
|
|
}
|
|
|
|
pub fn from_raw_success(narrative: Narrative, event: CommandEvent) -> AiCommand {
|
|
Self::from_narrative_and_event(narrative, event)
|
|
}
|
|
|
|
pub fn from_event(event: CommandEvent) -> AiCommand {
|
|
AiCommand {
|
|
valid: true,
|
|
reason: None,
|
|
narration: "".to_string(),
|
|
event: Some(event),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type ExecutionConversionResult = std::result::Result<AiCommand, EventConversionFailure>;
|
|
|
|
#[derive(Error, Clone, Debug)]
|
|
pub enum EventConversionFailure {
|
|
#[error("parsing failure: {0:?}")]
|
|
ParsingFailure(EventParsingFailure),
|
|
#[error("coherence failure: {0:?}")]
|
|
CoherenceFailure(EventCoherenceFailure),
|
|
}
|
|
|
|
impl From<EventCoherenceFailure> for EventConversionFailure {
|
|
fn from(value: EventCoherenceFailure) -> Self {
|
|
EventConversionFailure::CoherenceFailure(value)
|
|
}
|
|
}
|
|
|
|
impl From<EventParsingFailure> for EventConversionFailure {
|
|
fn from(value: EventParsingFailure) -> Self {
|
|
EventConversionFailure::ParsingFailure(value)
|
|
}
|
|
}
|
|
|
|
#[derive(Error, Clone, Debug)]
|
|
pub enum EventParsingFailure {
|
|
#[error("invalid parameter for {0:?}")]
|
|
InvalidParameter(RawCommandEvent),
|
|
|
|
#[error("unrecognized event - {0:?}")]
|
|
UnrecognizedEvent(RawCommandEvent),
|
|
}
|
|
|
|
#[derive(Error, Clone, Debug)]
|
|
pub enum EventCoherenceFailure {
|
|
#[error("target of command does not exist")]
|
|
TargetDoesNotExist(AiCommand),
|
|
|
|
#[error("uncategorized coherence failure: {1}")]
|
|
OtherError(AiCommand, String),
|
|
}
|
|
|
|
impl EventCoherenceFailure {
|
|
/// Consume self to extract the CommandEvent wrapped in this enum.
|
|
pub fn as_event(self) -> Option<CommandEvent> {
|
|
match self {
|
|
EventCoherenceFailure::OtherError(cmd, _) => cmd.event,
|
|
Self::TargetDoesNotExist(cmd) => cmd.event,
|
|
}
|
|
}
|
|
}
|