You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
414 lines
11 KiB
414 lines
11 KiB
/** |
|
* Enum representing possible weather conditions in the game. |
|
* @readonly |
|
* @enum {string} |
|
*/ |
|
export 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} |
|
*/ |
|
export 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 |
|
} |
|
} |
|
|
|
/** |
|
* Tiered pricing structure for supplies. |
|
* Price per unit decreases with larger quantities. |
|
* @type {Object.<string, Array.<{min: number, max: number, price: number}>>} |
|
*/ |
|
const SupplyPricing = { |
|
lemons: [ |
|
{ min: 1, max: 50, price: 0.02 }, |
|
{ min: 51, max: 100, price: 0.018 }, |
|
{ min: 101, max: Infinity, price: 0.015 } |
|
], |
|
sugar: [ |
|
{ min: 1, max: 50, price: 0.01 }, |
|
{ min: 51, max: 100, price: 0.009 }, |
|
{ min: 101, max: Infinity, price: 0.008 } |
|
], |
|
ice: [ |
|
{ min: 1, max: 100, price: 0.01 }, |
|
{ min: 101, max: 300, price: 0.009 }, |
|
{ min: 301, max: Infinity, price: 0.008 } |
|
], |
|
cups: [ |
|
{ min: 1, max: 100, price: 0.01 }, |
|
{ min: 101, max: Infinity, price: 0.009 } |
|
] |
|
}; |
|
|
|
/** |
|
* Calculate the cost of purchasing a supply item based on tiered pricing. |
|
* @param {string} item - The supply type (lemons, sugar, ice, cups) |
|
* @param {number} quantity - The quantity to purchase |
|
* @returns {number} The total cost |
|
*/ |
|
export function calculate_supply_cost(item, quantity) { |
|
if (quantity <= 0) return 0; |
|
const tiers = SupplyPricing[item]; |
|
if (!tiers) return 0; |
|
const tier = tiers.find(t => quantity >= t.min && quantity <= t.max); |
|
return tier ? Math.round(quantity * tier.price * 100) / 100 : 0; |
|
} |
|
|
|
/** |
|
* Get the pricing tiers for a supply item. |
|
* @param {string} item - The supply type |
|
* @returns {Array.<{min: number, max: number, price: number}>} The pricing tiers |
|
*/ |
|
export function get_supply_pricing(item) { |
|
return SupplyPricing[item] || []; |
|
} |
|
|
|
/** |
|
* 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} 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. |
|
*/ |
|
export function init_game() { |
|
return { |
|
player_money: 25.00, |
|
recipe: { |
|
lemons: 0, |
|
sugar: 0, |
|
ice: 0 |
|
}, |
|
supplies: { |
|
lemons: 0, |
|
sugar: 0, |
|
ice: 0, |
|
cups: 0 |
|
}, |
|
weather: Weather.SUNNY, |
|
price_per_cup: 1.00, |
|
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 |
|
} |
|
}, |
|
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. |
|
*/ |
|
export 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. |
|
*/ |
|
export 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. |
|
*/ |
|
export 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. |
|
*/ |
|
export function set_price_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, |
|
price_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). |
|
*/ |
|
export 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. |
|
*/ |
|
export 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; |
|
|
|
if (lemon_diff === 0 && sugar_diff === 0) { |
|
score += 0.2; // perfect recipe bonus |
|
} else { |
|
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; |
|
} |
|
|
|
/** |
|
* 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. |
|
*/ |
|
export 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( |
|
recipe.lemons > 0 ? game_state.supplies.lemons / recipe.lemons : 0, |
|
recipe.sugar > 0 ? game_state.supplies.sugar / recipe.sugar : 0, |
|
recipe.ice > 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 profit = price * cups_sold; |
|
|
|
return { |
|
...game_state, |
|
player_money: game_state.player_money + profit, |
|
supplies: remaining_supplies, |
|
cups_sold |
|
} |
|
}
|
|
|