minesweeper/src/main/js/Action.ts

190 lines
5.3 KiB
TypeScript

/**
* An action that can (possibly) be done and undone.
*/
export class Action {
readonly run: () => void;
readonly undo: () => boolean;
readonly isUndoable: () => boolean;
/**
* Constructs a new action.
*
* @param doe the action to perform
* @param undo the inverse of the action to perform, returning whether the action has been undone
* @param isUndoable whether the action is currently undoable
*/
constructor(doe: () => void, undo: () => boolean, isUndoable: () => boolean) {
this.run = doe;
this.undo = undo;
this.isUndoable = isUndoable;
}
}
/**
* A sequence of actions that can be done and undone.
*/
export class ActionSequence extends Action {
private readonly actions: Action[] = [];
private _isUndoable: boolean = true;
/**
* Constructs a new action sequence.
*/
constructor() {
super(() => this._run(), () => this._undo(), () => this._isUndoable);
}
/**
* Adds an action to the sequence.
*
* If the given action is not undoable, then this action sequence is no longer undoable.
*
* @param action the action to add
*/
addAction(action: Action): void {
if (!action.isUndoable())
this._isUndoable = false;
this.actions.push(action);
}
/**
* Returns `true` if and only if this action sequence does not contain any actions.
*
* @returns `true` if and only if this action sequence does not contain any actions
*/
isEmpty(): boolean {
return this.actions.length === 0;
}
/**
* Performs all actions in the sequence in order.
*/
private _run(): void {
this.actions.forEach(it => it.run());
}
/**
* Undoes all actions in the sequence in reverse order.
*/
private _undo(): boolean {
if (this._isUndoable)
[...this.actions].reverse().forEach(it => it.undo());
return this._isUndoable;
}
}
/**
* A history of actions that can be tracked granularly and can be undone rapidly.
*/
export class ActionHistory {
private readonly sequences: ActionSequence[] = [];
private sequenceIndex = -1;
private depth: number = 0;
/**
* The sequence at the current index.
*
* @private
*/
private get currentSequence(): ActionSequence | undefined {
return this.sequences[this.sequenceIndex];
}
/**
* `true` if and only if there is a sequence of actions that has not been committed yet.
*/
get hasUncommittedSequence(): boolean {
return this.depth > 0;
}
/**
* Starts a new sequence of actions, or continues the current uncommitted sequence if there is one.
*/
startSequence(): void {
if (this.depth === 0) {
this.sequenceIndex++;
this.sequences.length = this.sequenceIndex;
this.sequences.push(new ActionSequence());
}
this.depth++;
}
/**
* Adds an action to the current uncommitted sequence.
*
* @param action the action to add
*/
addAction(action: Action): void {
if (!this.hasUncommittedSequence)
throw new Error("Cannot add action if there is no uncommitted sequence.");
this.currentSequence!.addAction(action);
}
/**
* Commits the last sequence of actions or removes it if no actions were added, but only if the number of calls to
* this function equals the number of calls to `#startSequence`.
*/
commitSequence(): void {
if (!this.hasUncommittedSequence)
throw new Error("Cannot commit sequence if there is no uncommitted sequence.");
this.depth--;
if (this.depth === 0 && this.currentSequence!.isEmpty()) {
this.sequences.pop();
this.sequenceIndex--;
}
}
/**
* Redoes up to the last `amount` of sequences that were undone.
*
* @param amount the maximum number of sequences to redo
* @returns the actual amount of sequences that have been redone
*/
redo(amount: number = this.sequences.length - (this.sequenceIndex + 1)): number {
if (this.hasUncommittedSequence)
throw new Error("Cannot redo sequences while there is an uncommitted sequence.");
amount = Math.min(amount, this.sequences.length - (this.sequenceIndex + 1));
for (let i = 0; i < amount; i++) {
this.sequenceIndex++;
this.currentSequence!.run();
}
return amount;
}
/**
* Undoes up to and including the last `amount` sequences in this history, stopping prematurely if any of the
* actions cannot be undone.
*
* @param amount the maximum amount of sequences to undo
* @returns the actual amount of sequences that have been undone
*/
undo(amount: number = this.sequenceIndex + 1): number {
if (this.hasUncommittedSequence)
throw new Error("Cannot undo sequences while there is an uncommitted sequence.");
amount = Math.min(amount, this.sequenceIndex + 1);
let i;
for (i = 0; i < amount; i++) {
const sequence = this.currentSequence!;
if (!sequence.isUndoable())
break;
sequence.undo();
this.sequenceIndex--;
}
return i;
}
}