ai-game/game/src/models/commands.rs

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