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 = { [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; private _dishes: IndexList; private _input: InputStream; constructor( inputStream: InputStream, ingredients: IngredientBox, bowls: IndexList = {}, dishes: IndexList = {} ) { 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; dishes: IndexList; } { 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(obj: T): T { return JSON.parse(JSON.stringify(obj)); } }