commit a1c2b6abef163240a66224b12b19ab2663f007c9 Author: Stephanie Gredell Date: Sun Dec 21 09:41:08 2025 -0800 initial commit diff --git a/Gemini_Generated_Image_60ejy60ejy60ejy6.png b/Gemini_Generated_Image_60ejy60ejy60ejy6.png new file mode 100644 index 0000000..28186a3 Binary files /dev/null and b/Gemini_Generated_Image_60ejy60ejy60ejy6.png differ diff --git a/Gemini_Generated_Image_60mila60mila60mi.png b/Gemini_Generated_Image_60mila60mila60mi.png new file mode 100644 index 0000000..67c48ad Binary files /dev/null and b/Gemini_Generated_Image_60mila60mila60mi.png differ diff --git a/Gemini_Generated_Image_bw4vqvbw4vqvbw4v.png b/Gemini_Generated_Image_bw4vqvbw4vqvbw4v.png new file mode 100644 index 0000000..1cd6eaf Binary files /dev/null and b/Gemini_Generated_Image_bw4vqvbw4vqvbw4v.png differ diff --git a/Gemini_Generated_Image_d2uiz4d2uiz4d2ui.png b/Gemini_Generated_Image_d2uiz4d2uiz4d2ui.png new file mode 100644 index 0000000..35ce726 Binary files /dev/null and b/Gemini_Generated_Image_d2uiz4d2uiz4d2ui.png differ diff --git a/Gemini_Generated_Image_kefpnykefpnykefp.png b/Gemini_Generated_Image_kefpnykefpnykefp.png new file mode 100644 index 0000000..7d6057f Binary files /dev/null and b/Gemini_Generated_Image_kefpnykefpnykefp.png differ diff --git a/cups.png b/cups.png new file mode 100644 index 0000000..a2d3248 Binary files /dev/null and b/cups.png differ diff --git a/customer_right.png b/customer_right.png new file mode 100644 index 0000000..358540d Binary files /dev/null and b/customer_right.png differ diff --git a/game.js b/game.js new file mode 100644 index 0000000..55f6dbe --- /dev/null +++ b/game.js @@ -0,0 +1,389 @@ +/** + * Enum representing possible weather conditions in the game. + * @readonly + * @enum {string} + */ +const Weather = Object.freeze({ + COLD: 'cold', + CLOUDY: 'cloudy', + SUNNY: 'sunny', + HOT: 'hot' +}); + +/** + * Enum representing game duration options. + * Provides both numeric values and reverse lookups. + * @readonly + * @enum {number|string} + */ +const Days = Object.freeze({ + 7: 7, + 14: 14, + 30: 30 +}); + +/** + * Multiplier applied to base demand based on weather conditions. + * Higher values mean more customers. + * @type {Object.} + */ +const WeatherFactor = { + COLD: 0.5, + CLOUDY: 0.8, + SUNNY: 1.0, + HOT: 1.4 +} + +/** + * The ideal price per cup for each weather condition. + * Pricing closer to these values yields better sales. + * @type {Object.} + */ +const IdealPrice = { + COLD: 0.20, + CLOUDY: 0.30, + SUNNY: 0.35, + HOT: 0.50 +} + +/** + * The ideal recipe (ingredient ratios) for each weather condition. + * Using these ratios produces the best taste score. + * @type {Object.} + */ +const IdealRecipe = { + COLD: { + lemons: 1, + sugar: 2, + ice: 1 + }, + CLOUDY: { + lemons: 1, + sugar: 1, + ice: 2 + }, + SUNNY: { + lemons: 1, + sugar: 1, + ice: 3 + }, + HOT: { + lemons: 1, + sugar: 1, + ice: 4 + } +} + +/** + * Probability weights for each weather type. + * Used to randomly determine the day's weather. + * @type {Array.<{type: string, weight: number}> + */ +const WeatherChance = [ + { type: Weather.CLOUDY, weight: 40 }, + { type: Weather.SUNNY, weight: 35 }, + { type: Weather.HOT, weight: 15 }, + { type: Weather.COLD, weight: 10 } +] + +/** + * @typedef {Object} Recipe + * @property {number} lemons + * @property {number} sugar + * @property {number} ice + */ + +/** + * @typedef {Object} Supplies + * @property {number} lemons + * @property {number} sugar + * @property {number} ice + * @property {number} cups + */ + +/** + * @typedef {Object.} PriceTable + */ + +/** + * @typedef {Object} GameState + * @property {number} player_money + * @property {Recipe} recipe + * @property {Supplies} supplies + * @property {string} weather + * @property {number} price_per_cup + * @property {number} days + * @property {{ + * lemons: PriceTable, + * sugar: PriceTable, + * ice: PriceTable, + * cups: PriceTable + * }} supplies_prices + * @property {number} cost_per_cup + * @property {number} cups_sold + */ + +/** + * Initialize a new game state with default values. + * Sets up empty recipe, zero supplies, default weather, and pricing tables. + * + * @returns {GameState} A fresh game state ready to begin. + */ +function init_game() { + return { + player_money: 0, + recipe: { + lemons: 0, + sugar: 0, + ice: 0 + }, + supplies: { + lemons: 0, + sugar: 0, + ice: 0, + cups: 0 + }, + weather: Weather.SUNNY, + price_per_cup: 0, + days: Days[7], + supplies_prices: { + lemons: { + 12: 4.80, + 24: 7.20, + 48: 9.60 + }, + sugar: { + 12: 4.80, + 20: 7.00, + 50: 15.00 + }, + ice: { + 50: 1.00, + 200: 3.00, + 500: 5.00 + }, + cups: { + 75: 1.00, + 225: 2.35, + 400: 3.75 + } + }, + cost_per_cup: 1.00, + cups_sold: 0 + } +} + +/** + * Set a new recipe for making lemonade. + * Defines the ratio of ingredients per cup. + * + * @param {GameState} game_state - The current game state. + * @param {number} lemons - The number of lemons per cup. + * @param {number} sugar - The amount of sugar per cup. + * @param {number} ice - The amount of ice per cup. + * @returns {GameState} A new game state with the updated recipe. + */ +function set_recipe(game_state, lemons, sugar, ice) { + console.assert(game_state, 'game_state must be defined'); + console.assert(typeof lemons == 'number', 'lemons must be a number'); + console.assert(typeof sugar == 'number', 'sugar must be a number'); + console.assert(typeof ice == 'number', 'ice must be a number'); + + return { + ...game_state, + recipe: { + ...game_state.recipe, + lemons, + sugar, + ice + } + }; +} + +/** + * Determine the day's weather randomly based on weighted probabilities. + * Uses WeatherChance to select a weather type. + * + * @param {GameState} game_state - The current state of the game. + * @returns {GameState} A new game state with the updated weather. + */ +function set_weather(game_state) { + const totalWeight = WeatherChance.reduce((sum, w) => sum + w.weight, 0); + let roll = Math.random() * totalWeight; + + for (const w of WeatherChance) { + if (roll < w.weight) { + return { + ...game_state, + weather: w.type + } + } + + roll -= w.weight + } +} + +/** + * Set the number of days the game will play through. + * Valid options are 7 (week), 14 (two weeks), or 30 (month). + * + * @param {GameState} game_state - The current state of the game. + * @param {number} days - The number of days (7, 14, or 30). + * @returns {GameState} A new game state with the updated number of days. + */ +function set_days(game_state, days) { + console.assert(game_state, 'game_state must be defined'); + console.assert(typeof days === 'number', 'days must be a number'); + console.assert(Object.values(Days).includes(days), 'invalid days value'); + + return { + ...game_state, + days: days + } +} + +/** + * Set the price per cup of lemonade. + * Rounds the cost to two decimal places. + * + * @param {GameState} game_state - The current game state. + * @param {number} cost - The price to charge per cup. + * @returns {GameState} A new game state with the updated cost per cup. + */ +function set_cost_per_cup(game_state, cost) { + console.assert(typeof cost === 'number', 'cost must be a number'); + console.assert(game_state, 'game_state must be defined'); + return { + ...game_state, + cost_per_cup: Math.round(parseFloat(cost) * 100) / 100 + } +} + +/** + * Calculate the number of cups sold based on pricing, weather, and taste. + * Demand is influenced by weather conditions, price sensitivity, and a random factor. + * + * @param {number} price_per_cup - The price charged per cup. + * @param {number} cups_in_supplies - The number of cups available to sell. + * @param {string} weather - The current weather condition (from Weather enum). + * @param {number} [tasteScore=1] - The taste quality score (0.5 to 1.2). + * @returns {number} The number of cups sold (capped by available supplies). + */ +function calculate_cups_sold(price_per_cup, cups_in_supplies, weather, tasteScore = 1) { + const base_demand = 30; + const weather_factor = WeatherFactor[weather] || 1.0; + const ideal_price = IdealPrice[weather] || 0.35 + + const sensitivity = 1.5; + let price_effect = 1 - (price_per_cup - ideal_price) * sensitivity; + if (price_effect < 0) { + price_effect = 0; + } + + let demand = base_demand * weather_factor * price_effect * tasteScore; + demand *= 0.9 + Math.random() * 0.2; + + const cupsSold = Math.min(Math.floor(demand), cups_in_supplies); + + return cupsSold; +} + +/** + * Calculate a taste score based on how close the recipe is to ideal values. + * Score ranges from 0.5 (poor) to 1.2 (excellent). + * + * @param {number} lemons_per_cup - The number of lemons used per cup. + * @param {number} sugar_per_cup - The amount of sugar used per cup. + * @param {number} [ideal_lemons=1] - The ideal number of lemons per cup. + * @param {number} [ideal_sugar=1] - The ideal amount of sugar per cup. + * @returns {number} A taste score between 0.5 and 1.2. + */ +function calculate_taste_score(lemons_per_cup, sugar_per_cup, ideal_lemons = 1, ideal_sugar = 1) { + const lemon_diff = Math.abs(lemons_per_cup - ideal_lemons); + const sugar_diff = Math.abs(sugar_per_cup - ideal_sugar); + + let score = 1.0; + + score -= (lemon_diff * 0.3 + sugar_diff * 0.2); + + if (score < 0.5) score = 0.5; + if (score > 1.2) score = 1.2; + + return score; +} + +/** + * Simulate a single day of lemonade sales. + * Calculates cups sold based on recipe quality, weather, and pricing. + * + * @param {GameState} game_state - The current game state. + * @returns {number} The number of cups sold during the day. + */ +function simulate_day(game_state) { + console.assert(game_state, 'game_state must not be undefined'); + + const recipe = game_state.recipe; + const weather = game_state.weather; + const supplies = game_state.supplies; + + const taste_score = calculate_taste_score( + recipe.lemons, + recipe.sugar, + IdealRecipe[weather].lemons, + IdealRecipe[weather].sugar, + ); + + const cups_sold = calculate_cups_sold(game_state.cost_per_cup, supplies.cups, weather, taste_score); + + return cups_sold; +} + +/** + * Execute lemonade production and sales for the day. + * Calculates cups that can be made from available supplies, + * determines sales, updates inventory, and calculates profit. + * + * @param {GameState} game_state - The current game state. + * @returns {GameState} A new game state with updated money, supplies, and cups sold. + */ +function make_lemonade(game_state) { + console.assert(game_state, 'game_state must be defined'); + + const recipe = game_state.recipe; + const weather = game_state.weather; + const price = game_state.price_per_cup; + + const ideal = IdealRecipe[weather]; + const tasteScore = calculate_taste_score( + recipe.lemons, + recipe.sugar, + ideal.lemons, + ideal.sugar + ); + + const cups_available = Math.min( + game_state.supplies.lemons / recipe.lemons || 0, + game_state.supplies.sugar / recipe.sugar || 0, + game_state.supplies.ice / recipe.ice || 0, + game_state.supplies.cups + ); + + const cups_sold = calculate_cups_sold(price, cups_available, weather, tasteScore); + + const remaining_supplies = { + lemons: game_state.supplies.lemons - recipe.lemons * cups_sold, + sugar: game_state.supplies.sugar - recipe.sugar * cups_sold, + ice: game_state.supplies.ice - recipe.ice * cups_sold, + cups: game_state.supplies.cups - cups_sold + } + + const cost_per_cup = game_state.cost_per_cup; + const profit = (price - cost_per_cup) * cups_sold; + + return { + ...game_state, + player_money: game_state.player_money + profit, + supplies: remaining_supplies, + cups_sold + } +} diff --git a/grass.png b/grass.png new file mode 100644 index 0000000..0c46c7e Binary files /dev/null and b/grass.png differ diff --git a/grass_generated.png b/grass_generated.png new file mode 100644 index 0000000..58c7efd Binary files /dev/null and b/grass_generated.png differ diff --git a/ice.png b/ice.png new file mode 100644 index 0000000..84b0563 Binary files /dev/null and b/ice.png differ diff --git a/ice_generated.png b/ice_generated.png new file mode 100644 index 0000000..b03eb4c Binary files /dev/null and b/ice_generated.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..a10d7d6 --- /dev/null +++ b/index.html @@ -0,0 +1,54 @@ + + + + + Lemonade Stand + + + + + + + +
+ +

