/** * @author tint * @template Data, Input */ export class Game { /** * @param {number} history How many frames of history to keep in memory * @param {(data: Data, input: Input, frame: number) => Data} advance The function to apply game logic. For rollback to work properly, this must be a pure function, and can't mutate inputs. */ constructor(history, advance) { this.historysize = history; this.advance = advance; /** @type {{data: Data, input: Input}[]} */ this.history = []; this.historyStart = 0; // the game may have inputs from the "future" // (local input delay to make sure inputs play at the same time on all machines) // so the "present" isn't always the latest frame // the game loop should set this every frame this.currentFrame = 0; } startHistory(frame, data) { this.historyStart = frame; this.history = [{ data }]; this.currentFrame = frame; } getHistory(frame) { return this.history[frame - this.historyStart]; } getFrame() { return this.historyStart + this.history.length - 1; } getCurrentData() { const entry = this.history[this.history.length - 1]; return entry && entry.data; } /** * Sets the input at a specific frame. If that frame is in history, * the game will be rewound, the input applied, and then fast-forwarded to the current head. * If the frame is ahead of the current latest frame, the game will be run until that frame. * @param {number} frame The time to apply the input at * @param {Input} input The input */ setInput(frame, input) { this.editFrame(frame, index => { let past = this.history[index - 1]; if(index === 0) { past = { data: undefined }; } this.history[index] = { input, data: this.advance(past ? past.data : undefined, input, frame), }; }); } setData(frame, data) { this.editFrame(frame, index => { this.history[index] = { data, input: this.history[index] && this.history[index].input, } }); } /** * @param {number} frame * @param {(index: number) => void} edit */ editFrame(frame, edit) { const head = this.historyStart + this.history.length; if(frame < head) { if(frame < this.historyStart) { throw new Error("Tried to edit a past frame not in history:", frame); } edit(frame - this.historyStart); // fast forward back to the present with the new data for(let i = frame + 1; i < head; i++) { const past = this.history[i - this.historyStart - 1]; this.history[i - this.historyStart].data = this.advance( past ? past.data : undefined, this.history[i - this.historyStart].input, i ); } } else { // fast forward the inbetween frames with no input for(let i = head; i < frame; i++) { const entry = this.history[i - this.historyStart - 1]; this.history[i - this.historyStart] = { input: undefined, data: this.advance(entry ? entry.data : undefined, undefined, i), }; } edit(frame - this.historyStart); } while(this.history.length > this.historysize) { this.history.shift(); this.historyStart++; } } }