Browse Source

initial commit

master
Stephanie Gredell 1 week ago
commit
53f3da48bc
  1. 1
      .gitignore
  2. 50
      InputHandler.js
  3. 25
      PlanDayCommand.js
  4. 50
      Time.js
  5. 16
      enums.js
  6. 249
      game.js
  7. 89
      game.test.js
  8. 30
      index.html
  9. 45
      index.js
  10. 15
      jest.config.js
  11. 14
      jsconfig.json
  12. 4410
      package-lock.json
  13. 16
      package.json
  14. 14
      utils.js

1
.gitignore vendored

@ -0,0 +1 @@ @@ -0,0 +1 @@
node_modules

50
InputHandler.js

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
import { Phase, PlayerActions } from "./enums";
import { assert } from "./utils";
const valid_actions = Object.values(PlayerActions);
class InputHandler {
/**
* Handles input relating to the player's game actions.
* @class
* @property {import('./game.js').GameState} game_state - The game state this handler manipulates
* @property {import('./Time.js').Time} time - The time manager for the current game loop
*
* The InputHandler manages player events (clicks, keystrokes, etc.).
* It validates actions, updates game state, and interacts with the time system as needed.
*/
game_state;
time;
constructor(game_state, time) {
this.game_state = game_state;
this.time = time;
}
setup() {
document.addEventListener("click", this.handleClickEvent);
}
/**
* Click handler
* @param {Event} e
* @returns
*/
handleClickEvent(e) {
if (!e.target) return;
const el = /** @type {Element} */ (e.target)
const target = /** @type {HTMLElement | null} */ (el.closest("[data-action]"));
assert(!!target, "there must be a valid target");
const action = target?.dataset?.action ?? "";
switch (action) {
case PlayerActions.OPEN:
this.game_state.time.phase = Phase.OPEN;
break;
case PlayerActions.CLOSE:
this.game_state.time.phase = Phase.CLOSE;
break;
}
}
}

25
PlanDayCommand.js

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
import { Phase } from "./enums";
import { assert } from "./utils";
export class PlanDayCommand {
/**
* Executes the plan day command
* @param {import(".").GameState} game_state
* @returns {import(".").GameState}
*/
execute(game_state) {
assert(
game_state.time.phase === Phase.CLOSE,
"store must be closed before you can plan",
);
return {
...game_state,
time: {
...game_state.time,
day: game_state.time.day + 1,
phase: Phase.PLANNING,
},
};
}
}

50
Time.js

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
import { Phase } from "./enums.js";
import { PlanDayCommand } from "./StartDayCommand.js";
import { assert } from "./utils.js";
/**
* @typedef {import('./game.js').GameState} GameState
*/
/**
* Manages a single day in the bakery game
*/
export class Time {
/** @type {GameState} */
game_state;
/** @type {import("./game.js").Command[]} */
events;
/**
* @param {GameState} game_state - The current game state
*/
constructor(game_state) {
this.game_state = game_state;
this.events = [];
}
/**
* Queues a command for execution
* @param {import("./game.js").Command} command
*/
queue(command) {
this.events.push(command);
}
/**
* Updates the day timer
* @param {number} dt - Delta time in milliseconds
*/
update(dt) {
this.game_state.time.elapsed_ms += dt;
for (const command of this.events) {
this.game_state = command.execute(this.game_state);
}
}
/** Renders the current day state */
render() {}
}
export { Day };

16
enums.js

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
export const Phase = Object.freeze({
PLANNING: "planning",
OPEN: "open",
CLOSE: "close",
});
export const MenuItemState = Object.freeze({
ENABLED: "enabled",
DISABLED: "disabled",
});
export const PlayerActions = Object.freeze({
PLAN: "day",
OPEN: "day",
CLOSE: "day",
});

249
game.js

