UNPKG

@jeremyckahn/farmhand

Version:
1,132 lines (1,005 loc) 29 kB
import { COW_FERTILIZER_PRODUCTION_RATE_FASTEST, COW_FERTILIZER_PRODUCTION_RATE_SLOWEST, COW_MAXIMUM_VALUE_MATURITY_AGE, COW_MAXIMUM_VALUE_MULTIPLIER, COW_MILK_RATE_FASTEST, COW_MILK_RATE_SLOWEST, COW_MINIMUM_VALUE_MULTIPLIER, COW_STARTING_WEIGHT_BASE, COW_STARTING_WEIGHT_VARIANCE, COW_WEIGHT_MULTIPLIER_MAXIMUM, COW_WEIGHT_MULTIPLIER_MINIMUM, I_AM_RICH_BONUSES, MALE_COW_WEIGHT_MULTIPLIER, } from '../constants.js' import { carrot, carrotSeed, corn, milk1, potato, pumpkin, rainbowFertilizer, scarecrow, silverOre, spinach, wheat, } from '../data/items.js' import { carrotSoup } from '../data/recipes.js' import { cowColors, cropLifeStage, genders, standardCowColors, } from '../enums.js' import { animals, items as itemImages } from '../img/index.js' import { testCrop } from '../test-utils/index.js' import { getCowStub } from '../test-utils/stubs/cowStub.js' import { farmProductsSold } from './farmProductsSold.js' import { isItemAFarmProduct } from './isItemAFarmProduct.js' import { levelAchieved } from './levelAchieved.js' import { canMakeRecipe, castToMoney, chooseRandom, computeMarketPositions, dollarString, experienceNeededForLevel, generateCow, generateOffspringCow, get7DayAverage, getAvailableShopInventory, getCowFertilizerProductionRate, getCowImage, getCowMilkRate, getCowValue, getCowWeight, getCropLifeStage, getFinalCropItemIdFromSeedItemId, getGrowingPhase, getItemCurrentValue, getLifeStageRange, getPlotContentFromItemId, getPlotImage, getPriceEventForCrop, getRandomLevelUpReward, getRandomUnlockedCrop, getRangeCoords, getSalePriceMultiplier, getSeedItemIdFromFinalStageCropItemId, integerString, maxYieldOfRecipe, moneyTotal, percentageString, randomChoice, transformStateDataForImport, } from './index.js' const { SEED, GROWING, GROWN } = cropLifeStage describe('moneyTotal', () => { test('adds numbers', () => { expect(moneyTotal(0.1, 0.2)).toEqual(0.3) }) test('subtracts numbers', () => { expect(moneyTotal(1000, -999.99)).toEqual(0.01) }) }) describe('castToMoney', () => { test('does not change valid money value', () => { expect(castToMoney(1.23)).toEqual(1.23) }) test('rounds up', () => { expect(castToMoney(1.235)).toEqual(1.24) }) test('rounds down', () => { expect(castToMoney(1.234)).toEqual(1.23) }) }) describe('dollarString', () => { test('formats number to dollar string', () => { expect(dollarString(1234.567)).toEqual('$1,235') }) }) describe('integerString', () => { test('formats number to integer string string', () => { expect(integerString(1234.567)).toEqual('1,235') }) }) describe('isItemAFarmProduct', () => { test.each([ ['seed', carrotSeed, false], ['crop', carrot, true], ['milk', milk1, true], ['crafted item', carrotSoup, true], ])('when item is a %s', (_itemType, item, isAFarmProduct) => { expect(isItemAFarmProduct(item)).toBe(isAFarmProduct) }) }) describe('getItemCurrentValue', () => { let valueAdjustments beforeEach(() => { valueAdjustments = { carrot: 1.5, 'rainbow-fertilizer': 1.5, } }) describe('stable value item', () => { test('computes value', () => { expect(getItemCurrentValue(carrot, valueAdjustments)).toEqual( carrot.value * 1.5 ) }) }) describe('fluctuating value item', () => { test('computes value', () => { expect(getItemCurrentValue(rainbowFertilizer, valueAdjustments)).toEqual( rainbowFertilizer.value ) }) }) }) describe('generateCow', () => { describe('randomizer: lower bound', () => { beforeEach(() => { vitest.spyOn(Math, 'random').mockReturnValue(0) }) const baseCowProperties = { color: Object.keys(standardCowColors)[0], daysOld: 1, id: '123', isBred: false, name: 'Peach', } describe('female cows', () => { test('generates a cow', () => { const baseWeight = Math.round( COW_STARTING_WEIGHT_BASE - COW_STARTING_WEIGHT_VARIANCE ) expect( generateCow({ gender: genders.FEMALE, id: '123' }) ).toMatchObject({ ...baseCowProperties, gender: genders.FEMALE, baseWeight, }) }) }) describe('male cows', () => { test('generates a cow', () => { const baseWeight = Math.round( COW_STARTING_WEIGHT_BASE * MALE_COW_WEIGHT_MULTIPLIER - COW_STARTING_WEIGHT_VARIANCE ) expect(generateCow({ gender: genders.MALE, id: '123' })).toMatchObject({ ...baseCowProperties, gender: genders.MALE, baseWeight, }) }) }) }) describe('randomizer: upper bound', () => { beforeEach(() => { vitest.spyOn(Math, 'random').mockReturnValue(1) }) test('generates a cow', () => { const baseWeight = COW_STARTING_WEIGHT_BASE * MALE_COW_WEIGHT_MULTIPLIER + COW_STARTING_WEIGHT_VARIANCE expect(generateCow({ id: '123' })).toMatchObject({ baseWeight, color: Object.keys(standardCowColors).pop(), daysOld: 1, gender: Object.keys(genders).pop(), isBred: false, name: 'Peach', }) }) }) }) describe('generateOffspringCow', () => { let maleCow, femaleCow beforeEach(() => { vitest.spyOn(Math, 'random').mockReturnValue(1) maleCow = generateCow({ baseWeight: 2200, color: standardCowColors.ORANGE, colorsInBloodline: { [standardCowColors.ORANGE]: true, [standardCowColors.YELLOW]: true, }, gender: genders.MALE, }) femaleCow = generateCow({ baseWeight: 2000, color: standardCowColors.GREEN, colorsInBloodline: { [standardCowColors.GREEN]: true, [standardCowColors.WHITE]: true, }, gender: genders.FEMALE, }) }) test('generates offspring', () => { expect(generateOffspringCow(maleCow, femaleCow, 'foo')).toMatchObject({ color: chooseRandom([femaleCow.color, maleCow.color]), colorsInBloodline: { [standardCowColors.GREEN]: true, [standardCowColors.ORANGE]: true, [standardCowColors.WHITE]: true, [standardCowColors.YELLOW]: true, }, baseWeight: 2100, isBred: true, ownerId: 'foo', originalOwnerId: 'foo', }) }) test('order of parents does not matter', () => { const idProps = { id: '123' } const { ...offspring1 } = generateOffspringCow( maleCow, femaleCow, 'foo', idProps ) const { ...offspring2 } = generateOffspringCow( femaleCow, maleCow, 'foo', idProps ) expect(offspring1).toEqual(offspring2) }) test('two cows of the same gender throw an error', () => { expect(() => generateOffspringCow(femaleCow, femaleCow, 'foo')).toThrow() }) describe('rainbow cows', () => { test('cows with all of the colors in their bloodline are rainbow cows', () => { vitest.spyOn(Math, 'random').mockReturnValue(1) maleCow = generateCow({ baseWeight: 2200, color: standardCowColors.ORANGE, colorsInBloodline: { [standardCowColors.BLUE]: true, [standardCowColors.BROWN]: true, [standardCowColors.GREEN]: true, [standardCowColors.ORANGE]: true, [standardCowColors.PURPLE]: true, [standardCowColors.WHITE]: true, }, gender: genders.MALE, }) femaleCow = generateCow({ baseWeight: 2000, color: standardCowColors.GREEN, colorsInBloodline: { [standardCowColors.YELLOW]: true, }, gender: genders.FEMALE, }) expect(generateOffspringCow(maleCow, femaleCow, 'foo')).toMatchObject({ color: cowColors.RAINBOW, colorsInBloodline: { [standardCowColors.BLUE]: true, [standardCowColors.BROWN]: true, [standardCowColors.GREEN]: true, [standardCowColors.ORANGE]: true, [standardCowColors.PURPLE]: true, [standardCowColors.WHITE]: true, [standardCowColors.YELLOW]: true, }, baseWeight: 2100, isBred: true, ownerId: 'foo', originalOwnerId: 'foo', }) }) test('rainbow color is not stored in bloodline', () => { vitest.spyOn(Math, 'random').mockReturnValue(1) maleCow = generateCow({ baseWeight: 2200, color: cowColors.RAINBOW, colorsInBloodline: { [standardCowColors.BLUE]: true, [standardCowColors.BROWN]: true, [standardCowColors.GREEN]: true, [standardCowColors.ORANGE]: true, [standardCowColors.PURPLE]: true, [standardCowColors.WHITE]: true, [standardCowColors.YELLOW]: true, }, gender: genders.FEMALE, }) femaleCow = generateCow({ baseWeight: 2000, color: standardCowColors.WHITE, colorsInBloodline: { [standardCowColors.WHITE]: true, }, gender: genders.MALE, }) const { colorsInBloodline } = generateOffspringCow( maleCow, femaleCow, 'foo' ) expect(colorsInBloodline).toEqual({ [standardCowColors.BLUE]: true, [standardCowColors.BROWN]: true, [standardCowColors.GREEN]: true, [standardCowColors.ORANGE]: true, [standardCowColors.PURPLE]: true, [standardCowColors.WHITE]: true, [standardCowColors.YELLOW]: true, }) }) }) }) describe('getCowMilkRate', () => { describe('non-female cows', () => { test('computes correct milk rate', () => { expect( getCowMilkRate( generateCow({ gender: genders.MALE, }) ) ).toEqual(Infinity) }) }) describe('female cows', () => { const baseCow = generateCow({ gender: genders.FEMALE }) describe('minimal weightMultiplier', () => { test('computes correct milk rate', () => { expect( getCowMilkRate({ ...baseCow, weightMultiplier: COW_WEIGHT_MULTIPLIER_MINIMUM, }) ).toEqual(COW_MILK_RATE_SLOWEST) }) }) describe('median weightMultiplier', () => { test('computes correct milk rate', () => { expect(getCowMilkRate({ ...baseCow, weightMultiplier: 1 })).toEqual( (COW_MILK_RATE_SLOWEST + COW_MILK_RATE_FASTEST) / 2 ) }) }) describe('maximum weightMultiplier', () => { test('computes correct milk rate', () => { expect( getCowMilkRate({ ...baseCow, weightMultiplier: COW_WEIGHT_MULTIPLIER_MAXIMUM, }) ).toEqual(COW_MILK_RATE_FASTEST) }) }) }) }) describe('getCowFertilizerProductionRate', () => { describe('non-male cows', () => { test('computes correct fertilizer production rate', () => { expect( getCowFertilizerProductionRate( generateCow({ gender: genders.FEMALE, }) ) ).toEqual(Infinity) }) }) describe('male cows', () => { const baseCow = generateCow({ gender: genders.MALE }) describe('minimal weightMultiplier', () => { test('computes correct fertilizer production rate', () => { expect( getCowFertilizerProductionRate({ ...baseCow, weightMultiplier: COW_WEIGHT_MULTIPLIER_MINIMUM, }) ).toEqual(COW_FERTILIZER_PRODUCTION_RATE_SLOWEST) }) }) describe('median weightMultiplier', () => { test('computes correct fertilizer production rate', () => { expect( getCowFertilizerProductionRate({ ...baseCow, weightMultiplier: 1 }) ).toEqual( (COW_FERTILIZER_PRODUCTION_RATE_SLOWEST + COW_FERTILIZER_PRODUCTION_RATE_FASTEST) / 2 ) }) }) describe('maximum weightMultiplier', () => { test('computes correct fertilizer production rate', () => { expect( getCowFertilizerProductionRate({ ...baseCow, weightMultiplier: COW_WEIGHT_MULTIPLIER_MAXIMUM, }) ).toEqual(COW_FERTILIZER_PRODUCTION_RATE_FASTEST) }) }) }) }) describe('getCowValue', () => { const baseWeight = 100 test('computes value of cow for sale', () => { expect(getCowValue(generateCow({ baseWeight, daysOld: 1 }))).toEqual( baseWeight * 1.5 ) }) describe('computing sale value', () => { describe('young cow (worst value)', () => { test('computes cow value', () => { expect( getCowValue(generateCow({ baseWeight, daysOld: 1 }), true) ).toEqual(baseWeight * COW_MINIMUM_VALUE_MULTIPLIER) }) }) describe('old cow (best value)', () => { test('computes cow value', () => { expect( getCowValue( generateCow({ baseWeight, daysOld: COW_MAXIMUM_VALUE_MATURITY_AGE, }), true ) ).toEqual(baseWeight * COW_MAXIMUM_VALUE_MULTIPLIER) }) }) }) }) describe('getCowWeight', () => { test('computes cow value', () => { expect( getCowWeight(generateCow({ baseWeight: 100, weightMultiplier: 2 })) ).toEqual(200) }) }) describe('getLifeStageRange', () => { test('converts a cropTimeline to an array of stages', () => { expect(getLifeStageRange([1, 2])).toEqual([SEED, GROWING, GROWING]) }) test('converts a multi-stage growing cycle into the expected stages', () => { expect(getLifeStageRange([2, 1, 2, 1])).toEqual([ SEED, SEED, GROWING, GROWING, GROWING, GROWING, ]) }) }) describe('getCropLifeStage', () => { test('maps a life cycle label to an image name chunk', () => { const itemId = 'carrot' expect(getCropLifeStage(testCrop({ itemId, daysWatered: 0 }))).toBe(SEED) expect(getCropLifeStage(testCrop({ itemId, daysWatered: 2.5 }))).toBe( GROWING ) expect(getCropLifeStage(testCrop({ itemId, daysWatered: 5 }))).toBe(GROWN) }) }) describe('getPlotImage', () => { test('returns null when no plotContent is provided', () => { // @ts-expect-error expect(getPlotImage(null)).toBe(null) }) test('returns plot images for a crop', () => { const itemId = 'carrot' // @ts-expect-error expect(getPlotImage(testCrop({ itemId, daysWatered: 0 }))).toBe( itemImages['carrot-seed'] ) // @ts-expect-error expect(getPlotImage(testCrop({ itemId, daysWatered: 1 }))).toBe( itemImages['carrot-seed'] ) // @ts-expect-error expect(getPlotImage(testCrop({ itemId, daysWatered: 3 }))).toBe( itemImages['carrot-growing-2'] ) // @ts-expect-error expect(getPlotImage(testCrop({ itemId, daysWatered: 5 }))).toBe( itemImages['carrot'] ) }) test('returns item image for oreId', () => { // @ts-expect-error expect(getPlotImage(getPlotContentFromItemId(silverOre.id))).toBe( itemImages[silverOre.id] ) }) test('returns item image for other content', () => { // @ts-expect-error expect(getPlotImage(getPlotContentFromItemId('sprinkler'))).toBe( itemImages['sprinkler'] ) }) }) describe('getRangeCoords', () => { describe('surrounded by plots', () => { test('computes the plot range', () => { expect(getRangeCoords(1, 1, 1)).toEqual([ [ { x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0 }, ], [ { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, ], [ { x: 0, y: 2 }, { x: 1, y: 2 }, { x: 2, y: 2 }, ], ]) }) }) describe('edge testing', () => { test('in-range plots below field bounds are negative', () => { expect(getRangeCoords(1, 0, 0)).toEqual([ [ { x: -1, y: -1 }, { x: 0, y: -1 }, { x: 1, y: -1 }, ], [ { x: -1, y: 0 }, { x: 0, y: 0 }, { x: 1, y: 0 }, ], [ { x: -1, y: 1 }, { x: 0, y: 1 }, { x: 1, y: 1 }, ], ]) }) }) }) describe('getFinalCropItemIdFromSeedItemId', () => { test('gets "final" crop item id from seed item id', () => { expect(getFinalCropItemIdFromSeedItemId('carrot-seed')).toEqual('carrot') }) test('gets "final" crop item id from seed item id with varieties', () => { vitest.spyOn(Math, 'random').mockReturnValue(0) expect(getFinalCropItemIdFromSeedItemId('grape-seed')).toEqual( 'grape-chardonnay' ) }) }) describe('getSeedItemIdFromFinalStageCropItemId', () => { test('gets seed item from crop item', () => { expect(getSeedItemIdFromFinalStageCropItemId('carrot')).toEqual( 'carrot-seed' ) }) test('gets seed item from crop item with varieties', () => { expect(getSeedItemIdFromFinalStageCropItemId('grape-chardonnay')).toEqual( 'grape-seed' ) }) }) describe('maxYieldOfRecipe', () => { test('returns yield for no ingredients', () => { expect( maxYieldOfRecipe({ ingredients: { 'sample-item-1': 2 } }, []) ).toEqual(0) }) test('returns yield for some ingredients', () => { expect( maxYieldOfRecipe( { ingredients: { 'sample-item-1': 2, 'sample-item-2': 2 } }, [{ id: 'sample-item-1', quantity: 2 }] ) ).toEqual(0) expect( maxYieldOfRecipe( { ingredients: { 'sample-item-1': 2, 'sample-item-2': 2 } }, [ { id: 'sample-item-1', quantity: 1 }, { id: 'sample-item-2', quantity: 2 }, ] ) ).toEqual(0) expect( maxYieldOfRecipe( { ingredients: { 'sample-item-1': 2, 'sample-item-2': 2 } }, [ { id: 'sample-item-1', quantity: 4 }, { id: 'sample-item-2', quantity: 3 }, ] ) ).toEqual(1) }) test('returns yield for all ingredients', () => { expect( maxYieldOfRecipe( { ingredients: { 'sample-item-1': 2, 'sample-item-2': 2 } }, [ { id: 'sample-item-1', quantity: 2 }, { id: 'sample-item-2', quantity: 2 }, ] ) ).toEqual(1) expect( maxYieldOfRecipe( { ingredients: { 'sample-item-1': 2, 'sample-item-2': 2 } }, [ { id: 'sample-item-1', quantity: 4 }, { id: 'sample-item-2', quantity: 4 }, ] ) ).toEqual(2) }) }) describe('canMakeRecipe', () => { describe('player does not have sufficient ingredients', () => { test('evaluates inventory correctly', () => { expect( canMakeRecipe( // @ts-expect-error { ingredients: { 'sample-item-1': 2 } }, [{ id: 'sample-item-1', quantity: 1 }], 1 ) ).toBe(false) }) }) describe('player does have sufficient ingredients', () => { test('evaluates inventory correctly', () => { expect( canMakeRecipe( // @ts-expect-error { ingredients: { 'sample-item-1': 2 } }, [{ id: 'sample-item-1', quantity: 2 }], 1 ) ).toBe(true) }) }) }) describe('getRandomUnlockedCrop', () => { test('gets a random unlocked crop', () => { vitest.spyOn(Math, 'random').mockReturnValue(1) const crop = getRandomUnlockedCrop(['carrot-seed', 'pumpkin-seed']) expect(crop.id).toEqual('pumpkin') }) test('gets a random unlocked crop with varieties', () => { vitest.spyOn(Math, 'random').mockReturnValue(0) const crop = getRandomUnlockedCrop(['grape-seed']) expect(crop.id).toEqual('grape-chardonnay') }) }) describe('getPriceEventForCrop', () => { test('returns price event', () => { expect(getPriceEventForCrop(carrot)).toEqual({ itemId: carrot.id, daysRemaining: 4, }) }) }) describe('farmProductsSold', () => { test('sums products sold', () => { expect( farmProductsSold({ [carrot.id]: 3, [carrotSeed.id]: 2, }) ).toEqual(3) }) }) describe('levelAchieved', () => { const cases = [ [1, 0], [2, 100], [2, 150], [3, 400], [100, 980100], ] test.each(cases)( `returns level %p for %p experience`, (expectedLevel, experience) => { expect(levelAchieved(experience)).toEqual(expectedLevel) } ) }) describe('experienceNeededForLevel', () => { test.each([ [0, 1], [100, 2], [400, 3], [980100, 100], ])('it returns %s experience for level %s', (experienceNeeded, levelNum) => { expect(experienceNeededForLevel(levelNum)).toEqual(experienceNeeded) }) }) describe('getAvailableShopInventory', () => { test('computes shop inventory that has been unlocked', async () => { expect( getAvailableShopInventory({ items: {}, sprinklerRange: 0, tools: {}, stageFocusType: {}, }) ).toEqual([scarecrow]) }) }) describe('getRandomLevelUpReward', () => { test('returns a crop item', async () => { vitest.spyOn(Math, 'random').mockReturnValue(0) expect(getRandomLevelUpReward(2)).toEqual(carrotSeed) }) }) describe('get7DayAverage', () => { test('calculates 7 day revenue average', () => { expect(get7DayAverage([])).toBe(0) expect(get7DayAverage([-1, -1, -1, -1, -1, -1, -1])).toBe(-1) expect(get7DayAverage([1, 1, 1, 1, 1, 1, 1])).toBe(1) expect(get7DayAverage([1, 2, 3, 4, 5, 6, 7])).toBe(4) }) }) describe('computeMarketPositions', () => { test('computes day positions', () => { expect( computeMarketPositions( { [carrot.id]: 10, [pumpkin.id]: 5, [spinach.id]: 0 }, {}, [ { id: carrot.id, quantity: 5 }, { id: pumpkin.id, quantity: 10 }, { id: spinach.id, quantity: 0 }, { id: corn.id, quantity: 10 }, ] ) ).toEqual({ [carrot.id]: -1, [pumpkin.id]: 1, [corn.id]: 1, }) expect( computeMarketPositions( {}, { [carrot.id]: 10, [pumpkin.id]: 5, [spinach.id]: 0 }, [ { id: carrot.id, quantity: 5 }, { id: pumpkin.id, quantity: 10 }, { id: spinach.id, quantity: 0 }, { id: corn.id, quantity: 10 }, ] ) ).toEqual({ [carrot.id]: -1, [pumpkin.id]: 1, [corn.id]: 1, }) expect( computeMarketPositions( { [carrot.id]: 5, [pumpkin.id]: 5, [spinach.id]: 5, [corn.id]: 0, [potato.id]: 10, }, { [carrot.id]: 10, [pumpkin.id]: 5, [spinach.id]: 0, [potato.id]: 5, [wheat.id]: 10, }, [ { id: carrot.id, quantity: 5 }, { id: pumpkin.id, quantity: 10 }, { id: spinach.id, quantity: 0 }, { id: corn.id, quantity: 0 }, { id: potato.id, quantity: 5 }, { id: wheat.id, quantity: 5 }, ] ) ).toEqual({ [pumpkin.id]: 1, [spinach.id]: -1, [potato.id]: -1, [wheat.id]: -1, }) }) }) const percentageStringTests = [ [0.5, '50%'], [0.05, '5%'], [1, '100%'], [10, '1000%'], [-0.3, '-30%'], ] describe.each(percentageStringTests)( 'percentageString', (percent, expectedString) => { test(`it converts ${percent} to a ${expectedString}`, () => { expect(percentageString(Number(percent))).toEqual(expectedString) }) } ) describe('getSalePriceMultiplier', () => { test('it returns 1 when there are no completedAchievements', () => { expect(getSalePriceMultiplier({})).toEqual(1) }) test('it returns 1 when there are no relevant completedAchievements', () => { const completedAchievements = { irrelevant: true, 'also-irrelevant': true, } expect(getSalePriceMultiplier(completedAchievements)).toEqual(1) }) const iAmRichAchievements = [ ['i-am-rich-1', 1 + I_AM_RICH_BONUSES[0]], ['i-am-rich-2', 1 + I_AM_RICH_BONUSES[1]], ['i-am-rich-3', 1 + I_AM_RICH_BONUSES[2]], ] describe.each(iAmRichAchievements)( 'with I am Rich achievements completed', (achievementId, expectedMultiplier) => { test(`${achievementId} returns ${expectedMultiplier}`, () => { const completedAchievements = { [achievementId]: true, } expect(getSalePriceMultiplier(completedAchievements)).toEqual( expectedMultiplier ) }) } ) }) describe('randomChoice', () => { const choices = [ { weight: 0.2, name: 'first-choice' }, { weight: 0.5, name: 'second-choice' }, { weight: 0.3, name: 'third-choice' }, ] beforeEach(() => { vitest.spyOn(global.Math, 'random') }) test('it returns a choice at random', () => { const choice = randomChoice(choices) expect(choices.includes(choice)).toEqual(true) }) test('it can handle the lower bound of Math.random', () => { // @ts-expect-error global.Math.random.mockReturnValueOnce(0) const choice = randomChoice(choices) expect(choice).toEqual(choices[0]) }) test('it can handle the upper bound of Math.random', () => { // @ts-expect-error global.Math.random.mockReturnValueOnce(0.99) const choice = randomChoice(choices) expect(choice).toEqual(choices[2]) }) }) describe('getCowImage', () => { test('colors a cow template image', async () => { const cow = generateCow({ color: cowColors.GREEN, id: '1' }) const image = await getCowImage(cow) // image data can viewed with https://jaredwinick.github.io/base64-image-viewer/ expect(image).toMatchSnapshot() }) test('does not modify rainbow cow image', async () => { const cow = generateCow({ color: cowColors.RAINBOW }) const image = await getCowImage(cow) expect(image).toEqual(animals.cow.rainbow) }) }) describe('transformStateDataForImport', () => { /** * @type Partial<farmhand.state> */ let state beforeEach(() => { state = { dayCount: 100, experience: 10, inventoryLimit: 1000, loanBalance: 100, money: 1234, version: '1', cowBreedingPen: { cowId1: null, cowId2: null, daysUntilBirth: -1 }, cowInventory: [], } }) test('it returns a sanitized state without version', () => { const sanitizedState = transformStateDataForImport(state) const { version, ...stateWithoutVersion } = state expect(sanitizedState).toEqual(stateWithoutVersion) }) test('it calculates experience from itemsSold if experience is 0', () => { state.experience = 0 state.itemsSold = { carrot: 5, 'carrot-seed': 10, } const sanitizedState = transformStateDataForImport(state) const { version, ...stateWithoutVersion } = state expect(sanitizedState).toEqual({ ...stateWithoutVersion, experience: 5, }) }) test.each([ // Valid state, no cowBreedingPen changes needed { cowBreedingPen: { cowId1: 'abc', cowId2: null, daysUntilBirth: 2 }, cowInventory: [getCowStub({ id: 'abc' })], expectedCowBreedingPen: { cowId1: 'abc', cowId2: null, daysUntilBirth: 2, }, }, // Valid state, no cowBreedingPen changes needed { cowBreedingPen: { cowId1: 'abc', cowId2: 'def', daysUntilBirth: 2 }, cowInventory: [getCowStub({ id: 'def' }), getCowStub({ id: 'abc' })], expectedCowBreedingPen: { cowId1: 'abc', cowId2: 'def', daysUntilBirth: 2, }, }, // Invalid state, cowBreedingPen needs to be fixed { cowBreedingPen: { cowId1: 'abc', cowId2: 'def', daysUntilBirth: 2 }, cowInventory: [], expectedCowBreedingPen: { cowId1: null, cowId2: null, daysUntilBirth: -1, }, }, // Invalid state, cowBreedingPen needs to be fixed { cowBreedingPen: { cowId1: 'abc', cowId2: null, daysUntilBirth: 2 }, cowInventory: [], expectedCowBreedingPen: { cowId1: null, cowId2: null, daysUntilBirth: -1, }, }, ])( 'fixes corrupt cowBreedingPen if needed', ({ cowBreedingPen, cowInventory, expectedCowBreedingPen }) => { Object.assign(state, { cowBreedingPen, cowInventory }) const sanitizedState = transformStateDataForImport(state) const { version, ...stateWithoutVersion } = state expect(sanitizedState).toEqual({ ...stateWithoutVersion, cowBreedingPen: expectedCowBreedingPen, }) } ) }) describe('getGrowingPhase', () => { test.each([ [0, 0], [0, 1], [1, 2], [2, 3], ])('it returns phase %s when days watered is %s', (phase, daysWatered) => { const crop = { itemId: 'potato', daysWatered } expect(getGrowingPhase(crop)).toEqual(phase) }) })