Validate, Execute, Report
The standard library in Sharpee has easily received the most attention with change. We've mowed through at least a dozen refactors because my design thoughts keep finding improvement opportunities.
The latest changes are about how events are emitted (not what - that was covered in the last blog post). This also led to a discussion of the data we need within each action and decided to make this configurable. There is required data elements for each action, but the author can inject extra data to be used in event reporting.
We also refactored the engine's command-executor so that it's just orchestrating the action functions and nothing else. We dropped it from over 700 lines to around 150.
Now we have validate and execute returning events so report has all of the info for the completed action and is responsible for all event output.
/**
* Examining action - looks at objects in detail
*
* This is a read-only action that provides detailed information about objects.
* It validates visibility but doesn't change state.
*
* Uses three-phase pattern:
* 1. validate: Check target exists and is visible
* 2. execute: No mutations (read-only action)
* 3. report: Generate events with complete entity snapshot
*/
The data configuration is built into each action.
/**
* Build examining action success data
*
* Creates the complete data structure for examined events,
* including entity snapshots and trait-specific information.
*/
export const buildExaminingData: ActionDataBuilder<Record<string, unknown>> = (
context: ActionContext,
preState?: WorldModel,
postState?: WorldModel
): Record<string, unknown> => {
const actor = context.player;
const noun = context.command.directObject?.entity;
if (!noun) {
// Shouldn't happen if validation passed, but handle gracefully
return {
targetId: '',
targetName: 'nothing'
};
}
const isSelf = noun.id === actor.id;
// Capture complete entity snapshot for atomic event
const entitySnapshot = captureEntitySnapshot(noun, context.world, true);
// Build base event data
const eventData: Record<string, unknown> = {
// New atomic structure
target: entitySnapshot,
// Backward compatibility fields
targetId: noun.id,
targetName: isSelf ? 'yourself' : noun.name
};
if (isSelf) {
eventData.self = true;
return eventData; // No trait checking for self-examination
}
// Add trait-specific information
// Identity trait (description/brief)
if (noun.has(TraitType.IDENTITY)) {
const identityTrait = noun.get(TraitType.IDENTITY);
if (identityTrait) {
eventData.hasDescription = !!(identityTrait as any).description;
eventData.hasBrief = !!(identityTrait as any).brief;
}
}
// Container trait
if (noun.has(TraitType.CONTAINER)) {
const contents = context.world.getContents(noun.id);
const contentsSnapshots = captureEntitySnapshots(contents, context.world);
eventData.isContainer = true;
eventData.hasContents = contents.length > 0;
eventData.contentCount = contents.length;
// New: full snapshots
eventData.contentsSnapshots = contentsSnapshots;
// Backward compatibility: simple references
eventData.contents = contents.map(e => ({ id: e.id, name: e.name }));
// Check if open/closed
if (noun.has(TraitType.OPENABLE)) {
eventData.isOpenable = true;
eventData.isOpen = OpenableBehavior.isOpen(noun);
} else {
eventData.isOpen = true; // Containers without openable trait are always open
}
}
// Supporter trait
if (noun.has(TraitType.SUPPORTER)) {
const contents = context.world.getContents(noun.id);
const contentsSnapshots = captureEntitySnapshots(contents, context.world);
eventData.isSupporter = true;
eventData.hasContents = contents.length > 0;
eventData.contentCount = contents.length;
// New: full snapshots
eventData.contentsSnapshots = contentsSnapshots;
// Backward compatibility: simple references
eventData.contents = contents.map(e => ({ id: e.id, name: e.name }));
}
// Switchable trait
if (noun.has(TraitType.SWITCHABLE)) {
eventData.isSwitchable = true;
eventData.isOn = SwitchableBehavior.isOn(noun);
}
// Readable trait
if (noun.has(TraitType.READABLE)) {
const readableTrait = noun.get(TraitType.READABLE);
eventData.isReadable = true;
eventData.hasText = readableTrait ? !!(readableTrait as any).text : false;
}
// Wearable trait
if (noun.has(TraitType.WEARABLE)) {
eventData.isWearable = true;
eventData.isWorn = WearableBehavior.isWorn(noun);
}
// Door trait
if (noun.has(TraitType.DOOR)) {
eventData.isDoor = true;
// Check if door is openable
if (noun.has(TraitType.OPENABLE)) {
eventData.isOpenable = true;
eventData.isOpen = OpenableBehavior.isOpen(noun);
}
// Add lock status
if (noun.has(TraitType.LOCKABLE)) {
eventData.isLockable = true;
eventData.isLocked = LockableBehavior.isLocked(noun);
}
}
return eventData;
};
This refactor was merged last night and I'm going through unit tests to make sure we get back to a functioning Cloak of Darkness.