Implement green teleports and the Lynx/CC2 PRNG

This commit is contained in:
Eevee (Evelyn Woods) 2020-10-23 21:09:31 -06:00
parent 603a74a751
commit f1b040f176
3 changed files with 85 additions and 18 deletions

View File

@ -38,7 +38,9 @@ Give it a try, I guess! [https://c.eev.ee/lexys-labyrinth/](https://c.eev.ee/le
## Special thanks ## 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 - 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 - [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! - Everyone who contributed music — see [`js/soundtrack.js`](js/soundtrack.js) for a list!

View File

@ -212,6 +212,9 @@ export class Level {
this.hint_shown = null; this.hint_shown = null;
// TODO in lynx/steam, this carries over between levels; in tile world, you can set it manually // TODO in lynx/steam, this carries over between levels; in tile world, you can set it manually
this.force_floor_direction = 'north'; this.force_floor_direction = 'north';
// PRNG is initialized to zero
this._rng1 = 0;
this._rng2 = 0;
this.undo_stack = []; this.undo_stack = [];
this.pending_undo = []; 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 // Move the game state forwards by one tic
advance_tic(p1_primary_direction, p1_secondary_direction) { advance_tic(p1_primary_direction, p1_secondary_direction) {
if (this.state !== 'playing') { if (this.state !== 'playing') {
@ -906,25 +929,55 @@ export class Level {
} }
// Handle teleporting, now that the dust has cleared // 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) { 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 // Teleporters already containing an actor are blocked and unusable
if (dest.cell.some(tile => tile.type.is_actor && tile !== actor)) if (dest.cell.some(tile => tile.type.is_actor && tile !== actor))
continue; continue;
// Physically move the actor to the new teleporter // 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 // XXX not especially undo-efficient
this.remove_tile(actor); this.remove_tile(actor);
this.add_tile(actor, dest.cell); this.add_tile(actor, dest.cell);
if (this.attempt_step(actor, actor.direction)) {
// Success, teleportation complete // 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 // 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 // thinks the player is currently; position isn't updated til next turn
this.sfx.play_once('teleport', dest.cell); this.sfx.play_once('teleport', teleporter.cell);
break; 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);
}
} }
} }
} }

View File

@ -818,29 +818,41 @@ const TILE_TYPES = {
}, },
teleport_blue: { teleport_blue: {
draw_layer: LAYER_TERRAIN, 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); return level.iter_tiles_in_reading_order(me.cell, 'teleport_blue', true);
}, },
}, },
teleport_red: { teleport_red: {
draw_layer: LAYER_TERRAIN, draw_layer: LAYER_TERRAIN,
teleport_dest_order(me, level) { teleport_try_all_directions: true,
// FIXME you can control your exit direction from red teleporters teleport_allow_override: true,
teleport_dest_order(me, level, other) {
return level.iter_tiles_in_reading_order(me.cell, 'teleport_red'); return level.iter_tiles_in_reading_order(me.cell, 'teleport_red');
}, },
}, },
teleport_green: { teleport_green: {
draw_layer: LAYER_TERRAIN, draw_layer: LAYER_TERRAIN,
teleport_dest_order(me, level) { teleport_try_all_directions: true,
// FIXME exit direction is random; unclear if it's any direction or only unblocked ones teleport_dest_order(me, level, other) {
let all = Array.from(level.iter_tiles_in_reading_order(me.cell, 'teleport_green')); let all = Array.from(level.iter_tiles_in_reading_order(me.cell, 'teleport_green'));
// FIXME this should use the lynxish rng if (all.length <= 1) {
return [random_choice(all), me]; // 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: { teleport_yellow: {
draw_layer: LAYER_TERRAIN, 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 // 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); 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 || if (type.draw_layer === undefined ||
type.draw_layer !== Math.floor(type.draw_layer) || 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`); console.error(`Tile type ${name} has a bad draw layer`);
} }