Skip to content

Commit

Permalink
refactor: [closes #400] Consolidate random number logic (#409)
Browse files Browse the repository at this point in the history
* refactor: [closes #400] move isRandomNumberLessThan to RandomNumberService
* fix: use correct JSDoc annotation for colorizeCowTemplate
* refactor: remove unnecessary utils mock
  • Loading branch information
jeremyckahn committed Apr 18, 2023
1 parent f0eeef9 commit d214631
Show file tree
Hide file tree
Showing 17 changed files with 99 additions and 78 deletions.
1 change: 1 addition & 0 deletions .github/workflows/update-wiki.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name: Update Wiki

# TODO: Consider rolling this into run-release.yml
# https://github.com/jeremyckahn/farmhand/issues/407

on:
workflow_dispatch:
Expand Down
10 changes: 10 additions & 0 deletions src/common/services/randomNumber.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ export class RandomNumberService {
unseedRandomNumber() {
this.seededRandom = null
}

/**
* Compares given number against a randomly generated number.
* @param {number} chance Float between 0-1 to compare dice roll against.
* @returns {boolean} True if the dice roll was equal to or lower than the
* given chance, false otherwise.
*/
isRandomNumberLessThan(chance) {
return this.generateRandomNumber() <= chance
}
}

export const randomNumberService = new RandomNumberService()
29 changes: 29 additions & 0 deletions src/common/services/randomNumber.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { randomNumberService } from './randomNumber'

describe('RandomNumberService', () => {
describe('isRandomNumberLessThan', () => {
const chance = 0.6

beforeEach(() => {
jest.spyOn(randomNumberService, 'generateRandomNumber')
})

test('it returns true when random number is below chance', () => {
randomNumberService.generateRandomNumber.mockReturnValue(chance - 0.01)

expect(randomNumberService.isRandomNumberLessThan(chance)).toEqual(true)
})

test('it returns true when random number is same as chance', () => {
randomNumberService.generateRandomNumber.mockReturnValue(chance)

expect(randomNumberService.isRandomNumberLessThan(chance)).toEqual(true)
})

test('it returns false when random number is above chance', () => {
randomNumberService.generateRandomNumber.mockReturnValue(chance + 0.01)

expect(randomNumberService.isRandomNumberLessThan(chance)).toEqual(false)
})
})
})
6 changes: 4 additions & 2 deletions src/factories/ResourceFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
COAL_SPAWN_CHANCE,
STONE_SPAWN_CHANCE,
} from '../constants'
import { isRandomNumberLessThan, randomChoice } from '../utils'
import { randomChoice } from '../utils'

import { randomNumberService } from '../common/services/randomNumber'

import OreFactory from './OreFactory'
import CoalFactory from './CoalFactory'
Expand Down Expand Up @@ -116,7 +118,7 @@ export default class ResourceFactory {
default:
}

if (isRandomNumberLessThan(spawnChance)) {
if (randomNumberService.isRandomNumberLessThan(spawnChance)) {
const opt = randomChoice(this.resourceOptions)
const factory = ResourceFactory.getFactoryForItemType(opt.itemType)

Expand Down
16 changes: 10 additions & 6 deletions src/factories/ResourceFactory.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { randomNumberService } from '../common/services/randomNumber'
import { itemType, toolLevel } from '../enums'
import { isRandomNumberLessThan, randomChoice } from '../utils'
import { randomChoice } from '../utils'

import ResourceFactory from './ResourceFactory'

Expand All @@ -10,23 +11,26 @@ jest.mock('./StoneFactory')
jest.mock('../utils', () => ({
...jest.requireActual('../utils'),
randomChoice: jest.fn(),
isRandomNumberLessThan: jest.fn(),
}))

beforeEach(() => {
jest.spyOn(randomNumberService, 'isRandomNumberLessThan')
})

describe('ResourceFactory', () => {
let shovelLevel = toolLevel.DEFAULT

describe('generateResources', () => {
test('does not spawn any resources when dice roll is above resource spawn chance', () => {
isRandomNumberLessThan.mockReturnValue(false)
randomNumberService.isRandomNumberLessThan.mockReturnValue(false)

expect(ResourceFactory.instance().generateResources(shovelLevel)).toEqual(
[]
)
})

test('it can use the ore factory to generate ore', () => {
isRandomNumberLessThan.mockReturnValue(true)
randomNumberService.isRandomNumberLessThan.mockReturnValue(true)
randomChoice.mockReturnValueOnce({ itemType: itemType.ORE })

ResourceFactory.instance().generateResources(shovelLevel)
Expand All @@ -36,7 +40,7 @@ describe('ResourceFactory', () => {
})

test('it can use the coal factory to generate coal', () => {
isRandomNumberLessThan.mockReturnValue(true)
randomNumberService.isRandomNumberLessThan.mockReturnValue(true)
randomChoice.mockReturnValueOnce({ itemType: itemType.FUEL })

ResourceFactory.instance().generateResources(shovelLevel)
Expand All @@ -46,7 +50,7 @@ describe('ResourceFactory', () => {
})

test('it can use the stone factory to generate stone', () => {
isRandomNumberLessThan.mockReturnValue(true)
randomNumberService.isRandomNumberLessThan.mockReturnValue(true)
randomChoice.mockReturnValueOnce({ itemType: itemType.STONE })

ResourceFactory.instance().generateResources(shovelLevel)
Expand Down
4 changes: 2 additions & 2 deletions src/factories/StoneFactory.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { coal, stone } from '../data/ores'
import { COAL_SPAWN_CHANCE } from '../constants'
import { isRandomNumberLessThan } from '../utils'
import { randomNumberService } from '../common/services/randomNumber'

/**
* Resource factory used for spawning stone
Expand All @@ -16,7 +16,7 @@ export default class StoneFactory {

resources.push(this.spawnStone())

if (isRandomNumberLessThan(COAL_SPAWN_CHANCE)) {
if (randomNumberService.isRandomNumberLessThan(COAL_SPAWN_CHANCE)) {
resources.push(this.spawnCoal())
}

Expand Down
9 changes: 5 additions & 4 deletions src/factories/StoneFactory.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { randomNumberService } from '../common/services/randomNumber'
import { coal, stone } from '../data/ores'

import { isRandomNumberLessThan } from '../utils'

import StoneFactory from './StoneFactory'

jest.mock('../utils/isRandomNumberLessThan')
beforeEach(() => {
jest.spyOn(randomNumberService, 'isRandomNumberLessThan')
})

describe('StoneFactory', () => {
describe('generate', () => {
Expand All @@ -21,7 +22,7 @@ describe('StoneFactory', () => {
})

test('it generates a coal along with the stone when random chance passes', () => {
isRandomNumberLessThan.mockReturnValue(true)
randomNumberService.isRandomNumberLessThan.mockReturnValue(true)
const resources = stoneFactory.generate()

expect(resources).toEqual([stone, coal])
Expand Down
11 changes: 8 additions & 3 deletions src/game-logic/reducers/applyCrows.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { random } from '../../common/utils'
import { doesPlotContainCrop, isRandomNumberLessThan } from '../../utils'
import { doesPlotContainCrop } from '../../utils'
import { CROW_CHANCE, MAX_CROWS } from '../../constants'
import { CROWS_DESTROYED } from '../../templates'

import { randomNumberService } from '../../common/services/randomNumber'

import { modifyFieldPlotAt } from './modifyFieldPlotAt'
import { fieldHasScarecrow } from './helpers'

Expand Down Expand Up @@ -30,7 +32,10 @@ export function forEachPlot(state, callback) {
export const applyCrows = state => {
const { field, purchasedField } = state

if (fieldHasScarecrow(field) || isRandomNumberLessThan(1 - CROW_CHANCE)) {
if (
fieldHasScarecrow(field) ||
randomNumberService.isRandomNumberLessThan(1 - CROW_CHANCE)
) {
return state
}

Expand All @@ -39,7 +44,7 @@ export const applyCrows = state => {
let notificationMessages = []
const plotsWithCrops = []

forEachPlot(state, (plotContents, x, y) => {
forEachPlot(state, (_plotContents, x, y) => {
if (doesPlotContainCrop(state.field[y][x])) {
plotsWithCrops.push({ x, y })
}
Expand Down
17 changes: 9 additions & 8 deletions src/game-logic/reducers/applyCrows.test.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { findInField, isRandomNumberLessThan } from '../../utils'
import { findInField } from '../../utils'
import { MAX_CROWS, SCARECROW_ITEM_ID } from '../../constants'

import { applyCrows, forEachPlot } from './applyCrows'
import { randomNumberService } from '../../common/services/randomNumber'

jest.mock('../../utils', () => ({
...jest.requireActual('../../utils'),
isRandomNumberLessThan: jest.fn(),
}))
import { applyCrows, forEachPlot } from './applyCrows'

const CARROT = 'carrot'

beforeEach(() => {
jest.spyOn(randomNumberService, 'isRandomNumberLessThan')
})

describe('applyCrows', () => {
let state

Expand Down Expand Up @@ -38,7 +39,7 @@ describe('applyCrows', () => {
})

it('does not modify plots if rng fails', () => {
isRandomNumberLessThan.mockReturnValue(true)
randomNumberService.isRandomNumberLessThan.mockReturnValue(true)
const newState = applyCrows(state)

expect(newState.field).toEqual(state.field)
Expand All @@ -54,7 +55,7 @@ describe('applyCrows', () => {

describe('crows spawned', () => {
beforeEach(() => {
isRandomNumberLessThan.mockReturnValue(false)
randomNumberService.isRandomNumberLessThan.mockReturnValue(false)
Math.random.mockReturnValueOnce(1) // spawn max amount of crows
})

Expand Down
7 changes: 4 additions & 3 deletions src/game-logic/reducers/clearPlot.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import {
getCropLifeStage,
getPlotContentType,
getSeedItemIdFromFinalStageCropItemId,
isRandomNumberLessThan,
} from '../../utils'
import { HOE_LEVEL_TO_SEED_RECLAIM_RATE } from '../../constants'
import { randomNumberService } from '../../common/services/randomNumber'

import { addItemToInventory } from './addItemToInventory'

import { removeFieldPlotAt } from './removeFieldPlotAt'

const { GROWN } = cropLifeStage
Expand All @@ -32,7 +31,9 @@ export const clearPlot = (state, x, y) => {
if (
getPlotContentType(plotContent) === itemType.CROP &&
getCropLifeStage(plotContent) !== GROWN &&
isRandomNumberLessThan(HOE_LEVEL_TO_SEED_RECLAIM_RATE[hoeLevel])
randomNumberService.isRandomNumberLessThan(
HOE_LEVEL_TO_SEED_RECLAIM_RATE[hoeLevel]
)
) {
const seedId = getSeedItemIdFromFinalStageCropItemId(plotContent.itemId)
state = addItemToInventory(state, itemsMap[seedId])
Expand Down
9 changes: 5 additions & 4 deletions src/game-logic/reducers/clearPlot.test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { testCrop } from '../../test-utils'
import { toolType, toolLevel } from '../../enums'
import { getPlotContentFromItemId, isRandomNumberLessThan } from '../../utils'

import { getPlotContentFromItemId } from '../../utils'
import { INFINITE_STORAGE_LIMIT } from '../../constants'
import { randomNumberService } from '../../common/services/randomNumber'

import { clearPlot } from './clearPlot'

jest.mock('../../data/maps')
jest.mock('../../utils/isRandomNumberLessThan')

describe('clearPlot', () => {
describe('plotContent is a crop', () => {
Expand Down Expand Up @@ -115,7 +114,9 @@ describe('clearPlot', () => {

describe('hoe upgrades', () => {
beforeEach(() => {
isRandomNumberLessThan.mockReturnValue(true)
jest
.spyOn(randomNumberService, 'isRandomNumberLessThan')
.mockReturnValue(true)
})

describe('crop is not fully grown', () => {
Expand Down
4 changes: 3 additions & 1 deletion src/game-logic/reducers/minePlot.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { addItemToInventory } from './addItemToInventory'
import { showNotification } from './showNotification'
import { modifyFieldPlotAt } from './modifyFieldPlotAt'

const daysUntilClearPeriods = [1, 2, 2, 3]

/**
* @param {farmhand.state} state
* @param {number} x
Expand All @@ -32,7 +34,7 @@ export const minePlot = (state, x, y) => {
shovelLevel
)
let spawnedOre = null
let daysUntilClear = chooseRandom([1, 2, 2, 3])
let daysUntilClear = chooseRandom(daysUntilClearPeriods)

if (spawnedResources.length) {
// even when multiple resources are spawned, the first one is ok to use
Expand Down
5 changes: 2 additions & 3 deletions src/game-logic/reducers/spawnWeeds.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { WEEDS_SPAWN_CHANCE } from '../../constants'

import { randomNumberService } from '../../common/services/randomNumber'
import { weed } from '../../data/items'

import isRandomNumberLessThan from '../../utils/isRandomNumberLessThan'
import { getPlotContentFromItemId } from '../../utils'

/**
Expand All @@ -14,7 +13,7 @@ export function spawnWeeds(plotContents) {

let contents = null

if (isRandomNumberLessThan(WEEDS_SPAWN_CHANCE)) {
if (randomNumberService.isRandomNumberLessThan(WEEDS_SPAWN_CHANCE)) {
contents = getPlotContentFromItemId(weed.id)
}

Expand Down
10 changes: 6 additions & 4 deletions src/game-logic/reducers/spawnWeeds.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import isRandomNumberLessThan from '../../utils/isRandomNumberLessThan'
import { randomNumberService } from '../../common/services/randomNumber'

import { spawnWeeds } from './spawnWeeds'

jest.mock('../../utils/isRandomNumberLessThan')
beforeEach(() => {
jest.spyOn(randomNumberService, 'isRandomNumberLessThan')
})

describe('spawnWeeds', () => {
it('will not spawn weeds in an occupied plot', () => {
Expand All @@ -12,7 +14,7 @@ describe('spawnWeeds', () => {
})

it('will spawn weeds if the plot is empty and the random chance event succeeds', () => {
isRandomNumberLessThan.mockReturnValue(true)
randomNumberService.isRandomNumberLessThan.mockReturnValue(true)

expect(spawnWeeds(null)).toEqual({
itemId: 'weed',
Expand All @@ -21,7 +23,7 @@ describe('spawnWeeds', () => {
})

it('will not spawn weeds if the random chance fails', () => {
isRandomNumberLessThan.mockReturnValue(false)
randomNumberService.isRandomNumberLessThan.mockReturnValue(false)

expect(spawnWeeds(null)).toEqual(null)
})
Expand Down
3 changes: 1 addition & 2 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1223,7 +1223,7 @@ const colorizeCowTemplate = (() => {
/**
* @param {string} cowTemplate Base64 representation of an image
* @param {string} color
* @returns {string} Base64 representation of an image
* @returns {Promise.<string>} Base64 representation of an image
*/
return async (cowTemplate, color) => {
if (color === cowColors.RAINBOW) return animals.cow.rainbow
Expand Down Expand Up @@ -1324,5 +1324,4 @@ export const isOctober = () => new Date().getMonth() === 9
*/
export const isDecember = () => new Date().getMonth() === 11

export { default as isRandomNumberLessThan } from './isRandomNumberLessThan'
export { default as totalIngredientsInRecipe } from './totalIngredientsInRecipe'
12 changes: 0 additions & 12 deletions src/utils/isRandomNumberLessThan.js

This file was deleted.

Loading

0 comments on commit d214631

Please sign in to comment.