zork-ts 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,7 +55,7 @@ export class CyclopsBehavior extends BaseActorBehavior {
55
55
  this.wrathLevel = this.wrathLevel + 1;
56
56
  }
57
57
  const messageIndex = Math.min(absWrath - 1, CYCLOPS_MAD_MESSAGES.length - 1);
58
- if (messageIndex >= 0 && this.state !== ActorState.SLEEPING) {
58
+ if (messageIndex >= 0) {
59
59
  console.log(CYCLOPS_MAD_MESSAGES[messageIndex]);
60
60
  return true;
61
61
  }
@@ -395,7 +395,7 @@ export class CommandExecutor {
395
395
  }
396
396
  // SAY command needs raw input
397
397
  if (verb === 'SAY') {
398
- return handler.execute(state, command.directObject?.id, command.indirectObject?.id, command.preposition, parsedCommand.rawInput);
398
+ return handler.execute(state, command.directObject?.id, command.indirectObject?.id, command.preposition, command.rawInput);
399
399
  }
400
400
  // Intransitive verbs (no object required)
401
401
  if (['XYZZY', 'PLUGH', 'PLOVER', 'JUMP', 'LEAP', 'PRAY', 'CURSE', 'SING', 'LISTEN', 'SMELL', 'SNIFF', 'WAIT', 'Z', 'CLIMB', 'ECHO', 'DANCE', 'SWIM', 'DIG', 'SLEEP', 'WAKE', 'YELL', 'SCREAM', 'HELLO', 'HI', 'GOODBYE', 'THANK', 'YES', 'Y', 'NO'].includes(verb)) {
@@ -96,7 +96,8 @@ export class ThiefBehavior extends BaseActorBehavior {
96
96
  const room = state.getCurrentRoom();
97
97
  if (!room)
98
98
  return false;
99
- return room.flags.has(ObjectFlag.ONBIT);
99
+ // Check if room has ONBIT flag (rooms use string flags)
100
+ return room.flags.has('ONBIT');
100
101
  }
101
102
  /**
102
103
  * Check if troll is present in current room
@@ -1106,8 +1106,8 @@ export class ReadAction {
1106
1106
  stateChanges: takenMessage ? [{
1107
1107
  type: 'OBJECT_MOVED',
1108
1108
  objectId: objectId,
1109
- oldLocation: obj.location,
1110
- newLocation: 'PLAYER'
1109
+ oldValue: obj.location,
1110
+ newValue: 'PLAYER'
1111
1111
  }] : []
1112
1112
  };
1113
1113
  }
@@ -2215,7 +2215,7 @@ export class ClimbAction {
2215
2215
  if (upExit && upExit.destination) {
2216
2216
  // Track if room was visited before
2217
2217
  const newRoom = state.rooms.get(upExit.destination);
2218
- const wasVisited = newRoom?.hasFlag('TOUCHBIT') || false;
2218
+ const wasVisited = newRoom?.hasFlag(RoomFlag.TOUCHBIT) || false;
2219
2219
  // Move up
2220
2220
  state.setCurrentRoom(upExit.destination);
2221
2221
  // Get the new room and display its description
@@ -3729,7 +3729,11 @@ export class DropAllAction {
3729
3729
  // Use ObjectInteractionManager for context-aware empty-handed message
3730
3730
  const objectManager = new ZMachineObjectInteraction();
3731
3731
  const result = objectManager.handleDropAllCommand(state);
3732
- return result;
3732
+ return {
3733
+ success: result.success,
3734
+ message: result.message,
3735
+ stateChanges: []
3736
+ };
3733
3737
  }
3734
3738
  const messages = [];
3735
3739
  const stateChanges = [];
@@ -63,6 +63,7 @@ export declare enum VehicleType {
63
63
  */
64
64
  export interface GlobalFlags {
65
65
  CYCLOPS_FLAG: boolean;
66
+ DAM_LIGHTS: boolean;
66
67
  DEFLATE: boolean;
67
68
  DOME_FLAG: boolean;
68
69
  EMPTY_HANDED: boolean;
@@ -131,6 +131,7 @@ export const FLAG_DESCRIPTIONS = {
131
131
  NONLANDBIT: 'Room is not on land (water/boat required)',
132
132
  // Global Flags
133
133
  CYCLOPS_FLAG: 'Cyclops has been dealt with',
134
+ DAM_LIGHTS: 'Dam control panel lights are on',
134
135
  DEFLATE: 'Boat has been deflated',
135
136
  DOME_FLAG: 'Dome rope has been tied',
136
137
  EMPTY_HANDED: 'Player is carrying nothing',
@@ -147,6 +148,7 @@ export const FLAG_DESCRIPTIONS = {
147
148
  */
148
149
  export const INITIAL_GLOBAL_FLAGS = {
149
150
  CYCLOPS_FLAG: false,
151
+ DAM_LIGHTS: false,
150
152
  DEFLATE: false,
151
153
  DOME_FLAG: false,
152
154
  EMPTY_HANDED: false,
@@ -195,18 +195,6 @@ export const ALL_ROOMS = {
195
195
  flags: ['RLANDBIT', 'ONBIT', 'SACREDBIT'],
196
196
  globalObjects: ['TREE', 'SONGBIRD', 'WHITE-HOUSE', 'FOREST']
197
197
  },
198
- 'CANYON-VIEW': {
199
- id: 'CANYON-VIEW',
200
- name: 'Canyon View',
201
- description: 'Canyon View',
202
- longDescription: 'You are at the top of the Great Canyon on its west wall. From here there is a marvelous view of the canyon and parts of the Frigid River upstream. Across the canyon, the walls of the White Cliffs join the mighty ramparts of the Flathead Mountains to the east. Following the Canyon upstream to the north, Aragain Falls may be seen, complete with rainbow. The mighty Frigid River flows out from a great dark cavern. To the west and south can be seen an immense forest, stretching for miles around. A path leads northwest. It is possible to climb down into the canyon from here.',
203
- exits: [
204
- { direction: 'WEST', destination: 'CLEARING' },
205
- { direction: 'NW', destination: 'GRATING-CLEARING' },
206
- { direction: 'DOWN', destination: 'ROCKY-LEDGE' }
207
- ],
208
- flags: ['RLANDBIT', 'ONBIT', 'SACREDBIT']
209
- },
210
198
  'MOUNTAINS': {
211
199
  id: 'MOUNTAINS',
212
200
  name: 'Forest',
@@ -196,7 +196,7 @@ function randomizeObjects(state) {
196
196
  // Reset SWORD treasure value to 0
197
197
  const sword = state.getObject('SWORD');
198
198
  if (sword) {
199
- sword.tvalue = 0;
199
+ sword.value = 0;
200
200
  }
201
201
  // Get all objects in inventory
202
202
  const inventoryObjects = state.getInventoryObjects();
@@ -209,8 +209,8 @@ function randomizeObjects(state) {
209
209
  if (obj.id === 'LAMP' || obj.id === 'COFFIN') {
210
210
  continue;
211
211
  }
212
- // Treasures (tvalue > 0) go to random RLANDBIT rooms
213
- if (obj.tvalue && obj.tvalue > 0) {
212
+ // Treasures (value > 0) go to random RLANDBIT rooms
213
+ if (obj.value && obj.value > 0) {
214
214
  // Pick a random RLANDBIT room using 50% probability per room
215
215
  if (rlandRooms.length > 0) {
216
216
  let placed = false;
@@ -5,6 +5,7 @@
5
5
  import { createInitialGameState, validateRoomConnections, validateObjectLocations } from './gameFactory.js';
6
6
  import { ALL_ROOMS } from '../data/rooms-complete.js';
7
7
  import { ALL_OBJECTS } from '../data/objects-complete.js';
8
+ import { Direction } from '../rooms.js';
8
9
  /**
9
10
  * Verify content completeness
10
11
  */
@@ -187,10 +188,8 @@ export function verifyContent() {
187
188
  for (const [direction, exit] of room.exits.entries()) {
188
189
  if (exit.condition) {
189
190
  conditionalExitCount++;
190
- // Check if the condition flag exists in the game state
191
- if (!(exit.condition in state.flags)) {
192
- warnings.push(`Room ${roomId} exit ${direction} has condition ${exit.condition} which is not a known flag`);
193
- }
191
+ // Note: exit.condition is a function, not a flag name
192
+ // We can't easily validate the condition without executing it
194
193
  }
195
194
  }
196
195
  }
@@ -198,16 +197,16 @@ export function verifyContent() {
198
197
  // Check bidirectional connections
199
198
  console.log(`\n=== Checking Bidirectional Connections ===`);
200
199
  const oppositeDirections = {
201
- 'NORTH': 'SOUTH',
202
- 'SOUTH': 'NORTH',
203
- 'EAST': 'WEST',
204
- 'WEST': 'EAST',
205
- 'NORTHEAST': 'SOUTHWEST',
206
- 'SOUTHWEST': 'NORTHEAST',
207
- 'NORTHWEST': 'SOUTHEAST',
208
- 'SOUTHEAST': 'NORTHWEST',
209
- 'UP': 'DOWN',
210
- 'DOWN': 'UP',
200
+ [Direction.NORTH]: Direction.SOUTH,
201
+ [Direction.SOUTH]: Direction.NORTH,
202
+ [Direction.EAST]: Direction.WEST,
203
+ [Direction.WEST]: Direction.EAST,
204
+ [Direction.NE]: Direction.SW,
205
+ [Direction.SW]: Direction.NE,
206
+ [Direction.NW]: Direction.SE,
207
+ [Direction.SE]: Direction.NW,
208
+ [Direction.UP]: Direction.DOWN,
209
+ [Direction.DOWN]: Direction.UP,
211
210
  };
212
211
  let bidirectionalCount = 0;
213
212
  let unidirectionalCount = 0;
@@ -31,8 +31,15 @@ export interface GameObject {
31
31
  capacity?: number;
32
32
  size?: number;
33
33
  value?: number;
34
- hasFlag(flag: ObjectFlag): boolean;
34
+ hasFlag(flag: ObjectFlag | string): boolean;
35
+ addFlag(flag: ObjectFlag | string): void;
36
+ removeFlag(flag: ObjectFlag | string): void;
37
+ getProperty(key: string): any;
38
+ setProperty(key: string, value: any): void;
35
39
  isOpen(): boolean;
40
+ isContainer(): boolean;
41
+ isTakeable(): boolean;
42
+ providesLight(): boolean;
36
43
  }
37
44
  /**
38
45
  * GameObjectImpl provides a concrete implementation of GameObject
@@ -74,15 +81,15 @@ export declare class GameObjectImpl implements GameObject {
74
81
  /**
75
82
  * Check if object has a specific flag
76
83
  */
77
- hasFlag(flag: ObjectFlag): boolean;
84
+ hasFlag(flag: ObjectFlag | string): boolean;
78
85
  /**
79
86
  * Add a flag to the object
80
87
  */
81
- addFlag(flag: ObjectFlag): void;
88
+ addFlag(flag: ObjectFlag | string): void;
82
89
  /**
83
90
  * Remove a flag from the object
84
91
  */
85
- removeFlag(flag: ObjectFlag): void;
92
+ removeFlag(flag: ObjectFlag | string): void;
86
93
  /**
87
94
  * Toggle a flag on the object
88
95
  */
@@ -40,6 +40,7 @@ export interface Room {
40
40
  exits: Map<Direction, Exit>;
41
41
  objects: string[];
42
42
  globalObjects?: string[];
43
+ scenery?: string[];
43
44
  visited: boolean;
44
45
  flags: Set<RoomFlag>;
45
46
  hasFlag(flag: RoomFlag): boolean;
@@ -66,6 +67,7 @@ export declare class RoomImpl implements Room {
66
67
  exits: Map<Direction, Exit>;
67
68
  objects: string[];
68
69
  globalObjects?: string[];
70
+ scenery?: string[];
69
71
  visited: boolean;
70
72
  flags: Set<RoomFlag>;
71
73
  constructor(data: {
@@ -75,6 +77,7 @@ export declare class RoomImpl implements Room {
75
77
  exits?: Map<Direction, Exit>;
76
78
  objects?: string[];
77
79
  globalObjects?: string[];
80
+ scenery?: string[];
78
81
  visited?: boolean;
79
82
  flags?: RoomFlag[];
80
83
  });
@@ -32,6 +32,7 @@ export class RoomImpl {
32
32
  exits;
33
33
  objects;
34
34
  globalObjects;
35
+ scenery;
35
36
  visited;
36
37
  flags;
37
38
  constructor(data) {
@@ -41,6 +42,7 @@ export class RoomImpl {
41
42
  this.exits = data.exits || new Map();
42
43
  this.objects = data.objects || [];
43
44
  this.globalObjects = data.globalObjects;
45
+ this.scenery = data.scenery;
44
46
  this.visited = data.visited || false;
45
47
  this.flags = new Set(data.flags || []);
46
48
  }
@@ -474,8 +474,8 @@ const rainbowHandler = {
474
474
  if (state.currentRoom === 'CANYON-VIEW') {
475
475
  return 'From here?!?';
476
476
  }
477
- // Check if rainbow is solid (RAINBOW-FLAG)
478
- const rainbowSolid = state.getFlag('RAINBOW-FLAG');
477
+ // Check if rainbow is solid (RAINBOW_FLAG)
478
+ const rainbowSolid = state.getFlag('RAINBOW_FLAG');
479
479
  if (rainbowSolid) {
480
480
  // This would trigger movement logic
481
481
  return "You'll have to say which way...";
@@ -486,7 +486,7 @@ const rainbowHandler = {
486
486
  if (state.currentRoom === 'CANYON-VIEW') {
487
487
  return 'From here?!?';
488
488
  }
489
- const rainbowSolid = state.getFlag('RAINBOW-FLAG');
489
+ const rainbowSolid = state.getFlag('RAINBOW_FLAG');
490
490
  if (rainbowSolid) {
491
491
  return "You'll have to say which way...";
492
492
  }
@@ -2,7 +2,7 @@
2
2
  * Special Behavior Handlers
3
3
  * Handles complex state-dependent behaviors for special objects
4
4
  */
5
- import { ObjectFlag } from './data/flags.js';
5
+ import { ObjectFlag, RoomFlag } from './data/flags.js';
6
6
  import { initializeCandleTimer } from '../engine/daemons.js';
7
7
  /**
8
8
  * Registry of all special behaviors
@@ -700,7 +700,7 @@ const buttonBehavior = {
700
700
  }
701
701
  if (directObject === 'YELLOW-BUTTON' && yellowButton) {
702
702
  const room = state.getCurrentRoom();
703
- if (room?.hasFlag('ONBIT')) {
703
+ if (room?.hasFlag(RoomFlag.ONBIT)) {
704
704
  return 'The chests are already open.';
705
705
  }
706
706
  }
@@ -226,5 +226,20 @@ export declare class GameState {
226
226
  * @returns true if testing mode is enabled
227
227
  */
228
228
  isTestingMode(): boolean;
229
+ /**
230
+ * Get the current turn number (alias for moves)
231
+ * Used for compatibility with parity modules
232
+ */
233
+ get turn(): number;
234
+ /**
235
+ * Check if the current room is lit
236
+ * Wrapper for lighting system's isRoomLit function
237
+ */
238
+ hasLight(): boolean;
239
+ /**
240
+ * Check if player has an active light source
241
+ * Wrapper for checking inventory light sources
242
+ */
243
+ hasLightSource(): boolean;
229
244
  }
230
245
  //# sourceMappingURL=state.d.ts.map
@@ -407,5 +407,50 @@ export class GameState {
407
407
  isTestingMode() {
408
408
  return this.testingMode;
409
409
  }
410
+ /**
411
+ * Get the current turn number (alias for moves)
412
+ * Used for compatibility with parity modules
413
+ */
414
+ get turn() {
415
+ return this.moves;
416
+ }
417
+ /**
418
+ * Check if the current room is lit
419
+ * Wrapper for lighting system's isRoomLit function
420
+ */
421
+ hasLight() {
422
+ const room = this.getCurrentRoom();
423
+ if (!room)
424
+ return false;
425
+ // Check if room is inherently lit
426
+ if (room.hasFlag('ONBIT'))
427
+ return true;
428
+ // Check for light sources in inventory
429
+ for (const obj of this.getInventoryObjects()) {
430
+ if (obj.hasFlag('LIGHTBIT') && obj.hasFlag('ONBIT')) {
431
+ return true;
432
+ }
433
+ }
434
+ // Check for light sources in room
435
+ for (const objId of room.objects) {
436
+ const obj = this.getObject(objId);
437
+ if (obj && obj.hasFlag('LIGHTBIT') && obj.hasFlag('ONBIT')) {
438
+ return true;
439
+ }
440
+ }
441
+ return false;
442
+ }
443
+ /**
444
+ * Check if player has an active light source
445
+ * Wrapper for checking inventory light sources
446
+ */
447
+ hasLightSource() {
448
+ for (const obj of this.getInventoryObjects()) {
449
+ if (obj.hasFlag('LIGHTBIT') && obj.hasFlag('ONBIT')) {
450
+ return true;
451
+ }
452
+ }
453
+ return false;
454
+ }
410
455
  }
411
456
  //# sourceMappingURL=state.js.map
@@ -74,14 +74,20 @@ export class ZMachineObjectInteraction {
74
74
  return false;
75
75
  }
76
76
  // Check if object is in current room
77
- const currentRoom = gameState.currentRoom;
78
- if (currentRoom?.objects?.some(obj => obj.name.toLowerCase() === object.toLowerCase() ||
79
- obj.synonyms?.some(syn => syn.toLowerCase() === object.toLowerCase()))) {
77
+ const currentRoom = gameState.getCurrentRoom();
78
+ if (currentRoom?.objects?.some(objId => {
79
+ const obj = gameState.getObject(objId);
80
+ return obj && (obj.name.toLowerCase() === object.toLowerCase() ||
81
+ obj.synonyms?.some(syn => syn.toLowerCase() === object.toLowerCase()));
82
+ })) {
80
83
  return true;
81
84
  }
82
85
  // Check if object is in inventory
83
- if (gameState.inventory?.some(obj => obj.name.toLowerCase() === object.toLowerCase() ||
84
- obj.synonyms?.some(syn => syn.toLowerCase() === object.toLowerCase()))) {
86
+ if (gameState.inventory?.some(objId => {
87
+ const obj = gameState.getObject(objId);
88
+ return obj && (obj.name.toLowerCase() === object.toLowerCase() ||
89
+ obj.synonyms?.some(syn => syn.toLowerCase() === object.toLowerCase()));
90
+ })) {
85
91
  return true;
86
92
  }
87
93
  // Check if it's a room feature or scenery
@@ -105,14 +111,17 @@ export class ZMachineObjectInteraction {
105
111
  // Check visibility
106
112
  context.isVisible = this.checkObjectVisibility(object, gameState);
107
113
  // Check possession
108
- context.isPossessed = gameState.inventory?.some(obj => obj.name.toLowerCase() === object.toLowerCase() ||
109
- obj.synonyms?.some(syn => syn.toLowerCase() === object.toLowerCase())) || false;
114
+ context.isPossessed = gameState.inventory?.some(objId => {
115
+ const obj = gameState.getObject(objId);
116
+ return obj && (obj.name.toLowerCase() === object.toLowerCase() ||
117
+ obj.synonyms?.some(syn => syn.toLowerCase() === object.toLowerCase()));
118
+ }) || false;
110
119
  // Set location context
111
120
  if (context.isPossessed) {
112
121
  context.location = 'inventory';
113
122
  }
114
123
  else if (context.isVisible) {
115
- context.location = gameState.currentRoom?.name || 'unknown';
124
+ context.location = gameState.getCurrentRoom()?.name || 'unknown';
116
125
  }
117
126
  else {
118
127
  context.location = 'unknown';
@@ -171,10 +171,11 @@ export class ParityEnhancementEngine {
171
171
  */
172
172
  createMessageContext(command, gameState) {
173
173
  const context = this.parseCommand(command);
174
+ const currentRoom = gameState.getCurrentRoom();
174
175
  return {
175
176
  verb: context.verb,
176
177
  object: context.directObject,
177
- location: gameState.currentRoom?.name,
178
+ location: currentRoom?.name,
178
179
  command: command
179
180
  };
180
181
  }
@@ -84,6 +84,7 @@ export interface ParserErrorHandler {
84
84
  handleIncompleteCommand(verb: string, context: ParseContext): string;
85
85
  handleUnknownVerb(input: string): string;
86
86
  handleMalformedCommand(input: string): string;
87
+ verbRequiresObject(verb: string): boolean;
87
88
  }
88
89
  /**
89
90
  * ObjectInteractionManager interface for managing object interactions
@@ -100,6 +101,7 @@ export interface MessageConsistencyManager {
100
101
  standardizeMessage(messageType: MessageType, context: MessageContext): string;
101
102
  validateMessageFormat(message: string): boolean;
102
103
  getCanonicalMessage(messageType: MessageType, context: MessageContext): string;
104
+ formatErrorMessage(message: string): string;
103
105
  }
104
106
  /**
105
107
  * StateSynchronizationManager interface for game state validation
@@ -109,5 +111,6 @@ export interface StateSynchronizationManager {
109
111
  synchronizeObjectLocations(state: GameState): void;
110
112
  ensureInventoryConsistency(state: GameState): void;
111
113
  validateObjectAction?(action: string, objectId: string, gameState: GameState): any;
114
+ repairStateInconsistencies(state: GameState): ValidationResult;
112
115
  }
113
116
  //# sourceMappingURL=interfaces.d.ts.map
@@ -176,7 +176,7 @@ export class Serializer {
176
176
  location: obj.location,
177
177
  locationRelation: obj.locationRelation,
178
178
  properties: new Map(obj.properties),
179
- flags: new Set(obj.flags),
179
+ flags: obj.flags,
180
180
  capacity: obj.capacity,
181
181
  size: obj.size,
182
182
  value: obj.value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zork-ts",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Zork I: The Great Underground Empire - TypeScript Edition",
5
5
  "main": "dist/main.js",
6
6
  "bin": {
@@ -47,7 +47,7 @@
47
47
  "_securityNote": "pkg has GHSA-22r3-9w55-cj54 (moderate, local privilege escalation). Risk accepted: dev-only, local access required, no fix available. See dev-artifacts/docs/PACKAGING.md"
48
48
  },
49
49
  "engines": {
50
- "node": ">=18.0.0"
50
+ "node": ">=25.0.3"
51
51
  },
52
52
  "repository": {
53
53
  "type": "git",
@@ -69,7 +69,7 @@
69
69
  "author": "",
70
70
  "license": "MIT",
71
71
  "devDependencies": {
72
- "@types/node": "^24.10.4",
72
+ "@types/node": "^25.0.3",
73
73
  "pkg": "^5.8.1",
74
74
  "tsx": "^4.7.0",
75
75
  "typescript": "^5.3.0",
@@ -77,6 +77,6 @@
77
77
  },
78
78
  "dependencies": {
79
79
  "fast-check": "^4.5.3",
80
- "zod": "^4.1.13"
80
+ "zod": "^4.3.4"
81
81
  }
82
82
  }