From a639ce12db61f2ef085d1aaa164a74c674286e83 Mon Sep 17 00:00:00 2001 From: John Riccardi Date: Mon, 10 Jun 2024 13:05:58 -0500 Subject: [PATCH 1/4] Fix types for Randomization --- src/dice/Randomization.test.ts | 6 ++++++ src/dice/Randomization.ts | 8 +++++--- src/dice/__mocks__/Randomization.ts | 6 +++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/dice/Randomization.test.ts b/src/dice/Randomization.test.ts index 01592da..5b9b85a 100644 --- a/src/dice/Randomization.test.ts +++ b/src/dice/Randomization.test.ts @@ -27,5 +27,11 @@ describe('Randomization', () => { expect(uniqueItems.length).toBeGreaterThan(1) }) + + test('given an empty array, throws error', () => { + expect(() => Randomization.getRandomItem([])).toThrow( + new Error('Cannot get a random item from an empty array'), + ) + }) }) }) diff --git a/src/dice/Randomization.ts b/src/dice/Randomization.ts index 0cae3a9..e9b68ac 100644 --- a/src/dice/Randomization.ts +++ b/src/dice/Randomization.ts @@ -1,8 +1,10 @@ class Randomization { public static getRandomItem = (list: T[]): T => { - const length = list.length - const randomIndex = Math.floor(Math.random() * length) - return list[randomIndex] + if (list.length === 0) { + throw new Error('Cannot get a random item from an empty array') + } + const randomIndex = Math.floor(Math.random() * list.length) + return list[randomIndex]! } } diff --git a/src/dice/__mocks__/Randomization.ts b/src/dice/__mocks__/Randomization.ts index 6158284..dc315a9 100644 --- a/src/dice/__mocks__/Randomization.ts +++ b/src/dice/__mocks__/Randomization.ts @@ -1,6 +1,10 @@ class Randomization { public static getRandomItem = (list: T[]): T => { - return list[0] + if (list.length === 0) { + throw new Error('Cannot get a random item from an empty array') + } else { + return list[0]! + } } } From 441863108ccd387464b7c9de0ee76683cca2a946 Mon Sep 17 00:00:00 2001 From: John Riccardi Date: Mon, 10 Jun 2024 16:47:28 -0500 Subject: [PATCH 2/4] Fix Description class types --- src/knave/Description.test.ts | 7 +++-- src/knave/Description.ts | 54 ++++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/knave/Description.test.ts b/src/knave/Description.test.ts index 8442b96..335b404 100644 --- a/src/knave/Description.test.ts +++ b/src/knave/Description.test.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import Description from './Description' import traitsData from './data/traits.json' @@ -8,12 +9,12 @@ describe('Description', () => { const nouns = Object.keys(traitsData) const description = new Description() - const traits: ITraits = description.traits + const traits = description.traits nouns.forEach(noun => { test(`generates a ${noun} trait`, () => { - const possibleTraits = traitsData[noun as INoun] - expect(possibleTraits.includes(traits[noun as INoun])).toBeTruthy() + const possibleTraits = traitsData[noun] + expect(possibleTraits.includes(traits[noun])).toBeTruthy() }) }) }) diff --git a/src/knave/Description.ts b/src/knave/Description.ts index 8647fa9..a4eae38 100644 --- a/src/knave/Description.ts +++ b/src/knave/Description.ts @@ -1,26 +1,60 @@ import traitsData from './data/traits.json' import { Randomization } from '../dice' +export type Noun = + | 'physique' + | 'face' + | 'skin' + | 'hair' + | 'clothing' + | 'virtue' + | 'vice' + | 'speech' + | 'background' + | 'misfortune' + +type Traits = Record + class Description { - public traits + public traits: Traits constructor() { this.traits = this.generateRandomTraits() } - private generateRandomTraits() { - const nouns: string[] = Object.keys(traitsData) + private generateRandomTraits = () => { + const defaultTraits: Traits = { + physique: '', + face: '', + skin: '', + hair: '', + clothing: '', + virtue: '', + vice: '', + speech: '', + background: '', + misfortune: '', + } + + const parsedTraitData = this.getTraitData() - const randomTraits: Record = nouns.reduce( - (acc: Record, curr: string) => { - acc[curr] = Randomization.getRandomItem(traitsData[curr as INoun]) - return acc - }, - {}, - ) + const nouns = Object.keys(parsedTraitData) + + const randomTraits = nouns.reduce((acc, curr) => { + const currentTrait = curr as Noun + const randomTraitValue = Randomization.getRandomItem( + parsedTraitData[currentTrait], + ) + acc[currentTrait] = randomTraitValue + return acc + }, defaultTraits) return randomTraits } + + private getTraitData = () => { + return traitsData satisfies Record + } } export default Description From 84abe0b45f185ab9928a1ff2532ed2af5d202748 Mon Sep 17 00:00:00 2001 From: John Riccardi Date: Mon, 10 Jun 2024 20:54:54 -0500 Subject: [PATCH 3/4] Refactor Gear class --- src/knave/Gear.test.ts | 31 ++++++++++------ src/knave/Gear.ts | 84 +++++++++++++++++++++++++++--------------- 2 files changed, 73 insertions(+), 42 deletions(-) diff --git a/src/knave/Gear.test.ts b/src/knave/Gear.test.ts index b73eb7d..a635381 100644 --- a/src/knave/Gear.test.ts +++ b/src/knave/Gear.test.ts @@ -1,46 +1,47 @@ +import { vi } from 'vitest' import armorList from './data/armor.json' import gearList from './data/gear.json' import weaponList from './data/weapons.json' -import Gear from './Gear' +import Gear, { type GearItem } from './Gear' vi.mock('../dice/Randomization') describe('Gear', () => { const itemSlots = 12 - let gear: Gear - - beforeEach(() => { - gear = new Gear(itemSlots) - }) describe('initialization', () => { test('sets the max item slots', () => { + const gear = new Gear(itemSlots) expect(gear.itemSlots).toEqual(itemSlots) }) }) describe('items', () => { test('contains 2 days of rations', () => { - const rations = gear.items.filter((g: IGear) => g.name === 'rations') + const gear = new Gear(itemSlots) + const rations = gear.items.filter(gear => gear.name === 'rations') expect(rations).toHaveLength(2) }) test('contains 2 pieces of dungeonnering gear', () => { - const dungeoneeringGear = gear.items.filter((item: IGear) => + const gear = new Gear(itemSlots) + const dungeoneeringGear = gear.items.filter(item => gearList.dungeoneeringGear.includes(item), ) expect(dungeoneeringGear).toHaveLength(2) }) test('has one piece of gear from General Gear Set 1', () => { - const generalGear1 = gear.items.filter((item: IGear) => + const gear = new Gear(itemSlots) + const generalGear1 = gear.items.filter(item => gearList.generalGearSetOne.includes(item), ) expect(generalGear1).toHaveLength(1) }) test('has one piece of gear from General Gear 2', () => { - const generalGear2 = gear.items.filter((item: IGear) => + const gear = new Gear(itemSlots) + const generalGear2 = gear.items.filter(item => gearList.generalGearSetTwo.includes(item), ) expect(generalGear2).toHaveLength(1) @@ -49,11 +50,13 @@ describe('Gear', () => { describe('armor', () => { test('selects a random piece of armor', () => { + const gear = new Gear(itemSlots) expect(armorList.armor.includes(gear.armor)).toBeTruthy() }) test('adds the armor to the characters gear', () => { - const armor = gear.items.filter((item: IGear) => item.type === 'armor') + const gear = new Gear(itemSlots) + const armor = gear.items.filter(item => item.type === 'armor') expect(armor).toHaveLength(1) }) @@ -61,11 +64,15 @@ describe('Gear', () => { describe('weapon', () => { test('selects a random weapon', () => { + const gear = new Gear(itemSlots) expect(weaponList.weapons.includes(gear.weapon)).toBeTruthy() }) test('adds the weapon to the characters gear', () => { - const weapon = gear.items.filter((item: IGear) => item.type === 'weapon') + const gear = new Gear(itemSlots) + const weapon = gear.items.filter( + (item: GearItem) => item.type === 'weapon', + ) expect(weapon).toHaveLength(1) }) }) diff --git a/src/knave/Gear.ts b/src/knave/Gear.ts index cbf895b..1d52c40 100644 --- a/src/knave/Gear.ts +++ b/src/knave/Gear.ts @@ -3,72 +3,96 @@ import gearList from './data/gear.json' import weaponList from './data/weapons.json' import { Randomization } from '../dice' +type GearType = 'food' | 'tool' | 'light' | 'armor' | 'weapon' + +export type GearItem = { + count: number + name: string + slots: number + type: GearType +} + +type ArmorItem = GearItem & { + quality: number + defense: number +} + +type WeaponItem = GearItem & { + damage: string + hand: number + quality: number +} + class Gear { public itemSlots: number - public items: IGear[] - public armor: IArmor - public weapon: IWeapon + public items: GearItem[] + public armor: ArmorItem + public weapon: WeaponItem private itemSlotsUsed: number constructor(itemSlots: number) { - // Order matters! this.getRandomGear must be last this.itemSlots = itemSlots - this.itemSlotsUsed = 6 + this.itemSlotsUsed = 0 + this.items = this.getRandomGear() this.armor = this.getRandomArmor() this.weapon = this.getRandomWeapon() - this.items = this.getRandomGear() } - private getRandomArmor = (): IArmor => { + private getRandomArmor = (): ArmorItem => { const slotsRemaining = this.itemSlots - this.itemSlotsUsed - const availableArmor = (armorList.armor as IArmor[]).filter(armor => { + const availableArmor = (armorList.armor as ArmorItem[]).filter(armor => { // Ensures there is at least 1 slot remaining for a weapon to be added return armor.slots < slotsRemaining }) const armor = Randomization.getRandomItem(availableArmor) + this.itemSlotsUsed += armor.slots + this.items.push(armor) return armor } - private getRandomWeapon = (): IWeapon => { - const slotsRemaining = this.itemSlots - this.itemSlotsUsed - const availableWeapons = (weaponList.weapons as IWeapon[]).filter( - weapon => { - return weapon.slots <= slotsRemaining - }, - ) - const weapon = Randomization.getRandomItem(availableWeapons) - this.itemSlotsUsed += weapon.slots - return weapon - } - - private getRandomGear(): IGear[] { - const dg = gearList.dungeoneeringGear as IGear[] - const gen1 = gearList.generalGearSetOne as IGear[] - const gen2 = gearList.generalGearSetTwo as IGear[] + private getRandomGear(): GearItem[] { + const dg = gearList.dungeoneeringGear as GearItem[] + const gen1 = gearList.generalGearSetOne as GearItem[] + const gen2 = gearList.generalGearSetTwo as GearItem[] - const startingGear: IGear[] = [ + const startingGear: GearItem[] = [ { name: 'rations', count: 1, slots: 1, type: 'food' }, { name: 'rations', count: 1, slots: 1, type: 'food' }, ] - const dungeoneeringGear: IGear[] = new Array(2) + const dungeoneeringGear: GearItem[] = new Array(2) .fill(undefined) .map(() => Randomization.getRandomItem(dg)) const generalGear1 = Randomization.getRandomItem(gen1) const generalGear2 = Randomization.getRandomItem(gen2) - // TODO: refactor so this isn't dependent on getRandomArmor being called first in the constructor - return [ + const gear = [ ...startingGear, ...dungeoneeringGear, generalGear1, generalGear2, - this.armor, - this.weapon, ] + + const itemSlotsUsed = gear.reduce((acc, curr) => (acc += curr.slots), 0) + this.itemSlotsUsed = itemSlotsUsed + + return gear + } + + private getRandomWeapon = (): WeaponItem => { + const slotsRemaining = this.itemSlots - this.itemSlotsUsed + const availableWeapons = (weaponList.weapons as WeaponItem[]).filter( + weapon => { + return weapon.slots <= slotsRemaining + }, + ) + const weapon = Randomization.getRandomItem(availableWeapons) + this.itemSlotsUsed += weapon.slots + this.items.push(weapon) + return weapon } } From 6f9072a3d0c085018218d97e54876dd5e2fa92b6 Mon Sep 17 00:00:00 2001 From: John Riccardi Date: Tue, 11 Jun 2024 14:02:11 -0500 Subject: [PATCH 4/4] Fix Character types, refactor --- src/knave/Character.test.ts | 118 ++++++++++-------------------------- src/knave/Character.ts | 54 +++++++++++++---- src/knave/Description.ts | 2 +- src/knave/Gear.ts | 4 +- src/knave/KnaveTypes.d.ts | 59 ------------------ 5 files changed, 78 insertions(+), 159 deletions(-) delete mode 100644 src/knave/KnaveTypes.d.ts diff --git a/src/knave/Character.test.ts b/src/knave/Character.test.ts index 4d5a966..81c4769 100644 --- a/src/knave/Character.test.ts +++ b/src/knave/Character.test.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import KnaveCharacter from './Character' vi.mock('../dice/Dice') @@ -7,101 +8,42 @@ vi.mock('../dice/Randomization') describe('KnaveCharacter', () => { describe('generate()', () => { - let generatedCharacter: KnaveCharacter - - beforeEach(() => { - generatedCharacter = new KnaveCharacter() - }) - test('generates a level 1 character by default', () => { + const generatedCharacter = new KnaveCharacter() expect(generatedCharacter.level).toBe(1) }) describe('abilities', () => { - describe('charisma', () => { - test('generates a random charisma', () => { - expect(generatedCharacter.charisma.bonus).toBeLessThanOrEqual(6) - expect(generatedCharacter.charisma.bonus).toBeGreaterThanOrEqual(1) - }) - - test('has a charisma defense that is 10 higher than the bonus', () => { - expect(generatedCharacter.charisma.defense).toEqual( - generatedCharacter.charisma.bonus + 10, - ) - }) - }) - - describe('constitution', () => { - test('generates a random constitution', () => { - expect(generatedCharacter.constitution.bonus).toBeLessThanOrEqual(6) - expect(generatedCharacter.constitution.bonus).toBeGreaterThanOrEqual( - 1, - ) - }) - - test('has a constitution defense that is 10 higher than the bonus', () => { - expect(generatedCharacter.constitution.defense).toEqual( - generatedCharacter.constitution.bonus + 10, - ) - }) - }) - - describe('dexterity', () => { - test('generates a random dexterity', () => { - expect(generatedCharacter.dexterity.bonus).toBeLessThanOrEqual(6) - expect(generatedCharacter.dexterity.bonus).toBeGreaterThanOrEqual(1) - }) - - test('has a dexterity defense that is 10 higher than the bonus', () => { - expect(generatedCharacter.dexterity.defense).toEqual( - generatedCharacter.dexterity.bonus + 10, - ) - }) - }) - - describe('intelligence', () => { - test('generates a random intelligence', () => { - expect(generatedCharacter.intelligence.bonus).toBeLessThanOrEqual(6) - expect(generatedCharacter.intelligence.bonus).toBeGreaterThanOrEqual( - 1, - ) - }) - - test('has a intelligence defense that is 10 higher than the bonus', () => { - expect(generatedCharacter.intelligence.defense).toEqual( - generatedCharacter.intelligence.bonus + 10, - ) - }) - }) - - describe('strength', () => { - test('generates a random strength', () => { - expect(generatedCharacter.strength.bonus).toBeLessThanOrEqual(6) - expect(generatedCharacter.strength.bonus).toBeGreaterThanOrEqual(1) - }) - - test('has a strength defense that is 10 higher than the bonus', () => { - expect(generatedCharacter.strength.defense).toEqual( - generatedCharacter.strength.bonus + 10, - ) - }) - }) - - describe('wisdom', () => { - test('generates a random wisdom', () => { - expect(generatedCharacter.wisdom.bonus).toBeLessThanOrEqual(6) - expect(generatedCharacter.wisdom.bonus).toBeGreaterThanOrEqual(1) - }) + const abilityNames = [ + 'charisma', + 'constitution', + 'dexterity', + 'intelligence', + 'strength', + 'wisdom', + ] + test.each(abilityNames)( + 'generates a random %s bonus between 1-6 (inclusive)', + ability => { + const generatedCharacter = new KnaveCharacter() + expect(generatedCharacter[ability].bonus).toBeLessThanOrEqual(6) + expect(generatedCharacter[ability].bonus).toBeGreaterThanOrEqual(1) + }, + ) - test('has a wisdom defense that is 10 higher than the bonus', () => { - expect(generatedCharacter.wisdom.defense).toEqual( - generatedCharacter.wisdom.bonus + 10, + test.each(abilityNames)( + 'generates a %s defense that is 10 higher than the bonus', + ability => { + const generatedCharacter = new KnaveCharacter() + expect(generatedCharacter[ability].defense).toEqual( + generatedCharacter[ability].bonus + 10, ) - }) - }) + }, + ) describe('abilityScores', () => { test('has a getter method to get all ability scores', () => { + const generatedCharacter = new KnaveCharacter() const abilities = generatedCharacter.abilityScores const abilityNames = Object.keys(abilities) expect(abilityNames.sort()).toEqual([ @@ -117,30 +59,36 @@ describe('KnaveCharacter', () => { }) test('has a number of item slots equal to the constitution defense', () => { + const generatedCharacter = new KnaveCharacter() expect(generatedCharacter.itemSlots).toBe( generatedCharacter.constitution.defense, ) }) test('has a randomly generated starting copper pieces', () => { + const generatedCharacter = new KnaveCharacter() expect(generatedCharacter.copperPieces).toBeGreaterThanOrEqual(23) expect(generatedCharacter.copperPieces).toBeLessThanOrEqual(38) }) test('has a maxHP stat between 1 and 8', () => { + const generatedCharacter = new KnaveCharacter() expect(generatedCharacter.maxHp).toBeGreaterThanOrEqual(1) expect(generatedCharacter.maxHp).toBeLessThanOrEqual(8) }) test('generates a list of items', () => { + const generatedCharacter = new KnaveCharacter() expect(generatedCharacter.items.length).toBeGreaterThan(1) }) test('randomly generates armor', () => { + const generatedCharacter = new KnaveCharacter() expect(generatedCharacter.armor).toBeTruthy() }) test('randomly generates a weapon', () => { + const generatedCharacter = new KnaveCharacter() expect(generatedCharacter.weapon).toBeTruthy() }) }) diff --git a/src/knave/Character.ts b/src/knave/Character.ts index 68789c4..aa5894f 100644 --- a/src/knave/Character.ts +++ b/src/knave/Character.ts @@ -1,8 +1,25 @@ import { KnaveDescription, KnaveGear } from './' import { Dice } from '../dice' +import type { Traits } from './Description' +import type { ArmorItem, GearItem, WeaponItem } from './Gear' + +type Abilities = Record + +type AbilityName = + | 'charisma' + | 'constitution' + | 'dexterity' + | 'intelligence' + | 'strength' + | 'wisdom' + +type Ability = { + bonus: number + defense: number +} class Character { - public armor: IArmor = { + public armor: ArmorItem = { count: 0, defense: 0, name: '', @@ -11,11 +28,11 @@ class Character { type: 'armor', } public copperPieces: number = 0 - public items: IGear[] = [{ name: '', count: 0, type: 'food', slots: 0 }] + public items: GearItem[] = [{ name: '', count: 0, type: 'food', slots: 0 }] public itemSlots: number = 13 public level: number = 1 public maxHp: number = 4 - public traits: ITraits = { + public traits: Traits = { physique: '', face: '', skin: '', @@ -27,17 +44,17 @@ class Character { background: '', misfortune: '', } - public weapon: IWeapon = { - count: 1, - damage: 'd6', - hand: 1, + public weapon: WeaponItem = { + count: 0, + damage: '', + hand: 0, name: '', quality: 0, slots: 1, type: 'weapon', } - private abilities: IAbilities = { + private abilities: Abilities = { charisma: { bonus: 3, defense: 13 }, constitution: { bonus: 3, defense: 13 }, dexterity: { bonus: 3, defense: 13 }, @@ -103,9 +120,22 @@ class Character { return Dice.roll(8) } - private generateAbilities = (): IAbilities => { - const [charisma, constitution, dexterity, intelligence, strength, wisdom] = - Array(6).fill(undefined).map(this.rollAbilityScore) + private generateAbilities = (): Abilities => { + const { + charisma, + constitution, + dexterity, + intelligence, + strength, + wisdom, + } = { + charisma: this.rollAbilityScore(), + constitution: this.rollAbilityScore(), + dexterity: this.rollAbilityScore(), + intelligence: this.rollAbilityScore(), + strength: this.rollAbilityScore(), + wisdom: this.rollAbilityScore(), + } const abilities = { charisma: { bonus: charisma, defense: charisma + 10 }, @@ -124,7 +154,7 @@ class Character { return Math.min(...rolls) } - private generateTraits = (): ITraits => { + private generateTraits = (): Traits => { return new KnaveDescription().traits } } diff --git a/src/knave/Description.ts b/src/knave/Description.ts index a4eae38..a0936e0 100644 --- a/src/knave/Description.ts +++ b/src/knave/Description.ts @@ -13,7 +13,7 @@ export type Noun = | 'background' | 'misfortune' -type Traits = Record +export type Traits = Record class Description { public traits: Traits diff --git a/src/knave/Gear.ts b/src/knave/Gear.ts index 1d52c40..2e4107c 100644 --- a/src/knave/Gear.ts +++ b/src/knave/Gear.ts @@ -12,12 +12,12 @@ export type GearItem = { type: GearType } -type ArmorItem = GearItem & { +export type ArmorItem = GearItem & { quality: number defense: number } -type WeaponItem = GearItem & { +export type WeaponItem = GearItem & { damage: string hand: number quality: number diff --git a/src/knave/KnaveTypes.d.ts b/src/knave/KnaveTypes.d.ts deleted file mode 100644 index 8d489e3..0000000 --- a/src/knave/KnaveTypes.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -type IAbilities = Record - -type IAbilityName = - | 'charisma' - | 'constitution' - | 'dexterity' - | 'intelligence' - | 'strength' - | 'wisdom' - -interface IAbility { - bonus: number - defense: number -} - -type Item = { - count: number - name: string - defense?: number - damage?: string - hand?: number - slots: number - type: IGearType - quality?: number -} - -interface IGear { - count: number - name: string - slots: number - type: IGearType -} - -interface IArmor extends IGear { - quality: number - defense: number -} - -interface IWeapon extends IGear { - damage: string - hand: number - quality: number -} - -type IGearType = 'food' | 'tool' | 'light' | 'armor' | 'weapon' - -type ITraits = Record - -type INoun = - | 'physique' - | 'face' - | 'skin' - | 'hair' - | 'clothing' - | 'virtue' - | 'vice' - | 'speech' - | 'background' - | 'misfortune'