diff --git a/README.md b/README.md index 9cb7475..03ee3b4 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,9 @@ Give it a try, I guess! [https://c.eev.ee/lexys-labyrinth/](https://c.eev.ee/le ## Special thanks -- The incredible nerds who put together the [Chip Wiki](https://wiki.bitbusters.club/) and also reside on the Bit Busters Discord +- The incredible nerds who put together the [Chip Wiki](https://wiki.bitbusters.club/) and also reside on the Bit Busters Discord, including: + - ruben for documenting the CC2 PRNG + - The Architect for documenting the CC2 C2G parser - Everyone who worked on [Chip's Challenge Level Pack 1](https://wiki.bitbusters.club/Chip%27s_Challenge_Level_Pack_1), the default set of levels - [Tile World](https://wiki.bitbusters.club/Tile_World) for being an incredible reference on Lynx mechanics - Everyone who contributed music — see [`js/soundtrack.js`](js/soundtrack.js) for a list! diff --git a/js/game.js b/js/game.js index 78159ac..9d83d11 100644 --- a/js/game.js +++ b/js/game.js @@ -212,6 +212,9 @@ export class Level { this.hint_shown = null; // TODO in lynx/steam, this carries over between levels; in tile world, you can set it manually this.force_floor_direction = 'north'; + // PRNG is initialized to zero + this._rng1 = 0; + this._rng2 = 0; this.undo_stack = []; this.pending_undo = []; @@ -330,6 +333,26 @@ export class Level { } } + // Lynx PRNG, used unchanged in CC2 + prng() { + // TODO what if we just saved this stuff, as well as the RFF direction, at the beginning of + // each tic? + let rng1 = this._rng1; + let rng2 = this._rng2; + this.pending_undo.push(() => { + this._rng1 = rng1; + this._rng2 = rng2; + }); + + let n = (this._rng1 >> 2) - this._rng1; + if (!(this._rng1 & 0x02)) --n; + this._rng1 = (this._rng1 >> 1) | (this._rng2 & 0x80); + this._rng2 = (this._rng2 << 1) | (n & 0x01); + let ret = (this._rng1 ^ this._rng2) & 0xFF; + console.log(ret); + return ret; + } + // Move the game state forwards by one tic advance_tic(p1_primary_direction, p1_secondary_direction) { if (this.state !== 'playing') { @@ -906,25 +929,55 @@ export class Level { } // Handle teleporting, now that the dust has cleared - // FIXME something funny happening here, your input isn't ignore while walking out of it? + // FIXME something funny happening here, your input isn't ignored while walking out of it? if (teleporter) { - for (let dest of teleporter.type.teleport_dest_order(teleporter, this)) { + let original_direction = actor.direction; + let success = false; + for (let dest of teleporter.type.teleport_dest_order(teleporter, this, actor)) { // Teleporters already containing an actor are blocked and unusable if (dest.cell.some(tile => tile.type.is_actor && tile !== actor)) continue; // Physically move the actor to the new teleporter - // XXX is this right, compare with tile world? i overhear it's actually implemented as a slide? + // XXX lynx treats this as a slide and does it in a pass in the main loop // XXX not especially undo-efficient this.remove_tile(actor); this.add_tile(actor, dest.cell); - if (this.attempt_step(actor, actor.direction)) { - // Success, teleportation complete - // Sound plays from the origin cell simply because that's where the sfx player - // thinks the player is currently; position isn't updated til next turn - this.sfx.play_once('teleport', dest.cell); + + // Red and green teleporters attempt to spit you out in every direction before + // giving up on a destination (but not if you return to the original). + // Note that we use actor.direction here (rather than original_direction) because + // green teleporters modify it in teleport_dest_order, to randomize the exit + // direction + let direction = actor.direction; + let num_directions = 1; + if (teleporter.type.teleport_try_all_directions && dest !== teleporter) { + num_directions = 4; + } + for (let i = 0; i < num_directions; i++) { + if (this.attempt_step(actor, direction)) { + success = true; + // Sound plays from the origin cell simply because that's where the sfx player + // thinks the player is currently; position isn't updated til next turn + this.sfx.play_once('teleport', teleporter.cell); + break; + } + else { + direction = DIRECTIONS[direction].right; + } + } + + if (success) { break; } + else if (num_directions === 4) { + // Restore our original facing before continuing + // (For red teleports, we try every possible destination in our original + // movement direction, so this is correct. For green teleports, we only try one + // destination and then fall back to walking through the source in our original + // movement direction, so this is still correct.) + this.set_actor_direction(actor, original_direction); + } } } } diff --git a/js/tiletypes.js b/js/tiletypes.js index 24282d3..b41685e 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -818,29 +818,41 @@ const TILE_TYPES = { }, teleport_blue: { draw_layer: LAYER_TERRAIN, - teleport_dest_order(me, level) { + teleport_dest_order(me, level, other) { return level.iter_tiles_in_reading_order(me.cell, 'teleport_blue', true); }, }, teleport_red: { draw_layer: LAYER_TERRAIN, - teleport_dest_order(me, level) { - // FIXME you can control your exit direction from red teleporters + teleport_try_all_directions: true, + teleport_allow_override: true, + teleport_dest_order(me, level, other) { return level.iter_tiles_in_reading_order(me.cell, 'teleport_red'); }, }, teleport_green: { draw_layer: LAYER_TERRAIN, - teleport_dest_order(me, level) { - // FIXME exit direction is random; unclear if it's any direction or only unblocked ones + teleport_try_all_directions: true, + teleport_dest_order(me, level, other) { let all = Array.from(level.iter_tiles_in_reading_order(me.cell, 'teleport_green')); - // FIXME this should use the lynxish rng - return [random_choice(all), me]; + if (all.length <= 1) { + // If this is the only teleporter, just walk out the other side — and, crucially, do + // NOT advance the PRNG + return [me]; + } + // Note the iterator starts on the /next/ teleporter, so there's an implicit +1 here. + // The -1 is to avoid spitting us back out of the same teleporter, which will be last in + // the list + let target = all[level.prng() % (all.length - 1)]; + // Also set the actor's (initial) exit direction + level.set_actor_direction(other, ['north', 'east', 'south', 'west'][level.prng() % 4]); + return [target, me]; }, }, teleport_yellow: { draw_layer: LAYER_TERRAIN, - teleport_dest_order(me, level) { + teleport_allow_override: true, + teleport_dest_order(me, level, other) { // FIXME special pickup behavior; NOT an item though, does not combine with no sign return level.iter_tiles_in_reading_order(me.cell, 'teleport_yellow', true); }, @@ -1517,7 +1529,7 @@ for (let [name, type] of Object.entries(TILE_TYPES)) { if (type.draw_layer === undefined || type.draw_layer !== Math.floor(type.draw_layer) || - type.draw_layer >= 4) + type.draw_layer >= 5) { console.error(`Tile type ${name} has a bad draw layer`); }