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. // 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 // 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. // 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; const UNDO_STACK_SIZE = TICS_PER_SECOND * 10;
export class Level { export class Level {
constructor(stored_level, compat = {}) { constructor(stored_level, compat = {}) {
@ -229,7 +228,11 @@ export class Level {
this._blob_modifier = Math.floor(Math.random() * 256); 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(); this.pending_undo = this.create_undo_entry();
let n = 0; let n = 0;
@ -1322,13 +1325,21 @@ export class Level {
return entry; 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() { 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(); this.pending_undo = this.create_undo_entry();
if (this.undo_stack.length > UNDO_STACK_SIZE) { this.undo_stack_index += 1;
this.undo_stack.splice(0, this.undo_stack.length - UNDO_STACK_SIZE); this.undo_stack_index %= UNDO_STACK_SIZE;
}
} }
undo() { undo() {
@ -1338,11 +1349,20 @@ export class Level {
this._undo_entry(this.pending_undo); this._undo_entry(this.pending_undo);
this.pending_undo = this.create_undo_entry(); 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 // Reverse a single undo entry
_undo_entry(entry) { _undo_entry(entry) {
if (! entry) {
return;
}
// Undo in reverse order! There's no redo, so it's okay to destroy this // Undo in reverse order! There's no redo, so it's okay to destroy this
entry.reverse(); entry.reverse();
for (let undo of entry) { 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 // about to make a conscious move. Note that this means undoing all the way through
// force floors, even if you could override them! // force floors, even if you could override them!
let moved = false; let moved = false;
while (this.level.undo_stack.length > 0 && while (this.level.has_undo() &&
! (moved && this.level.player.slide_mode === null)) ! (moved && this.level.player.slide_mode === null))
{ {
this.undo(); this.undo();
@ -368,7 +368,7 @@ class Player extends PrimaryView {
}); });
this.rewind_button = this.root.querySelector('.controls .control-rewind'); this.rewind_button = this.root.querySelector('.controls .control-rewind');
this.rewind_button.addEventListener('click', ev => { this.rewind_button.addEventListener('click', ev => {
if (this.level.undo_stack.length > 0) { if (this.level.has_undo()) {
this.state = 'rewinding'; this.state = 'rewinding';
} }
}); });
@ -465,7 +465,7 @@ class Player extends PrimaryView {
} }
if (ev.key === 'z') { 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.state === 'stopped' || this.state === 'playing' || this.state === 'paused'))
{ {
this.set_state('rewinding'); this.set_state('rewinding');
@ -854,18 +854,15 @@ class Player extends PrimaryView {
this.advance_by(1); this.advance_by(1);
} }
else if (this.state === 'rewinding') { else if (this.state === 'rewinding') {
if (this.level.undo_stack.length === 0) { if (this.level.has_undo()) {
// 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 {
// Rewind by undoing one tic every tic // Rewind by undoing one tic every tic
this.undo(); this.undo();
this.update_ui(); 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; let dt = 1000 / TICS_PER_SECOND;