Browse Source

initial commit

master
Stephanie Gredell 3 weeks ago
commit
a1c2b6abef
  1. BIN
      Gemini_Generated_Image_60ejy60ejy60ejy6.png
  2. BIN
      Gemini_Generated_Image_60mila60mila60mi.png
  3. BIN
      Gemini_Generated_Image_bw4vqvbw4vqvbw4v.png
  4. BIN
      Gemini_Generated_Image_d2uiz4d2uiz4d2ui.png
  5. BIN
      Gemini_Generated_Image_kefpnykefpnykefp.png
  6. BIN
      cups.png
  7. BIN
      customer_right.png
  8. 389
      game.js
  9. BIN
      grass.png
  10. BIN
      grass_generated.png
  11. BIN
      ice.png
  12. BIN
      ice_generated.png
  13. 54
      index.html
  14. 272
      index.js
  15. BIN
      juice.png
  16. BIN
      lemons.png
  17. BIN
      maker.png
  18. BIN
      person_generated.png
  19. BIN
      stand.jpg
  20. BIN
      stand.png
  21. 66
      style.css
  22. BIN
      tree_generated.png
  23. BIN
      white_stand.png
  24. BIN
      white_stand_1.png

BIN
Gemini_Generated_Image_60ejy60ejy60ejy6.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

BIN
Gemini_Generated_Image_60mila60mila60mi.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 KiB

BIN
Gemini_Generated_Image_bw4vqvbw4vqvbw4v.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 KiB

BIN
Gemini_Generated_Image_d2uiz4d2uiz4d2ui.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

BIN
Gemini_Generated_Image_kefpnykefpnykefp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 KiB

BIN
cups.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
customer_right.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

389
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.<string, number>}
*/
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.<string, number>}
*/
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.<string, {lemons: number, sugar: number, ice: number}>}
*/
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.<number, number>} 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
}
}

BIN
grass.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
grass_generated.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
ice.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

BIN
ice_generated.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 KiB

54
index.html

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<title>Lemonade Stand</title>
<link rel="stylesheet" type="text/css" href="style.css" />
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<header class="game_header">
<img src="juice.png" class="game_header_icon">
<h1 class="game_header_title">Lemonade Stand</h1>
</header>
<div class="dashboard">
<section class="game_section">
<h2 class="section_title">Supplies</h2>
<ul class="game_list">
<li>Lemons: </li>
<li>Sugar: </li>
<li>Ice: </li>
<li>Cups: </li>
</ul>
</section>
<section class="game_section">
<h2 class="section_title">Total Earnings</h2>
<p>$0.00</p>
</section>
<section class="game_section">
<h2 class="section_title">Current Price</h2>
<p>$0.00</p>
</section>
<section class="game_section">
<h2 class="section_title">Weather</h2>
<p>Today's weather is: </p>
</section>
</div>
<canvas id="scene" width="1200" height="500"></canvas>
<script src="index.js"></script>
</body>
</html>

272
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.<string, Sprite>}
*/
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);

BIN
juice.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
lemons.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

BIN
maker.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

BIN
person_generated.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 KiB

BIN
stand.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
stand.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

66
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;
}

BIN
tree_generated.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

BIN
white_stand.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 KiB

BIN
white_stand_1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Loading…
Cancel
Save