@ -1,23 +1,17 @@
@@ -1,23 +1,17 @@
import {
Weather ,
Days ,
init _game ,
set _recipe ,
set _weather ,
set _days ,
set _price _per _cup ,
calculate _cups _sold ,
calculate _taste _score ,
make _lemonade
make _lemonade ,
calculate _supply _cost ,
get _supply _pricing ,
calculate _cost _per _cup
} from './game.js' ;
// Helpers
const withMockedRandom = ( value , fn ) => {
const orig = Math . random ;
Math . random = ( ) => value ;
try { fn ( ) ; } finally { Math . random = orig ; }
} ;
describe ( 'enums' , ( ) => {
test ( 'Weather is frozen and has expected values' , ( ) => {
expect ( Object . isFrozen ( Weather ) ) . toBe ( true ) ;
@ -29,30 +23,19 @@ describe('enums', () => {
@@ -29,30 +23,19 @@ describe('enums', () => {
} ) ;
} ) ;
test ( 'Days is frozen and contains allowed values' , ( ) => {
expect ( Object . isFrozen ( Days ) ) . toBe ( true ) ;
expect ( Days [ 7 ] ) . toBe ( 7 ) ;
expect ( Days [ 14 ] ) . toBe ( 14 ) ;
expect ( Days [ 30 ] ) . toBe ( 30 ) ;
expect ( Object . values ( Days ) . sort ( ( a , b ) => a - b ) ) . toEqual ( [ 7 , 14 , 30 ] ) ;
} ) ;
} ) ;
describe ( 'init_game' , ( ) => {
test ( 'returns correct default state shape and values' , ( ) => {
const s = init _game ( ) ;
expect ( s . player _money ) . toBe ( 0 ) ;
expect ( s . player _money ) . toBe ( 2.00 ) ;
expect ( s . recipe ) . toEqual ( { lemons : 0 , sugar : 0 , ice : 0 } ) ;
expect ( s . supplies ) . toEqual ( { lemons : 0 , sugar : 0 , ice : 0 , cups : 0 } ) ;
expect ( s . weather ) . toBe ( Weather . SUNNY ) ;
expect ( s . days ) . toBe ( 7 ) ;
expect ( s . price _per _cup ) . toBe ( 1.0 ) ;
expect ( s . cups _sold ) . toBe ( 0 ) ;
expect ( s . supplies _prices . lemons ) . toHaveProperty ( '12' , 4.80 ) ;
expect ( s . supplies _prices . sugar ) . toHaveProperty ( '50' , 15.00 ) ;
expect ( s . supplies _prices . ice ) . toHaveProperty ( '500' , 5.00 ) ;
expect ( s . supplies _prices . cups ) . toHaveProperty ( '400' , 3.75 ) ;
expect ( s . current _day ) . toBe ( 1 ) ;
expect ( s . total _earnings ) . toBe ( 0 ) ;
} ) ;
test ( 'does not share nested object references across calls' , ( ) => {
@ -61,7 +44,6 @@ describe('init_game', () => {
@@ -61,7 +44,6 @@ describe('init_game', () => {
expect ( a ) . not . toBe ( b ) ;
expect ( a . recipe ) . not . toBe ( b . recipe ) ;
expect ( a . supplies ) . not . toBe ( b . supplies ) ;
expect ( a . supplies _prices ) . not . toBe ( b . supplies _prices ) ;
} ) ;
} ) ;
@ -77,55 +59,34 @@ describe('set_recipe', () => {
@@ -77,55 +59,34 @@ describe('set_recipe', () => {
} ) ;
} ) ;
describe ( 'set_days' , ( ) => {
test ( 'sets valid days and returns new state' , ( ) => {
const s1 = init _game ( ) ;
const s2 = set _days ( s1 , 14 ) ;
expect ( s2 ) . not . toBe ( s1 ) ;
expect ( s2 . days ) . toBe ( 14 ) ;
expect ( s1 . days ) . toBe ( 7 ) ;
} ) ;
} ) ;
describe ( 'set_weather' , ( ) => {
test ( 'selects CLOUDY for roll < 40%' , ( ) => {
const s = init _game ( ) ;
withMockedRandom ( 0.00 , ( ) => {
expect ( set _weather ( s ) . weather ) . toBe ( Weather . CLOUDY ) ;
} ) ;
withMockedRandom ( 0.39 , ( ) => {
expect ( set _weather ( s ) . weather ) . toBe ( Weather . CLOUDY ) ;
} ) ;
expect ( set _weather ( s , 0.00 ) . weather ) . toBe ( Weather . CLOUDY ) ;
expect ( set _weather ( s , 0.39 ) . weather ) . toBe ( Weather . CLOUDY ) ;
} ) ;
test ( 'selects SUNNY for 40%-75%' , ( ) => {
const s = init _game ( ) ;
withMockedRandom ( 0.40 , ( ) => {
expect ( set _weather ( s ) . weather ) . toBe ( Weather . SUNNY ) ;
} ) ;
withMockedRandom ( 0.74 , ( ) => {
expect ( set _weather ( s ) . weather ) . toBe ( Weather . SUNNY ) ;
} ) ;
expect ( set _weather ( s , 0.40 ) . weather ) . toBe ( Weather . SUNNY ) ;
expect ( set _weather ( s , 0.74 ) . weather ) . toBe ( Weather . SUNNY ) ;
} ) ;
test ( 'selects HOT for 75%-90%' , ( ) => {
const s = init _game ( ) ;
withMockedRandom ( 0.75 , ( ) => {
expect ( set _weather ( s ) . weather ) . toBe ( Weather . HOT ) ;
} ) ;
withMockedRandom ( 0.89 , ( ) => {
expect ( set _weather ( s ) . weather ) . toBe ( Weather . HOT ) ;
} ) ;
expect ( set _weather ( s , 0.75 ) . weather ) . toBe ( Weather . HOT ) ;
expect ( set _weather ( s , 0.89 ) . weather ) . toBe ( Weather . HOT ) ;
} ) ;
test ( 'selects COLD for 90%-100%' , ( ) => {
const s = init _game ( ) ;
withMockedRandom ( 0.90 , ( ) => {
expect ( set _weather ( s ) . weather ) . toBe ( Weather . COLD ) ;
} ) ;
withMockedRandom ( 0.9999 , ( ) => {
expect ( set _weather ( s ) . weather ) . toBe ( Weather . COLD ) ;
expect ( set _weather ( s , 0.90 ) . weather ) . toBe ( Weather . COLD ) ;
expect ( set _weather ( s , 0.9999 ) . weather ) . toBe ( Weather . COLD ) ;
} ) ;
test ( 'returns fallback weather if loop completes' , ( ) => {
const s = init _game ( ) ;
expect ( set _weather ( s , 1.0 ) . weather ) . toBe ( Weather . SUNNY ) ;
} ) ;
} ) ;
@ -135,9 +96,7 @@ describe('calculate_taste_score', () => {
@@ -135,9 +96,7 @@ describe('calculate_taste_score', () => {
} ) ;
test ( 'penalizes lemon and sugar diffs correctly' , ( ) => {
// lemons off by 1 => -0.3
expect ( calculate _taste _score ( 2 , 1 , 1 , 1 ) ) . toBeCloseTo ( 0.7 ) ;
// sugar off by 2 => -0.4
expect ( calculate _taste _score ( 1 , 3 , 1 , 1 ) ) . toBeCloseTo ( 0.6 ) ;
} ) ;
@ -149,96 +108,107 @@ describe('calculate_taste_score', () => {
@@ -149,96 +108,107 @@ describe('calculate_taste_score', () => {
describe ( 'calculate_cups_sold' , ( ) => {
test ( 'caps at available cups and never negative' , ( ) => {
withMockedRandom ( 0 , ( ) => {
const sold = calculate _cups _sold ( 0.35 , 5 , Weather . SUNNY , 1 ) ;
const sold = calculate _cups _sold ( 0.35 , 5 , Weather . SUNNY , 1 , 0 ) ;
expect ( sold ) . toBeGreaterThanOrEqual ( 0 ) ;
expect ( Number . isInteger ( sold ) ) . toBe ( true ) ;
expect ( sold ) . toBeLessThanOrEqual ( 5 ) ;
} ) ;
} ) ;
test ( 'returns 0 when price_effect would go negative' , ( ) => {
withMockedRandom ( 0 , ( ) => {
expect ( calculate _cups _sold ( 999 , 100 , Weather . SUNNY , 1 ) ) . toBe ( 0 ) ;
} ) ;
expect ( calculate _cups _sold ( 999 , 100 , Weather . SUNNY , 1 , 0 ) ) . toBe ( 0 ) ;
} ) ;
test ( 'weather factor ordering (HOT >= SUNNY >= CLOUDY >= COLD) with same random roll' , ( ) => {
withMockedRandom ( 0 , ( ) => {
const cups = 10_000 ;
const taste = 1 ;
const hot = calculate _cups _sold ( 0.50 , cups , Weather . HOT , taste ) ;
const sunny = calculate _cups _sold ( 0.35 , cups , Weather . SUNNY , taste ) ;
const cloudy = calculate _cups _sold ( 0.30 , cups , Weather . CLOUDY , taste ) ;
const cold = calculate _cups _sold ( 0.20 , cups , Weather . COLD , taste ) ;
const random = 0.5 ;
const hot = calculate _cups _sold ( 0.50 , cups , Weather . HOT , taste , random ) ;
const sunny = calculate _cups _sold ( 0.35 , cups , Weather . SUNNY , taste , random ) ;
const cloudy = calculate _cups _sold ( 0.30 , cups , Weather . CLOUDY , taste , random ) ;
const cold = calculate _cups _sold ( 0.20 , cups , Weather . COLD , taste , random ) ;
expect ( hot ) . toBeGreaterThanOrEqual ( sunny ) ;
expect ( sunny ) . toBeGreaterThanOrEqual ( cloudy ) ;
expect ( cloudy ) . toBeGreaterThanOrEqual ( cold ) ;
} ) ;
test ( 'same inputs produce same output (pure function)' , ( ) => {
const a = calculate _cups _sold ( 0.35 , 100 , Weather . SUNNY , 1 , 0.5 ) ;
const b = calculate _cups _sold ( 0.35 , 100 , Weather . SUNNY , 1 , 0.5 ) ;
expect ( a ) . toBe ( b ) ;
} ) ;
} ) ;
describe ( 'make_lemonade' , ( ) => {
test ( 'updates money and supplies based on cups_sold (stub randomness via Math.random)' , ( ) => {
test ( 'updates money and supplies based on cups_sold' , ( ) => {
const s = {
... init _game ( ) ,
weather : Weather . SUNNY ,
price _per _cup : 1.00 ,
supplies : { lemons : 100 , sugar : 100 , ice : 300 , cups : 100 } ,
recipe : { lemons : 1 , sugar : 1 , ice : 3 } ,
player _money : 10
player _money : 10 ,
total _earnings : 0
} ;
withMockedRandom ( 0 , ( ) => {
const next = make _lemonade ( s ) ;
expect ( next ) . not . toBe ( s ) ;
expect ( next . cups _sold ) . toBeGreaterThanOrEqual ( 0 ) ;
expect ( next . cups _sold ) . toBeLessThanOrEqual ( 100 ) ;
// Inventory decreases consistently
expect ( next . supplies . cups ) . toBe ( 100 - next . cups _sold ) ;
expect ( next . supplies . lemons ) . toBe ( 100 - 1 * next . cups _sold ) ;
expect ( next . supplies . sugar ) . toBe ( 100 - 1 * next . cups _sold ) ;
expect ( next . supplies . ice ) . toBe ( 300 - 3 * next . cups _sold ) ;
// Profit
const expectedMoney = 10 + 1.00 * next . cups _sold ;
expect ( next . player _money ) . toBeCloseTo ( expectedMoney ) ;
} ) ;
expect ( next . total _earnings ) . toBe ( 1.00 * next . cups _sold ) ;
} ) ;
test ( 'can produce negative profit when price < price_per_cup ' , ( ) => {
const s = {
test ( 'accumulates total_earnings across multiple days ' , ( ) => {
let state = {
... init _game ( ) ,
weather : Weather . SUNNY ,
price _per _cup : 0.2 5 ,
supplies : { lemons : 1 00, sugar : 1 00, ice : 3 00, cups : 1 00 } ,
price _per _cup : 0.3 5 ,
supplies : { lemons : 2 00, sugar : 2 00, ice : 6 00, cups : 2 00 } ,
recipe : { lemons : 1 , sugar : 1 , ice : 3 } ,
player _money : 10
player _money : 0 ,
total _earnings : 5.00
} ;
withMockedRandom ( 0 , ( ) => {
const next = make _lemonade ( s ) ;
expect ( next . player _money ) . toBe ( 19.25 ) ;
} ) ;
const next = make _lemonade ( state ) ;
const profit = 0.35 * next . cups _sold ;
expect ( next . total _earnings ) . toBeCloseTo ( 5.00 + profit ) ;
} ) ;
test ( 'recipe with zero ingredient should not allow sales (expected 0 cups sold) ' , ( ) => {
test ( 'recipe with zero lemons should not allow sales ' , ( ) => {
const s = {
... init _game ( ) ,
weather : Weather . SUNNY ,
price _per _cup : 1.00 ,
price _per _cup : 0.10 ,
supplies : { lemons : 0 , sugar : 100 , ice : 300 , cups : 100 } ,
recipe : { lemons : 0 , sugar : 1 , ice : 3 } ,
player _money : 0
} ;
withMockedRandom ( 0 , ( ) => {
const next = make _lemonade ( s ) ;
expect ( next . cups _sold ) . toBe ( 0 ) ;
} ) ;
test ( 'limited by available supplies' , ( ) => {
const s = {
... init _game ( ) ,
weather : Weather . SUNNY ,
price _per _cup : 0.35 ,
supplies : { lemons : 5 , sugar : 100 , ice : 300 , cups : 100 } ,
recipe : { lemons : 1 , sugar : 1 , ice : 3 } ,
player _money : 0
} ;
const next = make _lemonade ( s ) ;
expect ( next . cups _sold ) . toBeLessThanOrEqual ( 5 ) ;
} ) ;
} ) ;
@ -250,7 +220,7 @@ describe('set_price_per_cup', () => {
@@ -250,7 +220,7 @@ describe('set_price_per_cup', () => {
expect ( next ) . not . toBe ( state ) ;
expect ( next . price _per _cup ) . toBe ( 1.25 ) ;
expect ( state . price _per _cup ) . toBe ( 1.00 ) ; // original unchanged
expect ( state . price _per _cup ) . toBe ( 1.00 ) ;
} ) ;
test ( 'rounds to two decimal places' , ( ) => {
@ -272,18 +242,96 @@ describe('set_price_per_cup', () => {
@@ -272,18 +242,96 @@ describe('set_price_per_cup', () => {
const next = set _price _per _cup ( state , 0.75 ) ;
expect ( next . days ) . toBe ( state . days ) ;
expect ( next . weather ) . toBe ( state . weather ) ;
expect ( next . supplies ) . toBe ( state . supplies ) ; // same reference, unchanged
expect ( next . supplies ) . toBe ( state . supplies ) ;
expect ( next . recipe ) . toBe ( state . recipe ) ;
} ) ;
test ( 'rounding edge cases follow JS rounding behavior' , ( ) => {
const state = init _game ( ) ;
// JS floating-point quirk: this rounds DOWN
expect ( set _price _per _cup ( state , 1.005 ) . price _per _cup ) . toBe ( 1.00 ) ;
expect ( set _price _per _cup ( state , 1.006 ) . price _per _cup ) . toBe ( 1.01 ) ;
} ) ;
} ) ;
describe ( 'calculate_supply_cost' , ( ) => {
test ( 'returns 0 for quantity <= 0' , ( ) => {
expect ( calculate _supply _cost ( 'lemons' , 0 ) ) . toBe ( 0 ) ;
expect ( calculate _supply _cost ( 'lemons' , - 5 ) ) . toBe ( 0 ) ;
} ) ;
test ( 'uses tier 1 pricing for lemons 1-50' , ( ) => {
expect ( calculate _supply _cost ( 'lemons' , 1 ) ) . toBe ( 0.02 ) ;
expect ( calculate _supply _cost ( 'lemons' , 50 ) ) . toBe ( 1.00 ) ;
} ) ;
test ( 'uses tier 2 pricing for lemons 51-100' , ( ) => {
expect ( calculate _supply _cost ( 'lemons' , 51 ) ) . toBe ( 0.92 ) ;
expect ( calculate _supply _cost ( 'lemons' , 100 ) ) . toBe ( 1.80 ) ;
} ) ;
test ( 'uses tier 3 pricing for lemons 101+' , ( ) => {
expect ( calculate _supply _cost ( 'lemons' , 101 ) ) . toBe ( 1.52 ) ;
expect ( calculate _supply _cost ( 'lemons' , 200 ) ) . toBe ( 3.00 ) ;
} ) ;
test ( 'calculates sugar pricing correctly' , ( ) => {
expect ( calculate _supply _cost ( 'sugar' , 50 ) ) . toBe ( 0.50 ) ;
expect ( calculate _supply _cost ( 'sugar' , 100 ) ) . toBe ( 0.90 ) ;
} ) ;
test ( 'calculates ice pricing correctly' , ( ) => {
expect ( calculate _supply _cost ( 'ice' , 100 ) ) . toBe ( 1.00 ) ;
expect ( calculate _supply _cost ( 'ice' , 300 ) ) . toBe ( 2.70 ) ;
} ) ;
test ( 'calculates cups pricing correctly' , ( ) => {
expect ( calculate _supply _cost ( 'cups' , 100 ) ) . toBe ( 1.00 ) ;
expect ( calculate _supply _cost ( 'cups' , 200 ) ) . toBe ( 1.80 ) ;
} ) ;
} ) ;
describe ( 'get_supply_pricing' , ( ) => {
test ( 'returns pricing tiers for lemons' , ( ) => {
const tiers = get _supply _pricing ( 'lemons' ) ;
expect ( tiers ) . toHaveLength ( 3 ) ;
expect ( tiers [ 0 ] ) . toEqual ( { min : 1 , max : 50 , price : 0.02 } ) ;
} ) ;
test ( 'returns pricing tiers for cups' , ( ) => {
const tiers = get _supply _pricing ( 'cups' ) ;
expect ( tiers ) . toHaveLength ( 2 ) ;
expect ( tiers [ 1 ] . max ) . toBe ( Infinity ) ;
} ) ;
test ( 'returns empty array for unknown item (triggers assertion warning)' , ( ) => {
expect ( get _supply _pricing ( 'bananas' ) ) . toEqual ( [ ] ) ;
} ) ;
} ) ;
describe ( 'calculate_cost_per_cup' , ( ) => {
test ( 'calculates cost from recipe ingredients' , ( ) => {
const state = init _game ( ) ;
const recipe = { lemons : 1 , sugar : 1 , ice : 3 } ;
const result = calculate _cost _per _cup ( state , recipe ) ;
// 1*0.02 + 1*0.01 + 3*0.01 + 0.01 (cup) = 0.07
expect ( result . cost _per _cup ) . toBe ( 0.07 ) ;
} ) ;
test ( 'returns 0.01 for empty recipe (just cup cost)' , ( ) => {
const state = init _game ( ) ;
const recipe = { lemons : 0 , sugar : 0 , ice : 0 } ;
const result = calculate _cost _per _cup ( state , recipe ) ;
expect ( result . cost _per _cup ) . toBe ( 0.01 ) ;
} ) ;
test ( 'does not mutate original state' , ( ) => {
const state = init _game ( ) ;
const recipe = { lemons : 2 , sugar : 2 , ice : 4 } ;
const result = calculate _cost _per _cup ( state , recipe ) ;
expect ( result ) . not . toBe ( state ) ;
expect ( state . cost _per _cup ) . toBe ( 0 ) ;
} ) ;
} ) ;