Rename directory "engines" to "languages"
This commit is contained in:
277
languages/chef/runtime/index.ts
Normal file
277
languages/chef/runtime/index.ts
Normal file
@ -0,0 +1,277 @@
|
||||
import {
|
||||
DocumentRange,
|
||||
LanguageEngine,
|
||||
StepExecutionResult,
|
||||
} from "../../types";
|
||||
import { parseProgram } from "../parser";
|
||||
import * as T from "../types";
|
||||
import InputStream from "./input-stream";
|
||||
import ChefKitchen from "./kitchen";
|
||||
|
||||
/** Type for an item in the call stack */
|
||||
type CallStackItem = {
|
||||
auxName?: string;
|
||||
kitchen: ChefKitchen;
|
||||
recipe: T.ChefRecipe;
|
||||
pc: number;
|
||||
};
|
||||
|
||||
// Default values for internal states
|
||||
// Factories are used to create new objects on reset
|
||||
const DEFAULT_CALL_STACK = (): CallStackItem[] => [];
|
||||
const DEFAULT_INPUT = (): InputStream => new InputStream("");
|
||||
const DEFAULT_AST = (): T.ChefProgram => ({
|
||||
main: { ingredients: {}, name: "", method: [] },
|
||||
auxes: {},
|
||||
});
|
||||
|
||||
export default class ChefLanguageEngine implements LanguageEngine<T.ChefRS> {
|
||||
private _ast: T.ChefProgram = DEFAULT_AST();
|
||||
private _stack: CallStackItem[] = DEFAULT_CALL_STACK();
|
||||
private _input: InputStream = DEFAULT_INPUT();
|
||||
|
||||
resetState() {
|
||||
this._ast = DEFAULT_AST();
|
||||
this._stack = DEFAULT_CALL_STACK();
|
||||
this._input = DEFAULT_INPUT();
|
||||
}
|
||||
|
||||
validateCode(code: string) {
|
||||
parseProgram(code);
|
||||
}
|
||||
|
||||
prepare(code: string, input: string) {
|
||||
this._ast = parseProgram(code);
|
||||
this._input = new InputStream(input);
|
||||
const mainKitchen = new ChefKitchen(
|
||||
this._input,
|
||||
this._ast.main.ingredients
|
||||
);
|
||||
this._stack.push({ kitchen: mainKitchen, recipe: this._ast.main, pc: -1 });
|
||||
}
|
||||
|
||||
executeStep(): StepExecutionResult<T.ChefRS> {
|
||||
let output: string | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Execution happens only for method steps and the "Serves" line.
|
||||
* `currFrame.pc === method.length` implies that execution is currently at the "Serves" line.
|
||||
*/
|
||||
|
||||
// Process next operation
|
||||
const currFrame = this.getCurrentFrame();
|
||||
if (currFrame.pc === -1) {
|
||||
// First execution step - dummy
|
||||
currFrame.pc += 1;
|
||||
} else if (currFrame.pc === currFrame.recipe.method.length) {
|
||||
// Execution of the "Serves" statement
|
||||
const serves = currFrame.recipe.serves!;
|
||||
output = this.getKitchenOutput(currFrame.kitchen, serves.num);
|
||||
currFrame.pc += 1;
|
||||
} else {
|
||||
// Execution of a method instruction
|
||||
const { op } = currFrame.recipe.method[currFrame.pc];
|
||||
output = this.processOp(op);
|
||||
}
|
||||
|
||||
// Save for renderer state, in case program ends in this step
|
||||
const mainFrame = this._stack[0];
|
||||
|
||||
{
|
||||
// Check for end of recipe and pop call stack
|
||||
const currFrame = this.getCurrentFrame();
|
||||
const methodLength = currFrame.recipe.method.length;
|
||||
if (currFrame.pc > methodLength) {
|
||||
// "Serves" statement was just executed - now fold call stack
|
||||
this.foldCallStack();
|
||||
} else if (currFrame.pc === methodLength) {
|
||||
// Check if "Serves" statement exists. If not, fold call stack.
|
||||
if (!currFrame.recipe.serves) this.foldCallStack();
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare location of next step
|
||||
let nextStepLocation: DocumentRange | null = null;
|
||||
if (this._stack.length !== 0) {
|
||||
const currFrame = this.getCurrentFrame();
|
||||
if (currFrame.pc === currFrame.recipe.method.length) {
|
||||
// Next step is "Serves" statement
|
||||
nextStepLocation = { line: currFrame.recipe.serves!.line + 1 };
|
||||
} else {
|
||||
// Next step is a regular method instruction
|
||||
const nextOp = currFrame.recipe.method[currFrame.pc];
|
||||
nextStepLocation = nextOp.location;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare call stack names list
|
||||
const stackNames = this._stack.length
|
||||
? this._stack.map((frame) => frame.auxName || "Main recipe")
|
||||
: ["End of program"];
|
||||
|
||||
// Serialize current kitchen's state
|
||||
const currentKitchen = this._stack.length
|
||||
? this._stack[this._stack.length - 1].kitchen
|
||||
: mainFrame.kitchen;
|
||||
|
||||
// Prepare and send execution result
|
||||
return {
|
||||
rendererState: {
|
||||
stack: stackNames,
|
||||
currentKitchen: currentKitchen.serialize(),
|
||||
},
|
||||
nextStepLocation,
|
||||
output,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an operation. Also updates program counter state.
|
||||
* Note that call stack popping must be done by caller when pc goes past end of recipe.
|
||||
* @param op Operation to process
|
||||
* @returns String representing operation output (stdout)
|
||||
*/
|
||||
private processOp(op: T.ChefOperation): string | undefined {
|
||||
const currRecipe = this.getCurrentFrame();
|
||||
let opOutput = "";
|
||||
|
||||
switch (op.code) {
|
||||
case "LOOP-OPEN": {
|
||||
// Check ingredient value and jump/continue
|
||||
const ing = currRecipe.kitchen.getIngredient(op.ing, true);
|
||||
if (ing.value === 0) currRecipe.pc = op.closer + 1;
|
||||
else currRecipe.pc += 1;
|
||||
break;
|
||||
}
|
||||
|
||||
case "LOOP-BREAK": {
|
||||
// Jump to one past the loop closer
|
||||
currRecipe.pc = op.closer + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
case "LOOP-CLOSE": {
|
||||
// Decrement value of ingredient
|
||||
if (op.ing) {
|
||||
const ing = currRecipe.kitchen.getIngredient(op.ing, true);
|
||||
ing.value = ing.value! - 1;
|
||||
}
|
||||
|
||||
// Check value of loop-opener ingredient
|
||||
const opener = currRecipe.recipe.method[op.opener].op;
|
||||
if (opener.code !== "LOOP-OPEN") throw new Error("Bad jump address");
|
||||
const ing = currRecipe.kitchen.getIngredient(opener.ing, true);
|
||||
if (ing.value === 0) currRecipe.pc += 1;
|
||||
else currRecipe.pc = op.opener;
|
||||
break;
|
||||
}
|
||||
|
||||
case "FNCALL": {
|
||||
currRecipe.pc += 1;
|
||||
this.forkToAuxRecipe(currRecipe.kitchen, op.recipe);
|
||||
break;
|
||||
}
|
||||
|
||||
case "END": {
|
||||
// If `num` provided, get baking dishes output
|
||||
if (op.num)
|
||||
opOutput += this.getKitchenOutput(currRecipe.kitchen, op.num);
|
||||
|
||||
// Move pc to past end of recipe. Call stack popping is handled by `executeStep`
|
||||
currRecipe.pc = currRecipe.recipe.method.length;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Simple kitchen operations
|
||||
currRecipe.kitchen.processOperation(op);
|
||||
currRecipe.pc += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (opOutput) return opOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty the first N dishes of given kitchen into text output.
|
||||
* @param numDishes Number of dishes to empty as output
|
||||
* @returns Concatenated output from N baking dishes
|
||||
*/
|
||||
private getKitchenOutput(kitchen: ChefKitchen, numDishes: number): string {
|
||||
let output = "";
|
||||
for (let i = 1; i <= numDishes; ++i)
|
||||
output += kitchen.serializeAndClearDish(i);
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forks the bowls and dishes of the topmost recipe in the call stack
|
||||
* into an auxiliary recipe, and push it to the call stack.
|
||||
*/
|
||||
private forkToAuxRecipe(kitchen: ChefKitchen, recipeName: string): void {
|
||||
const { bowls, dishes } = kitchen.serialize();
|
||||
const auxRecipe = this._ast.auxes[recipeName];
|
||||
const ingredientsClone = this.deepCopy(auxRecipe.ingredients);
|
||||
const auxKitchen = new ChefKitchen(
|
||||
this._input,
|
||||
ingredientsClone,
|
||||
bowls,
|
||||
dishes
|
||||
);
|
||||
this._stack.push({
|
||||
auxName: recipeName,
|
||||
recipe: auxRecipe,
|
||||
kitchen: auxKitchen,
|
||||
pc: 0,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Repeatedly, pops topmost frame from call stack and pours its first mixing bowl
|
||||
* into the caller frame's first mixing bowl in the same order. This is done until
|
||||
* a not-fully-executed frame appears at the top of the call stack.
|
||||
*
|
||||
* Consider the call stack as a long divided strip of paper with each cell denoting a frame.
|
||||
* Then, visualize this as repeatedly paper-folding the completely-executed cell at end of
|
||||
* the paper strip onto the adjacent cell, until a non-completed cell is reached.
|
||||
*
|
||||
* The first iteration is done regardless of whether the topmost frame is completed or not.
|
||||
*
|
||||
* If the call stack is empty after a popping, no pouring is done.
|
||||
*/
|
||||
private foldCallStack(): void {
|
||||
while (true) {
|
||||
// Pop topmost frame and fold first bowl into parent frame's kitchen
|
||||
const poppedFrame = this._stack.pop()!;
|
||||
if (this._stack.length === 0) break;
|
||||
const parentFrame = this.getCurrentFrame();
|
||||
const firstBowl = poppedFrame.kitchen.getBowl(1);
|
||||
parentFrame.kitchen.getBowl(1).push(...firstBowl);
|
||||
|
||||
// Check if new topmost frame is completed or not
|
||||
if (!this.isFrameCompleted(parentFrame)) break;
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if a call stack frame is completely executed */
|
||||
private isFrameCompleted(frame: CallStackItem): boolean {
|
||||
if (frame.pc < frame.recipe.method.length) return false;
|
||||
if (frame.pc > frame.recipe.method.length) return true;
|
||||
return !frame.recipe.serves;
|
||||
}
|
||||
|
||||
/** Get topmost frame in call stack. Throws if stack is empty. */
|
||||
private getCurrentFrame(): CallStackItem {
|
||||
if (this._stack.length === 0) throw new Error("Call stack is empty");
|
||||
return this._stack[this._stack.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* A naive function to create a deep copy of an object.
|
||||
* Uses JSON serialization, so non-simple values like Date won't work.
|
||||
*/
|
||||
private deepCopy<T extends {}>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
}
|
35
languages/chef/runtime/input-stream.ts
Normal file
35
languages/chef/runtime/input-stream.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { RuntimeError } from "../../worker-errors";
|
||||
|
||||
/**
|
||||
* A barebones input stream implementation for consuming integers from a string.
|
||||
*/
|
||||
export default class InputStream {
|
||||
private _text: string;
|
||||
|
||||
/** Create a new input stream loaded with the given input */
|
||||
constructor(text: string) {
|
||||
this._text = text;
|
||||
}
|
||||
|
||||
/** Remove leading whitespace from the current input stream */
|
||||
private exhaustLeadingWhitespace(): void {
|
||||
const firstChar = this._text.trim()[0];
|
||||
const posn = this._text.search(firstChar);
|
||||
this._text = this._text.slice(posn);
|
||||
}
|
||||
|
||||
/** Parse input stream for an integer */
|
||||
getNumber(): number {
|
||||
this.exhaustLeadingWhitespace();
|
||||
// The extra whitespace differentiates whether string is empty or all numbers.
|
||||
if (this._text === "") throw new RuntimeError("Unexpected end of input");
|
||||
let posn = this._text.search(/[^0-9]/);
|
||||
if (posn === 0)
|
||||
throw new RuntimeError(`Unexpected input character: '${this._text[0]}'`);
|
||||
if (posn === -1) posn = this._text.length;
|
||||
// Consume and parse numeric part
|
||||
const numStr = this._text.slice(0, posn);
|
||||
this._text = this._text.slice(posn);
|
||||
return parseInt(numStr, 10);
|
||||
}
|
||||
}
|
251
languages/chef/runtime/kitchen.ts
Normal file
251
languages/chef/runtime/kitchen.ts
Normal file
@ -0,0 +1,251 @@
|
||||
import {
|
||||
BakingDish,
|
||||
ChefKitchenOp,
|
||||
IngredientBox,
|
||||
IngredientItem,
|
||||
MixingBowl,
|
||||
StackItem,
|
||||
} from "../types";
|
||||
import InputStream from "./input-stream";
|
||||
import { RuntimeError } from "../../worker-errors";
|
||||
|
||||
/** Type for a list maintained as an index map */
|
||||
type IndexList<T> = { [k: string]: T };
|
||||
|
||||
/**
|
||||
* Class for manipulating resources and utensils for a single Chef kitchen.
|
||||
* Contains methods for modifying ingredients, bowls and dishes corresponding to Chef instructions.
|
||||
*/
|
||||
export default class ChefKitchen {
|
||||
private _ingredients: IngredientBox;
|
||||
private _bowls: IndexList<MixingBowl>;
|
||||
private _dishes: IndexList<BakingDish>;
|
||||
private _input: InputStream;
|
||||
|
||||
constructor(
|
||||
inputStream: InputStream,
|
||||
ingredients: IngredientBox,
|
||||
bowls: IndexList<MixingBowl> = {},
|
||||
dishes: IndexList<BakingDish> = {}
|
||||
) {
|
||||
this._ingredients = ingredients;
|
||||
this._bowls = bowls;
|
||||
this._dishes = dishes;
|
||||
this._input = inputStream;
|
||||
}
|
||||
|
||||
/** Serialize and create a deep copy of the kitchen's ingredients, bowls and dishes */
|
||||
serialize(): {
|
||||
ingredients: IngredientBox;
|
||||
bowls: IndexList<MixingBowl>;
|
||||
dishes: IndexList<BakingDish>;
|
||||
} {
|
||||
return {
|
||||
ingredients: this.deepCopy(this._ingredients),
|
||||
bowls: this.deepCopy(this._bowls),
|
||||
dishes: this.deepCopy(this._dishes),
|
||||
};
|
||||
}
|
||||
|
||||
/** Get mixing bowl by 1-indexed identifier */
|
||||
getBowl(bowlId: number): MixingBowl {
|
||||
if (this._bowls[bowlId - 1] == null) this._bowls[bowlId - 1] = [];
|
||||
return this._bowls[bowlId - 1];
|
||||
}
|
||||
|
||||
/** Get baking dish by 1-indexed identifier */
|
||||
getDish(dishId: number): BakingDish {
|
||||
if (this._dishes[dishId - 1] == null) this._dishes[dishId - 1] = [];
|
||||
return this._dishes[dishId - 1];
|
||||
}
|
||||
|
||||
/** Serialize baking dish into string and clear the dish */
|
||||
serializeAndClearDish(dishId: number): string {
|
||||
const dish = this.getDish(dishId);
|
||||
let output = "";
|
||||
while (dish.length !== 0) {
|
||||
const item = dish.pop()!;
|
||||
if (item.type === "liquid") output += String.fromCharCode(item.value);
|
||||
else output += " " + item.value.toString();
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
getIngredient(name: string, assertValue?: boolean): IngredientItem {
|
||||
const item = this._ingredients[name];
|
||||
if (!item) throw new RuntimeError(`Ingredient '${name}' does not exist`);
|
||||
if (assertValue && item.value == null)
|
||||
throw new RuntimeError(`Ingredient '${name}' is undefined`);
|
||||
else return item;
|
||||
}
|
||||
|
||||
/** Process a Chef kitchen operation on this kitchen */
|
||||
processOperation(op: ChefKitchenOp): void {
|
||||
if (op.code === "STDIN") this.stdinToIngredient(op.ing);
|
||||
else if (op.code === "PUSH") this.pushToBowl(op.bowlId, op.ing);
|
||||
else if (op.code === "POP") this.popFromBowl(op.bowlId, op.ing);
|
||||
else if (op.code === "ADD") this.addValue(op.bowlId, op.ing);
|
||||
else if (op.code === "SUBTRACT") this.subtractValue(op.bowlId, op.ing);
|
||||
else if (op.code === "MULTIPLY") this.multiplyValue(op.bowlId, op.ing);
|
||||
else if (op.code === "DIVIDE") this.divideValue(op.bowlId, op.ing);
|
||||
else if (op.code === "ADD-DRY") this.addDryIngredients(op.bowlId);
|
||||
else if (op.code === "LIQ-ING") this.liquefyIngredient(op.ing);
|
||||
else if (op.code === "LIQ-BOWL") this.liquefyBowl(op.bowlId);
|
||||
else if (op.code === "ROLL-BOWL") this.stirBowl(op.bowlId, op.num);
|
||||
else if (op.code === "ROLL-ING") this.stirIngredient(op.bowlId, op.ing);
|
||||
else if (op.code === "RANDOM") this.mixBowl(op.bowlId);
|
||||
else if (op.code === "CLEAR") this.cleanBowl(op.bowlId);
|
||||
else if (op.code === "COPY") this.pourIntoDish(op.bowlId, op.dishId);
|
||||
else throw new Error(`Unknown kitchen opcode: '${op["code"]}''`);
|
||||
}
|
||||
|
||||
/** Read a number from stdin into the value of an ingredient. */
|
||||
stdinToIngredient(ingredient: string): void {
|
||||
const value = this._input.getNumber();
|
||||
this.getIngredient(ingredient).value = value;
|
||||
}
|
||||
|
||||
/** Push value of an ingredient into a mixing bowl */
|
||||
pushToBowl(bowlId: number, ingredient: string): void {
|
||||
const item = this.getIngredient(ingredient, true);
|
||||
this.getBowl(bowlId).push({ ...(item as StackItem) });
|
||||
}
|
||||
|
||||
/** Pop value from a mixing bowl and store into an ingredient */
|
||||
popFromBowl(bowlId: number, ingredient: string): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
if (bowl.length === 0) throw new RuntimeError(`Bowl ${bowlId} is empty`);
|
||||
|
||||
const item = bowl.pop() as StackItem;
|
||||
this.getIngredient(ingredient).type = item.type;
|
||||
this.getIngredient(ingredient).value = item.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the value of an ingredient to the top of a mixing bowl,
|
||||
* pushing the result onto the same bowl
|
||||
*/
|
||||
addValue(bowlId: number, ingredient: string): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
if (bowl.length === 0) throw new RuntimeError(`Bowl ${bowlId} is empty`);
|
||||
const bowlValue = bowl.pop()!.value;
|
||||
const ingValue = this.getIngredient(ingredient, true).value as number;
|
||||
bowl.push({ type: "unknown", value: ingValue + bowlValue });
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract the value of an ingredient from the top of a mixing bowl,
|
||||
* pushing the result onto the same bowl
|
||||
*/
|
||||
subtractValue(bowlId: number, ingredient: string): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
if (bowl.length === 0) throw new RuntimeError(`Bowl ${bowlId} is empty`);
|
||||
const bowlValue = bowl.pop()!.value;
|
||||
const ingValue = this.getIngredient(ingredient, true).value as number;
|
||||
bowl.push({ type: "unknown", value: bowlValue - ingValue });
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiply the value of an ingredient with the top of a mixing bowl,
|
||||
* pushing the result onto the same bowl
|
||||
*/
|
||||
multiplyValue(bowlId: number, ingredient: string): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
if (bowl.length === 0) throw new RuntimeError(`Bowl ${bowlId} is empty`);
|
||||
const bowlValue = bowl.pop()!.value;
|
||||
const ingValue = this.getIngredient(ingredient, true).value as number;
|
||||
bowl.push({ type: "unknown", value: ingValue * bowlValue });
|
||||
}
|
||||
|
||||
/**
|
||||
* Divide the top of a mixing bowl by the value of an ingredient,
|
||||
* pushing the result onto the same bowl
|
||||
*/
|
||||
divideValue(bowlId: number, ingredient: string): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
if (bowl.length === 0) throw new RuntimeError(`Bowl ${bowlId} is empty`);
|
||||
const bowlValue = bowl.pop()!.value;
|
||||
const ingValue = this.getIngredient(ingredient, true).value as number;
|
||||
bowl.push({ type: "unknown", value: bowlValue / ingValue });
|
||||
}
|
||||
|
||||
/** Add values of all dry ingredients and push onto a mixing bowl */
|
||||
addDryIngredients(bowlId: number): void {
|
||||
const totalValue = Object.keys(this._ingredients).reduce((sum, name) => {
|
||||
const ing = this._ingredients[name];
|
||||
if (ing.type !== "dry") return sum;
|
||||
if (ing.value == null)
|
||||
throw new RuntimeError(`Ingredient ${name} is undefined`);
|
||||
return sum + ing.value;
|
||||
}, 0);
|
||||
this.getBowl(bowlId).push({ type: "dry", value: totalValue });
|
||||
}
|
||||
|
||||
/** Convert an ingredient into a liquid */
|
||||
liquefyIngredient(name: string): void {
|
||||
this.getIngredient(name).type = "liquid";
|
||||
}
|
||||
|
||||
/** Convert all items in a bowl to liquids */
|
||||
liquefyBowl(bowlId: number): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
bowl.forEach((item) => (item.type = "liquid"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll the top `num` elements of a bowl such that top item goes down `num` places.
|
||||
* If bowl has less than `num` items, top item goes to bottom of bowl.
|
||||
*/
|
||||
stirBowl(bowlId: number, num: number): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
const topIngredient = bowl.pop();
|
||||
if (!topIngredient) return;
|
||||
const posn = Math.max(bowl.length - num, 0);
|
||||
bowl.splice(posn, 0, topIngredient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll the top `num` elements of a bowl such that top item goes down `num` places ,
|
||||
* where `num` is the value of the specified ingredient. If bowl has less than `num` items,
|
||||
* top item goes to bottom of bowl.
|
||||
*/
|
||||
stirIngredient(bowlId: number, ingredient: string): void {
|
||||
const ing = this.getIngredient(ingredient, true);
|
||||
const num = ing.value as number;
|
||||
this.stirBowl(bowlId, num);
|
||||
}
|
||||
|
||||
/** Randomly shuffle the order of items in a mixing bowl */
|
||||
mixBowl(bowlId: number): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
|
||||
// Fisher-Yates algorithm
|
||||
let remaining = bowl.length;
|
||||
while (remaining) {
|
||||
const i = Math.floor(Math.random() * remaining--);
|
||||
const temp = bowl[i];
|
||||
bowl[i] = bowl[remaining];
|
||||
bowl[remaining] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove all items from a mixing bowl */
|
||||
cleanBowl(bowlId: number): void {
|
||||
this._bowls[bowlId - 1] = [];
|
||||
}
|
||||
|
||||
/** Copy the items of a mixing bowl to a baking dish in the same order */
|
||||
pourIntoDish(bowlId: number, dishId: number): void {
|
||||
const bowl = this.getBowl(bowlId);
|
||||
const dish = this.getDish(dishId);
|
||||
dish.push(...bowl);
|
||||
}
|
||||
|
||||
/**
|
||||
* A naive function to create a deep copy of an object.
|
||||
* Uses JSON serialization, so non-simple values like Date won't work.
|
||||
*/
|
||||
private deepCopy<T extends {}>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user