@jeremyckahn/farmhand
Version:
A farming game
1,132 lines (1,005 loc) • 29 kB
JavaScript
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)
})
})