UNPKG

adventurejs

Version:

A javascript port of the 350-point version of the classic Adventure game.

1,240 lines (1,019 loc) 29.3 kB
"use strict"; /** * This file is a reimplementation of the game written at: * * https://github.com/Quuxplusone/Advent/blob/master/POHL0350/ * * Basically, it's the 350-point version of the Colossal Cave Adventure as * written by Jerry Pohl in 1984. (And of course based on the game created by * Will Crowther, and then extended by Don Woods.) It's mostly faithful to the * original, but I made a few changes here and there... */ const _ = require('lodash'); // const logger = require('../../logger'); const gu = require('./gameUtils'); const DEF = require('./vars/defs.json'); const STRINGS = _.assign({}, require('./vars/shortCave.json'), require('./vars/longCave.json'), require('./vars/items.json'), require('./vars/dialogue.json'), require('./vars/travel.json'), require('./vars/vocab.json') ); const MAXOBJ = 100; // max # of objects in cave // const MAXWC = 301; // max # of adventure words const MAXLOC = 145; // max # of cave locations // const WORDSIZE = 20; // max # of chars in commands // const MAXMSG = 201; // max # of action messages // const MAXTRAV = 16+1; // max # of travel directions from loc const DWARFMAX = 7; // max # of nasty dwarves const MAXDIE = 3; // max # of deaths before close const MAXTRS = 79; // max # of treasures in cave // API: exports.makeState = makeState; function makeState(obj) { return new GameState(obj || {}); } function GameState(prev) { _(this) .assign(prev) .defaults({ __v: 0, state: "init", done: false, stopEarly: false, loopAgain: false, bug: null, lines: [], word1: null, word2: null, wordRest: null, forcePlugh: false, verb: 0, object: 0, motion: 0, turns: 0, loc: 2, oldloc: 2, oldloc2: 2, newloc: 1, cond: [ 0,5,1,5,5,1,1,5,17,1,1,0,0,32,0,0,2,0,0,64,2,2,2,0,6,0,2,0,0,0,0,2,2,0, 0,0,0,0,4,0,2,0,128,128,128,128,136,136,136,128,128,128,128,128,136,128, 136,0,8,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,128,136,136,0,0,8,136, 128,0,2,2,0,0,0,0,4,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,4,0,1,1,0,0,0,0,0, 8,8,8,8,8,8,8,8,8,0,0,0,0,0,0,0,0,0,0,3,3,3,3,3 ], place: [ 0,3,3,8,10,11,0,14,13,94,96,19,17,101,103,0,106,0,0,3,3,0,0,109,25,23, 111,35,0,97,0,119,117,117,0,130,0,126,140,0,96,0,0,0,0,0,0,0,0,0,18,27, 28,29,30,0,92,95,97,100,101,0,119,127,130,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ], fixed: [ 0,0,0,9,0,0,0,15,0,-1,0,-1,27,-1,0,0,0,-1,0,0,0,0,0,-1,-1,67,-1,110,0, -1,-1,121,122,122,0,-1,-1,-1,-1,0,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,121,0,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 ], visited: new Array(MAXLOC + 1).fill(0), prop: new Array(MAXOBJ/2).fill(0) .concat(new Array(MAXOBJ/2).fill(-1)), tally: 15, tally2: 0, limit: 330, lmwarn: 0, wzdark: 0, closing: 0, closed: 0, holding: 0, detail: 0, knfloc: 0, clock: 30, clock2: 50, panic: 0, dloc: [0,19,27,33,44,64,114], dflag: 0, dseen: [0,0,0,0,0,0,0], odloc: [0,0,0,0,0,0,0], daltloc: 18, dkill: 0, chloc: 114, chloc2: 140, bonus: 0, numdie: 0, object1: 0, gaveup: 0, foobar: 0, saveflg: 0, dbugflg: 0 }) .value(); Object.seal(this); } // Copy in methods from other files: _.assign(GameState.prototype, require('./methods/movement')); _.assign(GameState.prototype, require('./methods/verbs')); GameState.prototype.advance = function advance(msg) { msg = String(msg || ""); this.lines = []; if (this.done) { return this.lines; } this.stopEarly = false; var state_fn = "_state_" + this.state; if (_.isFunction(this[state_fn])) { this[state_fn](msg); } else { this._bug("Invalid state encountered: " + this.state); } return this.lines; }; GameState.prototype.isDone = function () { return Boolean(this.done); }; GameState.prototype.locationString = function () { var short = STRINGS.shortDescriptions[this.loc] || []; return short.join(" ").trim(); }; /** * These methods are called when an advance is done when the game is is various * states: */ GameState.prototype._state_init = function () { this.state = "init_yesno"; this._rspeak(65); }; GameState.prototype._state_init_yesno = function (msg) { var yesno = this._parseYesNo(msg); if (!yesno) { return; } if (yesno === 'y') { this._rspeak(1); } this.state = "playing"; this._gameTick(); }; GameState.prototype._state_playing = function (msg) { if (!this._handleInputs(msg)) { return; } if (this.done) { return; } this._gameTick(); }; GameState.prototype._state_death_query = function (msg) { var yesno = this._parseYesNo(msg); if (!yesno) { return; } if (yesno === 'n') { this._rspeak(54); this.gaveup = 1; // Set the gaveup flag to avoid bonuses during scoring. return this._gameOver(); } // Else, the person is begging for resurrection: this._rspeak(82 + (this.numdie) * 2); if (++this.numdie >= MAXDIE) { // This person's died too many times... return this._gameOver(); } this.place[DEF.obj.WATER] = 0; this.place[DEF.obj.OIL] = 0; if (gu.toting(this, DEF.obj.LAMP)) { this.prop[DEF.obj.LAMP] = 0; } // Drop all items either on the ground or (for the lamp) in room 1: for (var j=1; j<MAXOBJ; j++) { var i = MAXOBJ - j; if (gu.toting(this, i)) { var drop_room = (i === DEF.obj.LAMP) ? 1 : this.oldloc2; gu.drop(this, i, drop_room); } } this.state = "playing"; this.newloc = 3; this.oldloc = this.loc; this._gameTick(); }; GameState.prototype._state_dragon_yesno = function (msg) { var yesno = this._parseYesNo(msg); if (!yesno) { return; } this.state = 'playing'; if (yesno === 'y') { this._pspeak(DEF.obj.DRAGON, 1); this.prop[DEF.obj.DRAGON] = 2; this.prop[DEF.obj.RUG] = 0; gu.move(this, DEF.obj.DRAGON+MAXOBJ, -1); gu.move(this, DEF.obj.RUG+MAXOBJ, 0); gu.move(this, DEF.obj.DRAGON, 120); gu.move(this, DEF.obj.RUG, 120); for(var i=1; i<MAXOBJ; i++) { if (this.place[i] === 119 || this.place[i] === 121) { gu.move(this, i, 120); } } this.newloc = 120; } else { this._rspeak(54); } this._gameTick(); }; GameState.prototype._state_giveup_yesno = function (msg) { var yesno = this._parseYesNo(msg); if (!yesno) { return; } this._rspeak(54); if (yesno === 'n') { this.state = "playing"; this._gameTick(); return; } this.gaveup = true; this._gameOver(); }; GameState.prototype._state_read_oyster_yesno = function (msg) { var yesno = this._parseYesNo(msg); if (!yesno) { return; } // Note: I can find no evidence in the original source code that the user was // ever ACTUALLY penalized for reading this hint... this._rspeak((yesno === 'y') ? 193 : 54); this.state = "playing"; this._gameTick(); }; // The the actions that make up a turn, minus input parsing. This is so some // game states can skip input parsing themselves... GameState.prototype._gameTick = function () { do { if (this.done || this.stopEarly) { return; } this.loopAgain = false; this._handleClosingLogic(); this._handleDwarves(); this._handleMovement(); this._handleTimers(); } while (this.loopAgain); }; GameState.prototype._inTwistyMaze = function () { return (this.loc >= 42 && this.loc <= 58) || (this.loc >= 80 && this.loc <= 87); }; GameState.prototype._handleClosingLogic = function () { if (this.newloc < 9 && this.newloc !== 0 && this.closing) { this._rspeak(130); this.newloc = this.loc; if (!this.panic) { this.clock2 = 15; } this.panic = 1; } }; // Parses the user message, and acts on it. Returns true IFF input was accepted. GameState.prototype._handleInputs = function (msg) { if (!this._parseEnglish(msg)) { return false; } // logger.debug( // "Adventure: Loc[%d] Verb[%d] Obj[%d] Motion[%d]", // this.loc, this.verb, this.object, this.motion // ); if (this.motion) { this._doMove(); } else if (this.object) { this._doObject(); } else { this._intransitiveVerb(); } // TODO: This is a hack. Rewrite to not use a "global": if (this.stopEarly) { return false; } return true; }; GameState.prototype._doObject = function () { var obj = this.object; var loc = this.loc; // Transitive object? if (this.fixed[obj] === this.loc || gu.here(this, obj)) { this._transitiveObject(); return; } // Grate? if (obj === DEF.obj.GRATE) { if (loc === 1 || loc === 4 || loc === 7) { this.motion = DEF.verb.DEPRESSION; this._doMove(); } else if (loc > 9 && loc < 15) { this.motion = DEF.verb.ENTRANCE; this._doMove(); } return; } // Dwarf? if (gu.dcheck(this) && this.dflag >= 2) { this.object = DEF.obj.DWARF; this._transitiveObject(); return; } // Liquid here? const liq_here = gu.liq(this) === obj; const bot_here = gu.here(this, DEF.obj.BOTTLE); const liq_loc = gu.liqloc(this) === obj; if ((liq_here && bot_here) || liq_loc) { this._transitiveObject(); return; } // Plant? const is_plant = obj === DEF.obj.PLANT; const plant_here = gu.at(this, DEF.obj.PLANT2); const plant_prop = this.prop[DEF.obj.PLANT2] === 0; if (is_plant && plant_here && plant_prop) { this.object = DEF.obj.PLANT2; this._transitiveObject(); return; } // Knife? if (obj === DEF.obj.KNIFE && this.knfloc === loc) { this._rspeak(116); this.knfloc = -1; return; } // Dynamite? if (obj === DEF.obj.ROD && gu.here(this, DEF.obj.ROD2)) { this.object = DEF.obj.ROD2; this._transitiveObject(); return; } // Trying to say something? if (this.verb === DEF.averb.SAY) { this._transitiveObject(); return; } // Else, give up: var n = this._wordNoun(); var str = n ? `no ${n}` : "nothing like that"; this._msg(`I see ${str} here.`); }; // These w fns are used to pick words that match a type: GameState.prototype._wordNoun = function () { var w1 = this.word1, w2 = this.word2; if (w1 && this._analyzeWord(w1).type === 1) { return w1; } if (w2 && this._analyzeWord(w2).type === 1) { return w2; } return null; }; GameState.prototype._wordVerb = function () { var w1 = this.word1, w2 = this.word2; if (w1 && this._analyzeWord(w1).type === 2) { return w1; } if (w2 && this._analyzeWord(w2).type === 2) { return w2; } return null; }; GameState.prototype._transitiveObject = function () { if (this.verb) { this._transitiveVerb(); } else { var n = this._wordNoun(); var str = n ? `the ${n}` : "that"; this._msg(`What do you want to do with ${str}?`); } }; GameState.prototype._handleDwarves = function () { var i, j; // Handle dwarves restricting movement: const has_moved = this.newloc !== this.loc; const loc_forced = gu.forced(this, this.loc); const loc_pirate = !gu.locFlag(this, "NOPIRAT"); if (has_moved && !loc_forced && loc_pirate) { for (i=0; i<DWARFMAX-1; i++) { if (this.odloc[i] === this.newloc && this.dseen[i]) { this.newloc = this.loc; this._rspeak(2); break; } } } // Are dwarves allowed here? const newloc_forced = gu.forced(this, this.newloc); const newloc_no_pirates = gu.roomFlag(this, this.newloc, "NOPIRAT"); if (this.newloc === 0 || newloc_forced || newloc_no_pirates) { return; } // Have dwarves been activated? if (!this.dflag) { if (this.newloc >= 15) { this.dflag ++; } return; } // On first encounter, kill a random number of dwarves and drop an axe: if (this.dflag === 1) { // Hold off if user returned to safety, or if lucky: if (this.newloc < 15 || gu.pct(95)) { return; } this.dflag ++; for (i=1; i<3; i++) { if (gu.pct(50)) { this.dloc[ gu.randint(5) + 1] = 0; } } for(i=1; i<DWARFMAX-1; i++) { if (this.dloc[i] === this.newloc) { this.dloc[i] = this.daltloc; } this.odloc[i] = this.dloc[i]; } this._rspeak(3); gu.drop(this, DEF.obj.AXE, this.newloc); return; } var dtotal = 0, attack = 0, stick = 0, attempt; for (i=1; i<DWARFMAX; i++) { if (this.dloc[i] === 0) { continue; } // Dwarves move randomly: for (attempt = 1; attempt<20; ++attempt) { j = gu.randint(107) + 15; var not_old = j !== this.odloc[i]; var not_cur = j !== this.dloc[i]; var last_dwarf = i === DWARFMAX-1; var no_pirates = gu.roomFlag(this, j, "NOPIRAT"); if (not_old && not_cur && !(last_dwarf && no_pirates)) { break; } } // If couldn't move, just move to prior spot: if (attempt === 20) { j = this.odloc[i]; } this.odloc[i] = this.dloc[i]; this.dloc[i] = j; var already_seen_in_cave = this.dseen[i] && this.newloc >= 15; var cur_match = this.dloc[i] === this.newloc; var new_match = this.odloc[i] === this.newloc; // dseen is 1 iff already seen, and still in cave, or if location matches: this.dseen[i] = Number(already_seen_in_cave || cur_match || new_match); if (!this.dseen[i]) { continue; } this.dloc[i] = this.newloc; if (i === 6) { this._pirateLogic(); } else { dtotal ++; if (this.odloc[i] === this.dloc[i]) { attack ++; if (this.knfloc >= 0) { this.knfloc = this.newloc; } if (gu.randint(1000) < 95*(this.dflag-2)) { stick ++; } } } } if (dtotal === 0) { return; } if (dtotal > 1) { this._msg(`There are ${dtotal} threatening little dwarves in the room with you!`); this._msg(""); } else { this._rspeak(4); } if (attack === 0) { return; } if (this.dflag === 2) { this.dflag ++; } var k; if (attack > 1) { this._msg(`${attack} of them throw knives at you!!`); this._msg(""); k = 6; } else { this._rspeak(5); k = 52; } if (stick <= 1) { this._rspeak(stick+k); if (stick === 0) { return; } } else { this._msg(`${stick} of them get you !!!`); this._msg(""); } this.oldloc2 = this.newloc; this._death(); }; GameState.prototype._pirateLogic = function () { var k = 0; if (this.newloc === this.chloc || this.prop[DEF.obj.CHEST] >= 0) { return; } for (var j=50; j<=MAXTRS; j++) { var not_pyramid = j !== DEF.obj.PYRAMID; var not_with_pyramid = this.newloc !== this.place[DEF.obj.PYRAMID]; var not_with_emerald = this.newloc !== this.place[DEF.obj.EMERALD]; if (not_pyramid || (not_with_pyramid && not_with_emerald)) { if (gu.toting(this, j)) { return this._pirateStole(); } if (gu.here(this, j)) { k ++; } } } const tally_match = this.tally === this.tally2 + 1; const chest_place = this.place[DEF.obj.CHEST] === 0; const lamp_here = gu.here(this, DEF.obj.LAMP); const lamp_on = this.prop[DEF.obj.LAMP] === 1; if (tally_match && k === 0 && chest_place && lamp_here && lamp_on) { this._rspeak(186); gu.move(this, DEF.obj.CHEST, this.chloc); gu.move(this, DEF.obj.MESSAGE, this.chloc2); this.dloc[6] = this.chloc; this.odloc[6] = this.chloc; this.dseen[6] = 0; return; } if (this.odloc[6] !== this.dloc[6] && gu.pct(20)) { this._rspeak(127); } }; GameState.prototype._pirateStole = function () { this._rspeak(128); if (this.place[DEF.obj.MESSAGE] === 0) { gu.move(this, DEF.obj.CHEST, this.chloc); } gu.move(this, DEF.obj.MESSAGE, this.chloc2); for (var j=50; j<=MAXTRS; j++) { var is_pyramid = j === DEF.obj.PYRAMID; var is_with_pyramid = this.newloc === this.place[DEF.obj.PYRAMID]; var is_with_emerald = this.newloc === this.place[DEF.obj.EMERALD]; if (is_pyramid && (is_with_pyramid || is_with_emerald)) { continue; } if (gu.at(this, j) && this.fixed[j] === 0) { gu.carry(this, j); } if (gu.toting(this, j)) { gu.drop(this, j, this.chloc); } } this.dloc[6] = this.chloc; this.odloc[6] = this.chloc; this.dseen[6] = 0; }; GameState.prototype._handleMovement = function () { if (this.loc === this.newloc) { return; } this.turns ++; this.loc = this.newloc; if (this.loc === 0) { return this._death(); } if (gu.forced(this, this.loc)) { this._handleDescription(); this._doMove(); this.loopAgain = true; return; } // Wandering in the dark === 35% chance of death / movement: if (this.wzdark && gu.dark(this) && gu.pct(35)) { this._rspeak(23); this.oldloc2 = this.loc; return this._death(); } this._handleDescription(); }; GameState.prototype._handleDescription = function () { var desc; if (gu.toting(this, DEF.obj.BEAR)) { this._rspeak(141); } const is_dark = gu.dark(this); if (is_dark) { this._rspeak(16); } else if (this.visited[this.loc]) { desc = STRINGS.shortDescriptions[this.loc] || []; } else { desc = STRINGS.longDescriptions[this.loc] || []; } if (desc) { desc.forEach(line => this._msg(line)); this._msg(""); } if (this.loc === 33 && (gu.pct(25) || this.forcePlugh) && !this.closing) { this.forcePlugh = false; this._rspeak(8); } if (is_dark) { return; } // Else, we can see... this.visited[this.loc] ++; for (var i=1; i<MAXOBJ; i++) { if (gu.at(this, i)) { if (i === DEF.obj.STEPS && gu.toting(this, DEF.obj.NUGGET)) { continue; } if (this.prop[i] < 0) { if (this.closed) { continue; } this.prop[i] = 0; if (i === DEF.obj.RUG || i === DEF.obj.CHAIN) { this.prop[i] ++; } this.tally --; } var is_steps = i === DEF.obj.STEPS; var steps_here = this.loc === this.fixed[DEF.obj.STEPS]; var state = (is_steps && steps_here) ? 1 : this.prop[i]; this._pspeak(i, state); } } if (this.tally === this.tally2 && this.tally !== 0 && this.limit > 35) { this.limit = 35; } if (this._inTwistyMaze()) { this._dumpPaths(); } return; }; GameState.prototype._dumpPaths = function () { var paths = gu.paths(this.loc); var tot = paths[0].length + paths[1].length; if (!tot) { return; } var str = (tot === 1) ? "There is a path" : "There are paths"; if (paths[0].length === 1) { str += " " + paths[0][0]; } else if (paths[0].length === 2) { str += " both " + paths[0].join(" and "); } if (paths[1].length) { if (paths[0].length) { str += ", and"; } str += " to the"; if (paths[1].length === 1) { str += " " + paths[1][0]; } else { var len = paths[1].length; paths[1][len-1] = "and " + paths[1][len-1]; str += " " + paths[1].join(", "); } } this._msg(str + "."); this._msg(""); }; GameState.prototype._handleTimers = function () { if (this.done || this.stopEarly || this.loopAgain) { return; } // Closing time logic: if (this.closed) { if (this.prop[DEF.obj.OYSTER] < 0 && gu.toting(this, DEF.obj.OYSTER)) { this._pspeak(DEF.obj.OYSTER, 1); } for (var i=1; i<MAXOBJ; i++) { if (gu.toting(this, i) && this.prop[i] < 0) { this.prop[i] = -1-this.prop[i]; } } } // Store darkness here so that we can detect dark -> dark movement: this.wzdark = gu.dark(this); // ?? if (this.knfloc > 0 && this.knfloc !== this.loc) { this.knfloc = 0; } // As the grains of sand slip by... if (this._timer()) { this.loopAgain = true; } }; GameState.prototype._timer = function () { var i; this.foobar = this.foobar > 0 ? -this.foobar : 0; if (this.tally === 0 && this.loc >= 15 && this.loc !== 33) { this.clock--; } if (this.clock < 0) { this.clock2 --; } // Should we start closing the cave? if (this.clock === 0) { this.prop[DEF.obj.GRATE] = 0; this.prop[DEF.obj.FISSURE] = 0; for (i=1; i<DWARFMAX; i++) { this.dseen[i] = 0; } gu.move(this, this.TROLL, 0); gu.move(this, this.TROLL+MAXOBJ, 0); gu.move(this, this.TROLL2, 117); gu.move(this, this.TROLL2+MAXOBJ, 122); if (this.prop[DEF.obj.BEAR] !== 3) { gu.destroy(this, DEF.obj.BEAR); } this.prop[DEF.obj.CHAIN] = 0; this.fixed[DEF.obj.CHAIN] = 0; this.prop[DEF.obj.AXE] = 0; this.fixed[DEF.obj.AXE] = 0; this._rspeak(129); this.clock = -1; this.closing = 1; return false; } // Actually close the cave: if (this.clock2 === 0) { this.prop[DEF.obj.BOTTLE] = gu.put(this, DEF.obj.BOTTLE, 115, 1); this.prop[DEF.obj.PLANT] = gu.put(this, DEF.obj.PLANT, 115, 0); this.prop[DEF.obj.OYSTER] = gu.put(this, DEF.obj.OYSTER, 115, 0); this.prop[DEF.obj.LAMP] = gu.put(this, DEF.obj.LAMP, 115, 0); this.prop[DEF.obj.ROD] = gu.put(this, DEF.obj.ROD, 115, 0); this.prop[DEF.obj.DWARF] = gu.put(this, DEF.obj.DWARF, 115, 0); this.loc = 115; this.oldloc = 115; this.newloc = 115; gu.put(this, DEF.obj.GRATE, 116, 0); this.prop[DEF.obj.SNAKE] = gu.put(this, DEF.obj.SNAKE, 116, 1); this.prop[DEF.obj.BIRD] = gu.put(this, DEF.obj.BIRD, 116, 1); this.prop[DEF.obj.CAGE] = gu.put(this, DEF.obj.CAGE, 116, 0); this.prop[DEF.obj.ROD2] = gu.put(this, DEF.obj.ROD2, 116, 0); this.prop[DEF.obj.PILLOW] = gu.put(this, DEF.obj.PILLOW, 116, 0); this.prop[DEF.obj.MIRROR] = gu.put(this, DEF.obj.MIRROR, 115, 0); this.fixed[DEF.obj.MIRROR] = 116; for (i=1; i<MAXOBJ; i++) { if (gu.toting(this, i)) { gu.destroy(this, i); } } this._rspeak(132); this.closed = 1; this._handleDescription(); return true; } // Lamp batteries degrade: if (this.prop[DEF.obj.LAMP] === 1) { this.limit --; } const lamp_low = this.limit <= 30; const have_batteries = gu.here(this, DEF.obj.BATTERIES); const not_used = this.prop[DEF.obj.BATTERIES] === 0; const have_lamp = gu.here(this, DEF.obj.LAMP); // Batteries getting low, but have replacements: if (lamp_low && have_batteries && not_used && have_lamp) { this._rspeak(188); this.prop[DEF.obj.BATTERIES] = 1; if (gu.toting(this, DEF.obj.BATTERIES)) { gu.drop(this, DEF.obj.BATTERIES, this.loc); } this.limit += 2500; this.lmwarn = 0; return false; } // Batteries are dead: if (this.limit === 0) { this.limit --; this.prop[DEF.obj.LAMP] = 0; if (gu.here(this, DEF.obj.LAMP)) { this._rspeak(184); } return false; } // After batteries died, did person run outside: if (this.limit < 0 && this.loc <= 8) { this._rspeak(185); this.gaveup = 1; this._gameOver(); return false; } // Low battery warning: if (this.limit <= 30) { if (this.lmwarn || !gu.here(this, DEF.obj.LAMP)) { return false; } this.lmwarn = 1; i = 187; if (this.place[DEF.obj.BATTERIES] === 0) { i = 183; } if (this.prop[DEF.obj.BATTERIES] === 1) { i = 189; } this._rspeak(i); return false; } return false; }; /** * Methods for talking / querying user: */ const YES_RE = /^(y|yes|yeah|yarp|ya|yep|sure|ok|affirmative)$/i; const NO_RE = /^(n|no|nope|nah|narp)$/i; GameState.prototype._parseYesNo = function (msg) { msg = String(msg).trim(); if (YES_RE.test(msg)) { return "y"; } if (NO_RE.test(msg)) { return "n"; } this._msg("Please say either 'yes' or 'no'"); return null; }; GameState.prototype._parseEnglish = function (msg) { const fail_msg = "bad grammar..."; this.word1 = this.word2 = null; this.verb = this.object = this.motion = 0; var split = msg.trim().toLowerCase().split(/\s+/, 2); var w1 = this._analyzeWord(split[0]); if (!w1) { return false; } this.word1 = split[0]; if (w1.type === 2 && w1.val === DEF.averb.SAY) { this.verb = DEF.averb.SAY; this.object = 1; this.wordRest = msg.trim().replace(/^say\s+/i, ''); return true; } var w2 = { type: -1, val: -1 }; if (split[1]) { w2 = this._analyzeWord(split[1]); if (!w2) { return false; } } this.word2 = split[1]; // Hackish rewrite: Sometimes people want to do: "enter <location>", but // enter doesn't combo with locations. Rewrite them to support it: if (w1.word === "enter" && w2.type === 0) { w1 = w2; w2 = { type: -1, val: -1 }; } if (w1.type === 3 && w2.type === 3 && w1.val === 51 && w2.val === 51) { this._outWords(); return 0; } else if (w1.type === 3) { this._rspeak(w1.val); return 0; } else if (w2.type === 3) { this._rspeak(w2.val); return 0; } else if (w1.type === 0) { if (w2.type === 0) { this._msg(fail_msg); return 0; } this.motion = w1.val; } else if (w2.type === 0) { this.motion = w2.val; } else if (w1.type === 1) { this.object = w1.val; if (w2.type === 2) { this.verb = w2.val; } if (w2.type === 1) { this._msg(fail_msg); return 0; } } else if (w1.type === 2) { this.verb = w1.val; if (w2.type === 1) { this.object = w2.val; } if (w2.type === 2) { this._msg(fail_msg); return 0; } } else { this._bug(36); return 0; } return 1; }; // List a lot of the useful words: GameState.prototype._outWords = function () { var list = STRINGS.vocab .filter(e => (e.code < 1000 || (e.code < 3000 && e.code > 1999))) .filter(e => e.word !== 'xyzzy' && e.word !== 'plugh') .reduce(function (acc, e) { if (acc[acc.length-1].length >= 5) { acc.push([]); } acc[acc.length-1].push(_.pad(e.word, 12)); return acc; }, [[]]) .map(row => row.join(" ")); list.forEach(x => this._msg(x)); }; const ORDERED_WORDS = _.sortBy(STRINGS.vocab, "code"); GameState.prototype._analyzeWord = function (word, min) { min = min || 0; var matches = ORDERED_WORDS .filter(e => e.word === word) .filter(e => e.code >= min); if (!matches.length) { this._rspeak([60, 61, 13][gu.randint(3)]); return null; } var m = matches[0]; var code = min ? m.code % 1000 : m.code; return { code, word, type: Math.floor(code / 1000), val: code % 1000 }; }; GameState.prototype._pspeak = function (prop_id, state_id) { // TODO: Verify that state_id is being used correctly here... var msg = STRINGS.items[prop_id][state_id+1]; if (msg) { this._msg(msg); this._msg(""); } return; }; GameState.prototype._rspeak = function (msg_id) { if (!msg_id) { return; } var messages = STRINGS.dialogue[msg_id] || []; messages.forEach(msg => this._msg(msg)); this._msg(""); return; }; GameState.prototype._dwarfDeath = function () { this._rspeak(136); return this._death(); }; GameState.prototype._death = function () { if (this.closing || this.closed) { // Sorry, but it's too close to closing time... this._rspeak(131); this.numdie ++; return this._gameOver(); } this._rspeak(81 + (this.numdie) * 2); this.state = "death_query"; this.stopEarly = true; return; }; GameState.prototype._gameOver = function () { this.done = true; this.stopEarly = true; this._msg(" * GAME OVER *"); this._msg(""); this._score(); return; }; GameState.prototype._score = function () { var t, i, k, s, extra; s = t = extra = 0; for (i=50; i<=MAXTRS; i++) { if (i === DEF.obj.CHEST) { k = 14; } else if (i > DEF.obj.CHEST) { k = 16; } else { k = 12; } if (this.prop[i] >= 0) { t += 2; } if (this.place[i] === 3 && this.prop[i] === 0) { t += k - 2; } } this._msg("Showing up: 2"); s += 2; if (t) { s += t; this._msg("Treasures: " + t); } // No survival bonus if you gave up: t = (MAXDIE - this.numdie) * 10; if (t && !this.gaveup && this.done) { s += t; this._msg("Survival: " + t); } if (!this.gaveup && this.done) { extra += 4; } t = this.dflag ? 25 : 0; if (t) { s += t; this._msg("Getting well in: " + t); } t = this.closing ? 25 : 0; if (t) { s += t; this._msg("Masters section: " + t); } if (this.closed) { if (this.bonus === 0) { t = 10; } else if (this.bonus === 135) { t = 25; } else if (this.bonus === 134) { t = 30; } else if (this.bonus === 133) { t = 45; } s += t; this._msg("Bonus: " + t); } if (this.place[DEF.obj.MAGAZINE] === 108) { extra += 1; } if (extra) { s += extra; this._msg("Secrets: " + extra); } this._msg("SCORE: " + s); }; GameState.prototype._msg = function (str) { this.lines.push(String(str)); return; }; /* * Will exit a state object, and give a bug value: */ GameState.prototype._bug = function _bug(code) { this.done = true; this.bug = code; this.stopEarly = true; this.loopAgain = false; if (_.isNumber(code)) { this._msg(`Fatal error. (Bug #${code}) :cry:`); } else { this._msg(`Fatal error. (${code}) :cry:`); } return; };