@ -0,0 +1,249 @@ @@ -0,0 +1,249 @@
import { MenuItemState } from "./enums.js";
import { assert } from "./utils.js";
/**
* @typedef {Object} SuppliesBase
* @property {number} flour
* @property {number} eggs
* @property {number} oil
* @property {number} butter
* @property {number} milk
* @property {number} yeast
* @property {number} chocolate
* @property {number} fruit
* @property {number} sugar
*/
/** @typedef {SuppliesBase & {[key: string]: number}} Supplies */
/**
* @typedef {'enabled' | 'disabled'} MenuItemStateValue
*/
/**
* @typedef {Object} MenuItem
* @property {Partial<Supplies>} ingredients
* @property {number} oven_slots
* @property {number} base_demand
* @property {MenuItemStateValue} state
*/
/**
* @typedef {'bread' | 'sweet_rolls' | 'vanilla_cake' | 'cookies' | 'fruit_muffins' | 'croissants' | 'chocolate_cookies' | 'chocolate_cake' | 'fruit_tart'} MenuItemName
*/
/**
* @typedef {Object} GameState
* @property {number} player_money
* @property {{ day: number, phase: string, elapsed_ms: number, day_length_ms: number, tick_ms: number }} time
* @property {Supplies} supplies
* @property {Record<MenuItemName, MenuItem>} menu
* @property {Supplies} store
* @property {{ capacity: number, used: number, queue: string[] }} oven
* @property {Record<MenuItemName, number>} prices
*/
/**
* @typedef {Object} Command
* @property {(game_state: GameState) => GameState} execute
*/
/**
* Creates a new game state with default values
* @returns {GameState}
*/
function init_game_state() {
return {
player_money: 0,
time: {
day: 1,
phase: "planning",
elapsed_ms: 0,
day_length_ms: 30000,
tick_ms: 250,
},
supplies: {
flour: 0,
eggs: 0,
oil: 0,
butter: 0,
milk: 0,
yeast: 0,
chocolate: 0,
fruit: 0,
sugar: 0,
},
menu: {
bread: {
ingredients: {
flour: 2,
yeast: 1,
milk: 1,
},
oven_slots: 1,
base_demand: 40,
state: MenuItemState.ENABLED,
},
sweet_rolls: {
ingredients: {
flour: 2,
sugar: 1,
butter: 1,
milk: 1,
},
oven_slots: 1,
base_demand: 35,
state: MenuItemState.ENABLED,
},
vanilla_cake: {
ingredients: {
flour: 2,
eggs: 2,
sugar: 2,
milk: 1,
oil: 1,
},
oven_slots: 2,
base_demand: 25,
state: MenuItemState.ENABLED,
},
cookies: {
ingredients: {
flour: 1,
eggs: 1,
sugar: 1,
butter: 1,
},
oven_slots: 2,
base_demand: 40,
state: MenuItemState.ENABLED,
},
fruit_muffins: {
ingredients: {
flour: 2,
eggs: 1,
sugar: 1,
fruit: 1,
},
oven_slots: 2,
base_demand: 30,
state: MenuItemState.ENABLED,
},
croissants: {
ingredients: {
flour: 2,
butter: 2,
milk: 1,
yeast: 1,
},
oven_slots: 3,
base_demand: 20,
state: MenuItemState.ENABLED,
},
chocolate_cookies: {
ingredients: {
flour: 1,
eggs: 1,
sugar: 1,
butter: 1,
chocolate: 1,
},
oven_slots: 2,
base_demand: 35,
state: MenuItemState.ENABLED,
},
chocolate_cake: {
ingredients: {
flour: 2,
eggs: 2,
sugar: 2,
butter: 1,
chocolate: 2,
milk: 1,
},
oven_slots: 3,
base_demand: 12,
state: MenuItemState.ENABLED,
},
fruit_tart: {
ingredients: {
flour: 2,
butter: 2,
sugar: 1,
eggs: 1,
fruit: 2,
},
oven_slots: 4,
base_demand: 15,
state: MenuItemState.ENABLED,
},
},
store: {
flour: 0.6,
sugar: 0.4,
yeast: 0.3,
oil: 0.7,
milk: 0.8,
eggs: 0.9,
butter: 1.2,
fruit: 1.3,
chocolate: 1.5,
},
oven: {
capacity: 4,
used: 0,
queue: [],
},
prices: {
bread: 4.0,
sweet_rolls: 5.0,
vanilla_cake: 9.0,
cookies: 6.0,
fruit_muffins: 6.5,
croissants: 7.5,
chocolate_cookies: 7.0,
chocolate_cake: 14.0,
fruit_tart: 12.0,
},
};
}
/**
* Bakes an item if supplies and oven capacity allow
* @param {GameState} game_state - The current game state
* @param {MenuItemName} item_name - The name of the item to bake
* @returns {GameState} The updated game state
*/
function bake_items(game_state, item_name) {
const menu_item = game_state.menu[item_name];
const oven_slots = menu_item.oven_slots;
const canFitInOven =
game_state.oven.used + oven_slots <= game_state.oven.capacity;
assert(canFitInOven, "oven must have capacity before baking");
assert(!!game_state, "game_state must be defined");
assert(
game_state.menu[item_name].state === MenuItemState.ENABLED,
"item must be bakeable with current supplies",
);
// supplies update
const updated_supplies = {
...game_state.supplies,
};
for (const ingredient in menu_item.ingredients) {
updated_supplies[ingredient] -= menu_item.ingredients[ingredient] ?? 0;
}
return {
...game_state,
supplies: updated_supplies,
oven: {
...game_state.oven,
used: game_state.oven.used + oven_slots,
},
};
}
export { init_game_state, bake_items };

