Use a ring buffer for undo; don't pause when running out of undo during rewind

This commit is contained in:
Eevee (Evelyn Woods) 2020-11-03 11:57:16 -07:00
parent 350ac08d4d
commit 8ff0bd803a
2 changed files with 35 additions and 18 deletions

View File

@ -168,7 +168,6 @@ class GameEnded extends Error {}
// The undo stack is implemented with a ring buffer, and this is its size. One entry per tic.
// Based on Chrome measurements made against the pathological level CCLP4 #40 (Periodic Lasers) and
// sitting completely idle, undo consumes about 2 MB every five seconds.
// TODO actually make it a ring buffer
const UNDO_STACK_SIZE = TICS_PER_SECOND * 10;
export class Level {
constructor(stored_level, compat = {}) {
@ -229,7 +228,11 @@ export class Level {
this._blob_modifier = Math.floor(Math.random() * 256);
}
this.undo_stack = [];
this.undo_stack = new Array(UNDO_STACK_SIZE);
for (let i = 0; i < UNDO_STACK_SIZE; i++) {
this.undo_stack[i] = null;
}
this.undo_stack_index = 0;
this.pending_undo = this.create_undo_entry();
let n = 0;
@ -1322,13 +1325,21 @@ export class Level {
return entry;
}
has_undo() {
let prev_index = this.undo_stack_index - 1;
if (prev_index < 0) {
prev_index += UNDO_STACK_SIZE;
}
return this.undo_stack[prev_index] !== null;
}
commit() {
this.undo_stack.push(this.pending_undo);
this.undo_stack[this.undo_stack_index] = this.pending_undo;
this.pending_undo = this.create_undo_entry();
if (this.undo_stack.length > UNDO_STACK_SIZE) {
this.undo_stack.splice(0, this.undo_stack.length - UNDO_STACK_SIZE);
}
this.undo_stack_index += 1;
this.undo_stack_index %= UNDO_STACK_SIZE;
}
undo() {
@ -1338,11 +1349,20 @@ export class Level {
this._undo_entry(this.pending_undo);
this.pending_undo = this.create_undo_entry();
this._undo_entry(this.undo_stack.pop());
this.undo_stack_index -= 1;
if (this.undo_stack_index < 0) {
this.undo_stack_index += UNDO_STACK_SIZE;
}
this._undo_entry(this.undo_stack[this.undo_stack_index]);
this.undo_stack[this.undo_stack_index] = null;
}
// Reverse a single undo entry
_undo_entry(entry) {
if (! entry) {
return;
}
// Undo in reverse order! There's no redo, so it's okay to destroy this
entry.reverse();
for (let undo of entry) {

View File

@ -348,7 +348,7 @@ class Player extends PrimaryView {
// about to make a conscious move. Note that this means undoing all the way through
// force floors, even if you could override them!
let moved = false;
while (this.level.undo_stack.length > 0 &&
while (this.level.has_undo() &&
! (moved && this.level.player.slide_mode === null))
{
this.undo();
@ -368,7 +368,7 @@ class Player extends PrimaryView {
});
this.rewind_button = this.root.querySelector('.controls .control-rewind');
this.rewind_button.addEventListener('click', ev => {
if (this.level.undo_stack.length > 0) {
if (this.level.has_undo()) {
this.state = 'rewinding';
}
});
@ -465,7 +465,7 @@ class Player extends PrimaryView {
}
if (ev.key === 'z') {
if (this.level.undo_stack.length > 0 &&
if (this.level.has_undo() &&
(this.state === 'stopped' || this.state === 'playing' || this.state === 'paused'))
{
this.set_state('rewinding');
@ -854,18 +854,15 @@ class Player extends PrimaryView {
this.advance_by(1);
}
else if (this.state === 'rewinding') {
if (this.level.undo_stack.length === 0) {
// TODO detect if we hit the start of the level (rather than just running the undo
// buffer dry) and change to 'waiting' instead
// TODO pausing seems rude actually, it should just hover in-place?
this._advance_handle = null;
this.set_state('paused');
}
else {
if (this.level.has_undo()) {
// Rewind by undoing one tic every tic
this.undo();
this.update_ui();
}
// If there are no undo entries left, freeze in place until the player stops rewinding,
// which I think is ye olde VHS behavior
// TODO detect if we hit the start of the level (rather than just running the undo
// buffer dry) and change to 'waiting' instead?
}
let dt = 1000 / TICS_PER_SECOND;