Lemonade Stand

+
+ +
+
+

Supplies

+ +
    +
  • Lemons:
  • +
  • Sugar:
  • +
  • Ice:
  • +
  • Cups:
  • +
+
+ +
+

Total Earnings

+ +

$0.00

+
+ + +
+

Current Price

+ +

$0.00

+
+ +
+

Weather

+ +

Today's weather is:

+
+
+ + + + + + diff --git a/index.js b/index.js new file mode 100644 index 0000000..19ee8c2 --- /dev/null +++ b/index.js @@ -0,0 +1,272 @@ +/** + * @fileoverview Lemonade stand game rendering module. + * Handles sprite loading and canvas rendering. + */ + +/** @type {HTMLCanvasElement} */ +const canvas = document.getElementById('scene'); + +/** @type {CanvasRenderingContext2D} */ +const ctx = canvas.getContext('2d'); + +/** @type {number} Global scale factor for rendering */ +const scale = 0.4; + +/** + * @typedef {Object} SpriteOptions + * @property {string} source - Path to the sprite image file + * @property {number} [frameWidth] - Width of each frame for animated sprites + * @property {number} [frameHeight] - Height of each frame for animated sprites + * @property {number} [frameCount=1] - Number of frames in the sprite sheet + * @property {number} [frameIndex] - Current frame index for animation + */ + +/** + * @typedef {Object} Sprite + * @property {string} source - Path to the sprite image file + * @property {HTMLImageElement} image - The loaded image element + * @property {boolean} ready - Whether the image has finished loading + * @property {number|null} frameWidth - Width of each frame (null for single-frame sprites) + * @property {number|null} frameHeight - Height of each frame (null for single-frame sprites) + * @property {number} frameCount - Total number of frames in the sprite + * @property {number} [frameIndex] - Current frame index for animation + */ + +/** + * @typedef {Object} Cup + * @property {Sprite} sprite - Reference to the cup sprite template + * @property {number} x - X position on canvas + * @property {number} y - Y position on canvas + * @property {number} scale - Render scale + * @property {number} frameIndex - Current frame (0 = empty, 1 = filled) + * @property {boolean} filled - Whether the cup is filled + * @property {Function} fill - Fills the cup + * @property {Function} empty - Empties the cup + * @property {Function} isFilled - Returns whether cup is filled + */ + +/** + * Collection of sprite templates (shared image/frame data) + * @type {Object.} + */ +const sprites = { + stand: createSprite({ source: 'stand.png' }), + cup: createSprite({ + source: 'cups.png', + frameWidth: 251, + frameHeight: 330, + frameCount: 2 + }), + maker: createSprite({ + source: 'maker.png', + frameWidth: 562 / 2, + frameHeight: 432, + frameCount: 2 + }), + ice: createSprite({ + source: 'ice.png' + }), + lemons: createSprite({ + source: 'lemons.png' + }), + grass: createSprite({ + source: 'grass.png' + }) +}; + +/** + * Creates a cup instance. + * + * @param {number} x - X position + * @param {number} y - Y position + * @param {number} [scale=0.1] - Render scale + * @returns {Cup} + */ +function createCup(x, y, scale = 0.1) { + return { + sprite: sprites.cup, + x, + y, + scale, + frameIndex: 0, + filled: false, + + /** Fills the cup */ + fill() { + this.filled = true; + this.frameIndex = 1; + }, + + /** Empties the cup */ + empty() { + this.filled = false; + this.frameIndex = 0; + }, + + /** @returns {boolean} Whether the cup is filled */ + isFilled() { + return this.filled; + } + }; +} + +// Game objects +const cups = [ + createCup(155, 250), + createCup(185, 250), + createCup(215, 250), + createCup(245, 250), + +] + +/** + * Creates a new sprite object from the given options. + * Automatically triggers a render when the image loads. + * + * @param {SpriteOptions} options - Configuration options for the sprite + * @returns {Sprite} The created sprite object + */ +function createSprite(options) { + const image = new Image(); + + const sprite = { + source: options.source, + image, + ready: false, + + // optionals + frameWidth: options.frameWidth || null, + frameHeight: options.frameHeight || null, + frameCount: options.frameCount ?? 1, + frameIndex: options.frameIndex || 0 + }; + + image.onload = () => { + sprite.ready = true; + render(); + } + + image.src = options.source; + return sprite; +} + +/** + * Draws a sprite onto the canvas at the specified position. + * + * @param {Sprite} spriteObject - The sprite to draw + * @param {number} x - The x-coordinate on the canvas + * @param {number} y - The y-coordinate on the canvas + * @param {number} scale - Scale factor for rendering + * @returns {void} + */ +function drawSprite(spriteObject, x, y, scale) { + if (!spriteObject.ready) return; + + const frameIndex = spriteObject.frameIndex; + + const isFramedSprite = spriteObject.frameWidth !== null + || spriteObject.frameHeight !== null; + + if (!isFramedSprite) { + const drawWidth = spriteObject.image.naturalWidth * scale; + const drawHeight = spriteObject.image.naturalHeight * scale; + + ctx.drawImage(spriteObject.image, x, y, drawWidth, drawHeight); + return; + } + + // framed horizontal sprite + const sourceX = frameIndex * spriteObject.frameWidth; + const sourceY = 0; + + const drawWidth = spriteObject.frameWidth * scale; + const drawHeight = spriteObject.frameHeight * scale; + + ctx.drawImage( + spriteObject.image, + sourceX, + sourceY, + spriteObject.frameWidth, + spriteObject.frameHeight, + x, + y, + drawWidth, + drawHeight + ); +} + +/** + * Draws a cup (or any object with sprite, x, y, scale, frameIndex). + * + * @param {Cup} instance - The cup to draw + * @returns {void} + */ +function drawInstance(instance) { + const { sprite, x, y, scale, frameIndex } = instance; + + if (!sprite.ready) return; + + const isFramedSprite = sprite.frameWidth !== null + || sprite.frameHeight !== null; + + if (!isFramedSprite) { + const drawWidth = sprite.image.naturalWidth * scale; + const drawHeight = sprite.image.naturalHeight * scale; + ctx.drawImage(sprite.image, x, y, drawWidth, drawHeight); + return; + } + + // framed horizontal sprite + const sourceX = frameIndex * sprite.frameWidth; + const drawWidth = sprite.frameWidth * scale; + const drawHeight = sprite.frameHeight * scale; + + ctx.drawImage( + sprite.image, + sourceX, + 0, + sprite.frameWidth, + sprite.frameHeight, + x, + y, + drawWidth, + drawHeight + ); +} + +function drawGround() { + if (!sprites.grass.ready) return; + + const pattern = ctx.createPattern(sprites.grass.image, 'repeat') + ctx.fillStyle = pattern; + + const groundHeight = 250; + ctx.fillRect(0, canvas.height - groundHeight, canvas.width, groundHeight) +} + +function render() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + drawGround(); + + drawSprite(sprites.maker, 130, 140, 0.4); + drawSprite(sprites.stand, 50, 50, 0.5); + drawSprite(sprites.ice, 250, 190, 0.125); + + + // Draw all cup instances + cups.forEach(cup => drawInstance(cup)); + + drawSprite(sprites.lemons, 60, 220, 0.1); + +} + +render(); + +// Example: fill the first cup after 1 second +setTimeout(() => { + cups[0].fill(); + sprites.maker.frameIndex = 1; + render(); + +}, 1000); diff --git a/juice.png b/juice.png new file mode 100644 index 0000000..636b932 Binary files /dev/null and b/juice.png differ diff --git a/lemons.png b/lemons.png new file mode 100644 index 0000000..8dd20e4 Binary files /dev/null and b/lemons.png differ diff --git a/maker.png b/maker.png new file mode 100644 index 0000000..6ef7dc2 Binary files /dev/null and b/maker.png differ diff --git a/person_generated.png b/person_generated.png new file mode 100644 index 0000000..0d9aee4 Binary files /dev/null and b/person_generated.png differ diff --git a/stand.jpg b/stand.jpg new file mode 100644 index 0000000..347c592 Binary files /dev/null and b/stand.jpg differ diff --git a/stand.png b/stand.png new file mode 100644 index 0000000..a20fa08 Binary files /dev/null and b/stand.png differ diff --git a/style.css b/style.css new file mode 100644 index 0000000..50dc2e5 --- /dev/null +++ b/style.css @@ -0,0 +1,66 @@ +@import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap'); + +body { + background: linear-gradient(180deg, #cfefff 0%, #f7ffe5 100%); + min-height: 100vh; + font-family: 'Inter', sans-serif; +} + +.game_header { + display: flex; + margin: 0 auto; + width: 500px; + align-items: center; +} + +.dashboard { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 20px; +} + +.game_header_title { + font-family: 'Fredoka', cursive; + font-size: 54px; + color: #FDB813; + text-shadow: 3px 3px 0px #3f7a33; + letter-spacing: 1px; +} + +.game_header_icon { + height: 64px; +} + +.game_section { + background-color: #FFF9E6; + box-sizing: border-box; + padding: 12px; + border: 4px solid #8fd16a; + border-radius: 20px; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); + color: #7A6146; +} + +.game_list { + list-style: none; + margin: 0; + padding: 0; +} + +.section_title { + font-family: 'Fredoka', sans-serif; + margin: 0; + padding: 0; + color: #5C4632; + letter-spacing: 1px; +} + +canvas { + width: 1200px; + height: 500px; + margin: 0 auto; + display: block; + pointer-events: none; +} diff --git a/tree_generated.png b/tree_generated.png new file mode 100644 index 0000000..5193a96 Binary files /dev/null and b/tree_generated.png differ diff --git a/white_stand.png b/white_stand.png new file mode 100644 index 0000000..dabe5e7 Binary files /dev/null and b/white_stand.png differ diff --git a/white_stand_1.png b/white_stand_1.png new file mode 100644 index 0000000..3640f30 Binary files /dev/null and b/white_stand_1.png differ