89
game.test.js

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
import { init_game, bake_items, is_menu_item_enabled } from './game.js';
describe('init_game', () => {
test('should create a new game state with default values', () => {
const game = init_game();
expect(game.player_money).toBe(0);
expect(game.time.day).toBe(1);
expect(game.time.phase).toBe('planning');
expect(game.oven.capacity).toBe(4);
expect(game.oven.used).toBe(0);
});
test('should start with zero supplies', () => {
const game = init_game();
expect(game.supplies.flour).toBe(0);
expect(game.supplies.eggs).toBe(0);
expect(game.supplies.sugar).toBe(0);
});
});
describe('is_menu_item_enabled', () => {
test('should return false when supplies are insufficient', () => {
const game = init_game();
// No supplies, so nothing should be enabled
expect(is_menu_item_enabled(game, 'bread')).toBe(false);
expect(is_menu_item_enabled(game, 'cookies')).toBe(false);
});
test('should return true when supplies are sufficient', () => {
const game = init_game();
// Add enough supplies for bread (flour: 2, yeast: 1, milk: 1)
game.supplies.flour = 10;
game.supplies.yeast = 5;
game.supplies.milk = 5;
expect(is_menu_item_enabled(game, 'bread')).toBe(true);
});
test('should return false for non-existent menu item', () => {
const game = init_game();
expect(is_menu_item_enabled(game, 'pizza')).toBe(false);
});
});
describe('bake_items', () => {
test('should deduct ingredients when baking', () => {
const game = init_game();
// Add supplies for bread
game.supplies.flour = 10;
game.supplies.yeast = 5;
game.supplies.milk = 5;
const result = bake_items(game, 'bread');
// Bread uses: flour: 2, yeast: 1, milk: 1
expect(result.supplies.flour).toBe(8);
expect(result.supplies.yeast).toBe(4);
expect(result.supplies.milk).toBe(4);
});
test('should increase oven used slots', () => {
const game = init_game();
game.supplies.flour = 10;
game.supplies.yeast = 5;
game.supplies.milk = 5;
const result = bake_items(game, 'bread');
// Bread uses 1 oven slot
expect(result.oven.used).toBe(1);
});
test('should not mutate the original game state', () => {
const game = init_game();
game.supplies.flour = 10;
game.supplies.yeast = 5;
game.supplies.milk = 5;
bake_items(game, 'bread');
// Original should be unchanged
expect(game.supplies.flour).toBe(10);
expect(game.oven.used).toBe(0);
});
});

30
index.html

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
<html>
<head>
<title>Bakery</title>
</head>
<body>
<h1>Bake Shop</h1>
<button id="start_day">Start Day</button>
<div class="dashboard">
<div class="game_section">
<h2>Supplies</h2>
<ul>
<li>flour: <span data-binding="supplies.flour"></span></li>
<li>eggs: <span data-binding="supplies.eggs"></span></li>
<li>oil: <span data-binding="supplies.oil"></span></li>
<li>butter: <span data-binding="supplies.butter"></span></li>
<li>milk: <span data-binding="supplies.milk"></span></li>
<li>yeast: <span data-binding="supplies.yeast"></span></li>
<li>chocolate: <span data-binding="supplies.chocolate"></span></li>
<li>fruit: <span data-binding="supplies.fruit"></span></li>
<li>sugar: <span data-binding="supplies.sugar"></span></li>
</ul>
</div>
<!-- game_section -->
</div>
<!-- dashboard -->
</body>
<script src="index.js" type="module"></script>
</html>

45
index.js

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
import { Time } from "./Time.js";
import { init_game_state } from "./game.js";
/**
* @typedef {import('./game.js').GameState} GameState
*/
/** @type {GameState} */
let game_state = init_game_state();
let time = new Time(game_state);
/** @type {number} */
let last = performance.now();
/**
* Main game loop - runs every frame
* @param {number} now - Current timestamp from requestAnimationFrame
*/
function loop(now) {
const dt = now - last;
last = now;
update(dt);
render();
requestAnimationFrame(loop);
}
/**
* Updates game state based on elapsed time
* @param {number} dt - Delta time in milliseconds
*/
function update(dt) {
time.update(dt);
}
/**
* Renders the current game state
*/
function render() {
//render stuff here
}
requestAnimationFrame(loop);

15
jest.config.js

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
export default {
// Use the Node.js test environment
testEnvironment: 'node',
// Look for test files in these patterns
testMatch: [
'**/__tests__/**/*.js',
'**/*.test.js'
],
// Enable verbose output so you can see each test
verbose: true,
};

14
jsconfig.json

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
{
"compilerOptions": {
"checkJs": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"moduleResolution": "node",
"module": "ES2022",
"target": "ES2022"
},
"include": ["*.js"],
"exclude": ["node_modules", "*.test.js"]
}

4410
package-lock.json generated

File diff suppressed because it is too large Load Diff

16
package.json

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
{
"name": "bakery",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"jest": "^30.2.0"
}
}

14
utils.js

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
/**
* Asserts a condition is true, throws and breaks the game if not
* @param {boolean} condition - The condition to check
* @param {string} message - Error message if assertion fails
* @throws {Error} Throws an error if the condition is false
*/
export function assert(condition, message) {
if (!condition) {
const error = new Error(`Assertion failed: ${message}`);
console.error(error);
throw error;
}
}
Loading…
Cancel
Save