diff --git a/config.json b/config.json index 5bbf008ad..b157071f2 100644 --- a/config.json +++ b/config.json @@ -90,6 +90,15 @@ "difficulty": 2, "topics": ["classes", "floating_point_numbers"] }, + { + "slug": "dnd-character", + "name": "DnD Character", + "uuid": "6f34eb4d-4028-4f89-9965-c9a887f416a0", + "practices": [], + "prerequisites": [], + "difficulty": 2, + "topics": ["classes"] + }, { "slug": "pangram", "name": "Pangram", diff --git a/exercises/practice/dnd-character/.docs/instructions.md b/exercises/practice/dnd-character/.docs/instructions.md new file mode 100644 index 000000000..f1ccc3b7b --- /dev/null +++ b/exercises/practice/dnd-character/.docs/instructions.md @@ -0,0 +1,33 @@ +# Description + +For a game of [Dungeons & Dragons][dnd], each player starts by generating a +character they can play with. This character has, among other things, six +abilities; strength, dexterity, constitution, intelligence, wisdom and +charisma. These six abilities have scores that are determined randomly. You +do this by rolling four 6-sided dice and record the sum of the largest three +dice. You do this six times, once for each ability. + +Your character's initial hitpoints are 10 + your character's constitution +modifier. You find your character's constitution modifier by subtracting 10 +from your character's constitution, divide by 2 and round down. + +Write a random character generator that follows the rules above. + +For example, the six throws of four dice may look like: + +- 5, 3, 1, 6: You discard the 1 and sum 5 + 3 + 6 = 14, which you assign to strength. +- 3, 2, 5, 3: You discard the 2 and sum 3 + 5 + 3 = 11, which you assign to dexterity. +- 1, 1, 1, 1: You discard the 1 and sum 1 + 1 + 1 = 3, which you assign to constitution. +- 2, 1, 6, 6: You discard the 1 and sum 2 + 6 + 6 = 14, which you assign to intelligence. +- 3, 5, 3, 4: You discard the 3 and sum 5 + 3 + 4 = 12, which you assign to wisdom. +- 6, 6, 6, 6: You discard the 6 and sum 6 + 6 + 6 = 18, which you assign to charisma. + +Because constitution is 3, the constitution modifier is -4 and the hitpoints are 6. + +## Notes + +Most programming languages feature (pseudo-)random generators, but few +programming languages are designed to roll dice. One such language is [Troll]. + +[dnd]: https://en.wikipedia.org/wiki/Dungeons_%26_Dragons +[troll]: http://hjemmesider.diku.dk/~torbenm/Troll/ diff --git a/exercises/practice/dnd-character/.eslintignore b/exercises/practice/dnd-character/.eslintignore new file mode 100644 index 000000000..e3e45c396 --- /dev/null +++ b/exercises/practice/dnd-character/.eslintignore @@ -0,0 +1,12 @@ +!.meta + +# Protected or generated +.git +.vscode + +# When using npm +node_modules/* + +# Configuration files +babel.config.js +jest.config.js \ No newline at end of file diff --git a/exercises/practice/dnd-character/.eslintrc b/exercises/practice/dnd-character/.eslintrc new file mode 100644 index 000000000..3fbb9032c --- /dev/null +++ b/exercises/practice/dnd-character/.eslintrc @@ -0,0 +1,23 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json", + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 2020, + "sourceType": "module" + }, + "extends": "@exercism/eslint-config-typescript", + "env": { + "jest": true + }, + "overrides": [ + { + "files": [".meta/proof.ci.ts", ".meta/exemplar.ts", "*.test.ts"], + "excludedFiles": ["custom.test.ts"], + "extends": "@exercism/eslint-config-typescript/maintainers" + } + ] +} diff --git a/exercises/practice/dnd-character/.meta/config.json b/exercises/practice/dnd-character/.meta/config.json new file mode 100644 index 000000000..4df44f7e9 --- /dev/null +++ b/exercises/practice/dnd-character/.meta/config.json @@ -0,0 +1,12 @@ +{ + "blurb": "Randomly generate Dungeons & Dragons characters.", + "authors": ["JoshiRaez", "SleeplessByte"], + "contributors": [], + "files": { + "solution": ["dnd-character.ts"], + "test": ["dnd-character.test.ts"], + "example": [".meta/proof.ci.ts"] + }, + "source": "Simon Shine, Erik Schierboom", + "source_url": "https://github.com/exercism/problem-specifications/issues/616#issuecomment-437358945" +} diff --git a/exercises/practice/dnd-character/.meta/proof.ci.ts b/exercises/practice/dnd-character/.meta/proof.ci.ts new file mode 100644 index 000000000..9a1b05d92 --- /dev/null +++ b/exercises/practice/dnd-character/.meta/proof.ci.ts @@ -0,0 +1,40 @@ +export class DnDCharacter { + public readonly hitpoints: number + + public readonly strength: number + public readonly dexterity: number + public readonly constitution: number + public readonly intelligence: number + public readonly wisdom: number + public readonly charisma: number + + constructor() { + this.strength = DnDCharacter.generateAbilityScore() + this.dexterity = DnDCharacter.generateAbilityScore() + this.constitution = DnDCharacter.generateAbilityScore() + this.intelligence = DnDCharacter.generateAbilityScore() + this.wisdom = DnDCharacter.generateAbilityScore() + this.charisma = DnDCharacter.generateAbilityScore() + + this.hitpoints = 10 + DnDCharacter.getModifierFor(this.constitution) + } + + public static generateAbilityScore(): number { + return this.rollDice(4) + .sort() + .slice(1, 4) + .reduce((acu, act) => acu + act, 0) + } + + public static getModifierFor(abilityValue: number): number { + return Math.floor((abilityValue - 10) / 2) + } + + private static rollDice(quantity: number): number[] { + return new Array(quantity).fill(0).map(() => this.rollDie()) + } + + private static rollDie(): number { + return Math.floor(Math.random() * 6) + 1 + } +} diff --git a/exercises/practice/dnd-character/.meta/tests.toml b/exercises/practice/dnd-character/.meta/tests.toml new file mode 100644 index 000000000..eedfb2c4c --- /dev/null +++ b/exercises/practice/dnd-character/.meta/tests.toml @@ -0,0 +1,76 @@ +[1e9ae1dc-35bd-43ba-aa08-e4b94c20fa37] +description = "Ability modifier for score 3 is -4" +include = true + +[cc9bb24e-56b8-4e9e-989d-a0d1a29ebb9c] +description = "Ability modifier for score 4 is -3" +include = true + +[5b519fcd-6946-41ee-91fe-34b4f9808326] +description = "Ability modifier for score 5 is -3" +include = true + +[dc2913bd-6d7a-402e-b1e2-6d568b1cbe21] +description = "Ability modifier for score 6 is -2" +include = true + +[099440f5-0d66-4b1a-8a10-8f3a03cc499f] +description = "Ability modifier for score 7 is -2" +include = true + +[cfda6e5c-3489-42f0-b22b-4acb47084df0] +description = "Ability modifier for score 8 is -1" +include = true + +[c70f0507-fa7e-4228-8463-858bfbba1754] +description = "Ability modifier for score 9 is -1" +include = true + +[6f4e6c88-1cd9-46a0-92b8-db4a99b372f7] +description = "Ability modifier for score 10 is 0" +include = true + +[e00d9e5c-63c8-413f-879d-cd9be9697097] +description = "Ability modifier for score 11 is 0" +include = true + +[eea06f3c-8de0-45e7-9d9d-b8cab4179715] +description = "Ability modifier for score 12 is 1" +include = true + +[9c51f6be-db72-4af7-92ac-b293a02c0dcd] +description = "Ability modifier for score 13 is 1" +include = true + +[94053a5d-53b6-4efc-b669-a8b5098f7762] +description = "Ability modifier for score 14 is 2" +include = true + +[8c33e7ca-3f9f-4820-8ab3-65f2c9e2f0e2] +description = "Ability modifier for score 15 is 2" +include = true + +[c3ec871e-1791-44d0-b3cc-77e5fb4cd33d] +description = "Ability modifier for score 16 is 3" +include = true + +[3d053cee-2888-4616-b9fd-602a3b1efff4] +description = "Ability modifier for score 17 is 3" +include = true + +[bafd997a-e852-4e56-9f65-14b60261faee] +description = "Ability modifier for score 18 is 4" +include = true + +[4f28f19c-2e47-4453-a46a-c0d365259c14] +description = "Random ability is within range" +include = true + +[385d7e72-864f-4e88-8279-81a7d75b04ad] +description = "Random character is valid" +include = true + +[2ca77b9b-c099-46c3-a02c-0d0f68ffa0fe] +description = "Each ability is only calculated once" +include = true + diff --git a/exercises/practice/dnd-character/babel.config.js b/exercises/practice/dnd-character/babel.config.js new file mode 100644 index 000000000..eca257951 --- /dev/null +++ b/exercises/practice/dnd-character/babel.config.js @@ -0,0 +1,20 @@ +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + useBuiltIns: 'entry', + corejs: '3.17', + }, + ], + '@babel/preset-typescript', + ], + plugins: [ + '@babel/proposal-class-properties', + '@babel/proposal-object-rest-spread', + '@babel/plugin-syntax-bigint', + ], +} diff --git a/exercises/practice/dnd-character/dnd-character.test.ts b/exercises/practice/dnd-character/dnd-character.test.ts new file mode 100644 index 000000000..fbb1eab11 --- /dev/null +++ b/exercises/practice/dnd-character/dnd-character.test.ts @@ -0,0 +1,109 @@ +import { DnDCharacter } from './dnd-character' + +describe('Ability modifier', () => { + test('Ability modifier for score 3 is -4', () => { + expect(DnDCharacter.getModifierFor(3)).toEqual(-4) + }) + + test('Ability modifier for score 4 is -3', () => { + expect(DnDCharacter.getModifierFor(4)).toEqual(-3) + }) + + test('Ability modifier for score 5 is -3', () => { + expect(DnDCharacter.getModifierFor(5)).toEqual(-3) + }) + + test('Ability modifier for score 6 is -2', () => { + expect(DnDCharacter.getModifierFor(6)).toEqual(-2) + }) + + test('Ability modifier for score 7 is -2', () => { + expect(DnDCharacter.getModifierFor(7)).toEqual(-2) + }) + + test('Ability modifier for score 8 is -1', () => { + expect(DnDCharacter.getModifierFor(8)).toEqual(-1) + }) + + test('Ability modifier for score 9 is -1', () => { + expect(DnDCharacter.getModifierFor(9)).toEqual(-1) + }) + + test('Ability modifier for score 10 is 0', () => { + expect(DnDCharacter.getModifierFor(10)).toEqual(0) + }) + + test('Ability modifier for score 11 is 0', () => { + expect(DnDCharacter.getModifierFor(11)).toEqual(0) + }) + + test('Ability modifier for score 12 is 1', () => { + expect(DnDCharacter.getModifierFor(12)).toEqual(1) + }) + + test('Ability modifier for score 13 is 1', () => { + expect(DnDCharacter.getModifierFor(13)).toEqual(1) + }) + + test('Ability modifier for score 14 is 2', () => { + expect(DnDCharacter.getModifierFor(14)).toEqual(2) + }) + + test('Ability modifier for score 15 is 2', () => { + expect(DnDCharacter.getModifierFor(15)).toEqual(2) + }) + + test('Ability modifier for score 16 is 3', () => { + expect(DnDCharacter.getModifierFor(16)).toEqual(3) + }) + + test('Ability modifier for score 17 is 3', () => { + expect(DnDCharacter.getModifierFor(17)).toEqual(3) + }) + + test('Ability modifier for score 18 is 4', () => { + expect(DnDCharacter.getModifierFor(18)).toEqual(4) + }) +}) + +describe('Ability generator', () => { + test('Random ability is within range', () => { + const abilityScore = DnDCharacter.generateAbilityScore() + expect(abilityScore).toBeGreaterThanOrEqual(3) + expect(abilityScore).toBeLessThanOrEqual(18) + }) +}) + +describe('Character creation', () => { + test('Random character is valid', () => { + const character = new DnDCharacter() + + expect(character.hitpoints).toEqual( + 10 + DnDCharacter.getModifierFor(character.constitution) + ) + + expect(character.strength).toBeGreaterThanOrEqual(3) + expect(character.strength).toBeLessThanOrEqual(18) + + expect(character.dexterity).toBeGreaterThanOrEqual(3) + expect(character.dexterity).toBeLessThanOrEqual(18) + + expect(character.constitution).toBeGreaterThanOrEqual(3) + expect(character.constitution).toBeLessThanOrEqual(18) + + expect(character.intelligence).toBeGreaterThanOrEqual(3) + expect(character.intelligence).toBeLessThanOrEqual(18) + + expect(character.wisdom).toBeGreaterThanOrEqual(3) + expect(character.wisdom).toBeLessThanOrEqual(18) + + expect(character.charisma).toBeGreaterThanOrEqual(3) + expect(character.charisma).toBeLessThanOrEqual(18) + }) + + test('Each ability is only calculated once', () => { + const character = new DnDCharacter() + + expect(character.strength === character.strength).toBeTruthy() + }) +}) diff --git a/exercises/practice/dnd-character/dnd-character.ts b/exercises/practice/dnd-character/dnd-character.ts new file mode 100644 index 000000000..c3391a155 --- /dev/null +++ b/exercises/practice/dnd-character/dnd-character.ts @@ -0,0 +1,9 @@ +export class DnDCharacter { + public static generateAbilityScore(): number { + throw new Error('Remove this statement and implement this function') + } + + public static getModifierFor(abilityValue: number): number { + throw new Error('Remove this statement and implement this function') + } +} diff --git a/exercises/practice/dnd-character/jest.config.js b/exercises/practice/dnd-character/jest.config.js new file mode 100644 index 000000000..d6b552072 --- /dev/null +++ b/exercises/practice/dnd-character/jest.config.js @@ -0,0 +1,19 @@ +module.exports = { + verbose: true, + projects: [''], + testMatch: [ + '**/__tests__/**/*.[jt]s?(x)', + '**/test/**/*.[jt]s?(x)', + '**/?(*.)+(spec|test).[jt]s?(x)', + ], + testPathIgnorePatterns: [ + '/(?:production_)?node_modules/', + '.d.ts$', + '/test/fixtures', + '/test/helpers', + '__mocks__', + ], + transform: { + '^.+\\.[jt]sx?$': 'babel-jest', + }, +} diff --git a/exercises/practice/dnd-character/package.json b/exercises/practice/dnd-character/package.json new file mode 100644 index 000000000..10679111f --- /dev/null +++ b/exercises/practice/dnd-character/package.json @@ -0,0 +1,33 @@ +{ + "name": "@exercism/typescript-dnd-character", + "version": "1.0.0", + "description": "Exercism practice exercise on dnd-character", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/exercism/typescript" + }, + "devDependencies": { + "@babel/core": "^7.15.5", + "@babel/plugin-proposal-class-properties": "^7.14.5", + "@babel/plugin-proposal-object-rest-spread": "^7.14.7", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/preset-env": "^7.15.4", + "@babel/preset-typescript": "^7.15.0", + "@types/jest": "^26.0.24", + "@types/node": "^14.17.14", + "@exercism/eslint-config-typescript": "^0.3.0", + "babel-jest": "^26.6.3", + "core-js": "^3.17.2", + "eslint": "^7.32.0", + "eslint-plugin-import": "^2.24.2", + "jest": "^26.6.3", + "typescript": "^4.4.2" + }, + "scripts": { + "test": "yarn lint:types && jest --no-cache", + "lint": "yarn lint:types && yarn lint:ci", + "lint:types": "yarn tsc --noEmit -p .", + "lint:ci": "eslint . --ext .tsx,.ts" + } +} diff --git a/exercises/practice/dnd-character/tsconfig.json b/exercises/practice/dnd-character/tsconfig.json new file mode 100644 index 000000000..7314e3544 --- /dev/null +++ b/exercises/practice/dnd-character/tsconfig.json @@ -0,0 +1,19 @@ +{ + "display": "Configuration for Node LTS", + "compilerOptions": { + "lib": ["es2020"], + "module": "commonjs", + "target": "es2020", + + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + // Because we'll be using babel + // Ensure that Babel can safely transpile files in the TypeScript project + "isolatedModules": true + }, + "include": ["*", ".meta/*"], + "exclude": ["node_modules"] +}