From faf9d64553da700f721ec1d5fd6227e4b09ec346 Mon Sep 17 00:00:00 2001 From: Hugo Saracino Date: Mon, 6 Jul 2020 15:18:55 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20exercice=20number=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/exo2/exo2.test.ts | 232 ++++++++++++++++++++++++++++++++++++++++++ src/exo2/exo2.ts | 192 ++++++++++++++++++++++++++++++++++ src/exo2/index.ts | 1 + 3 files changed, 425 insertions(+) create mode 100644 src/exo2/exo2.test.ts create mode 100644 src/exo2/exo2.ts create mode 100644 src/exo2/index.ts diff --git a/src/exo2/exo2.test.ts b/src/exo2/exo2.test.ts new file mode 100644 index 0000000..ad02679 --- /dev/null +++ b/src/exo2/exo2.test.ts @@ -0,0 +1,232 @@ +import * as Either from 'fp-ts/lib/Either'; +import * as Option from 'fp-ts/lib/Option'; + +import { + Warrior, + Wizard, + Archer, + Damage, + noTargetFailure, + invalidTargetFailure, + checkTargetAndSmash, + checkTargetAndBurn, + checkTargetAndShoot, + smashOption, + burnOption, + shootOption, + attack, +} from './exo2'; + +describe('exo2', () => { + describe('checkTargetAndSmash', () => { + it('should return a NoTarget error if no unit is selected', () => { + const result = checkTargetAndSmash(Option.none); + const expected = Either.left( + noTargetFailure('No unit currently selected'), + ); + + expect(result).toStrictEqual(expected); + }); + + it('should return an InvalidTarget error if the wrong unit is selected', () => { + const archer = new Archer(); + const resultArcher = checkTargetAndSmash(Option.some(archer)); + const expectedArcher = Either.left( + invalidTargetFailure('Archer cannot perform smash'), + ); + + const wizard = new Wizard(); + const resultWizard = checkTargetAndSmash(Option.some(wizard)); + const expectedWizard = Either.left( + invalidTargetFailure('Wizard cannot perform smash'), + ); + + expect(resultArcher).toStrictEqual(expectedArcher); + + expect(resultWizard).toStrictEqual(expectedWizard); + }); + + it('should return the proper type of damage', () => { + const warrior = new Warrior(); + const result = checkTargetAndSmash(Option.some(warrior)); + const expected = Either.right(Damage.Physical); + + expect(result).toStrictEqual(expected); + }); + }); + + describe('checkTargetAndBurn', () => { + it('should return a NoTarget error if no unit is selected', () => { + const result = checkTargetAndBurn(Option.none); + const expected = Either.left( + noTargetFailure('No unit currently selected'), + ); + + expect(result).toStrictEqual(expected); + }); + + it('should return an InvalidTarget error if the wrong unit is selected', () => { + const warrior = new Warrior(); + const resultWarrior = checkTargetAndBurn(Option.some(warrior)); + const expectedWarrior = Either.left( + invalidTargetFailure('Warrior cannot perform burn'), + ); + + const archer = new Archer(); + const resultArcher = checkTargetAndBurn(Option.some(archer)); + const expectedArcher = Either.left( + invalidTargetFailure('Archer cannot perform burn'), + ); + + expect(resultWarrior).toStrictEqual(expectedWarrior); + + expect(resultArcher).toStrictEqual(expectedArcher); + }); + + it('should return the proper type of damage', () => { + const wizard = new Wizard(); + const result = checkTargetAndBurn(Option.some(wizard)); + const expected = Either.right(Damage.Magical); + + expect(result).toStrictEqual(expected); + }); + }); + + describe('checkTargetAndShoot', () => { + it('should return a NoTarget error if no unit is selected', () => { + const result = checkTargetAndShoot(Option.none); + const expected = Either.left( + noTargetFailure('No unit currently selected'), + ); + + expect(result).toStrictEqual(expected); + }); + + it('should return an InvalidTarget error if the wrong unit is selected', () => { + const warrior = new Warrior(); + const resultWarrior = checkTargetAndShoot(Option.some(warrior)); + const expectedWarrior = Either.left( + invalidTargetFailure('Warrior cannot perform shoot'), + ); + + const wizard = new Wizard(); + const resultWizard = checkTargetAndShoot(Option.some(wizard)); + const expectedWizard = Either.left( + invalidTargetFailure('Wizard cannot perform shoot'), + ); + + expect(resultWarrior).toStrictEqual(expectedWarrior); + + expect(resultWizard).toStrictEqual(expectedWizard); + }); + + it('should return the proper type of damage', () => { + const archer = new Archer(); + const result = checkTargetAndShoot(Option.some(archer)); + const expected = Either.right(Damage.Ranged); + + expect(result).toStrictEqual(expected); + }); + }); + + describe('smashOption', () => { + it('should return Option.none if the character is of the wrong type', () => { + const wizard = new Wizard(); + const archer = new Archer(); + + const resultWizard = smashOption(wizard); + const resultArcher = smashOption(archer); + + const expected = Option.none; + + expect(resultWizard).toStrictEqual(expected); + expect(resultArcher).toStrictEqual(expected); + }); + + it('should return Option.some(Damage.Physical) if the character is a warrior', () => { + const warrior = new Warrior(); + + const result = smashOption(warrior); + const expected = Option.some(Damage.Physical); + + expect(result).toStrictEqual(expected); + }); + }); + + describe('burnOption', () => { + it('should return Option.none if the character is of the wrong type', () => { + const warrior = new Warrior(); + const archer = new Archer(); + + const resultWarrior = burnOption(warrior); + const resultArcher = burnOption(archer); + + const expected = Option.none; + + expect(resultWarrior).toStrictEqual(expected); + expect(resultArcher).toStrictEqual(expected); + }); + + it('should return Option.some(Damage.Magical) if the character is a wizard', () => { + const wizard = new Wizard(); + + const result = burnOption(wizard); + const expected = Option.some(Damage.Magical); + + expect(result).toStrictEqual(expected); + }); + }); + + describe('shootOption', () => { + it('should return Option.none if the character is of the wrong type', () => { + const warrior = new Warrior(); + const wizard = new Wizard(); + + const resultWizard = shootOption(wizard); + const resultWarrior = shootOption(warrior); + + const expected = Option.none; + + expect(resultWarrior).toStrictEqual(expected); + expect(resultWizard).toStrictEqual(expected); + }); + + it('should return Option.some(Damage.Ranged) if the character is an archer', () => { + const archer = new Archer(); + + const result = shootOption(archer); + const expected = Option.some(Damage.Ranged); + + expect(result).toStrictEqual(expected); + }); + }); + + describe('attack', () => { + it('should return the correct number of each type of attacks', () => { + const warrior = new Warrior(); + const wizard = new Wizard(); + const archer = new Archer(); + + const army = [ + warrior, + wizard, + archer, + wizard, + wizard, + archer, + warrior, + wizard, + archer, + ]; + + const result = attack(army); + const expected = { + [Damage.Physical]: 2, + [Damage.Magical]: 4, + [Damage.Ranged]: 3, + }; + + expect(result).toStrictEqual(expected); + }); + }); +}); diff --git a/src/exo2/exo2.ts b/src/exo2/exo2.ts new file mode 100644 index 0000000..e7eecfb --- /dev/null +++ b/src/exo2/exo2.ts @@ -0,0 +1,192 @@ +// `fp-ts` training Exercice 2 +// Let's have fun with combinators! + +import * as Option from 'fp-ts/lib/Option'; +import * as Either from 'fp-ts/lib/Either'; + +import { Failure } from '../Failure'; +import { unimplemented } from '../utils'; + +/////////////////////////////////////////////////////////////////////////////// +// SETUP // +/////////////////////////////////////////////////////////////////////////////// + +// We are developping a small game, and the player can control either one of +// three types of characters, mainly differentiated by the type of damage they +// can put out. + +// Our main `Character` type is a simple union of all the concrete character +// types. +export type Character = Warrior | Wizard | Archer; + +// We have three types of `Damage`, each corresponding to a character type. +export enum Damage { + Physical = 'Physical damage', + Magical = 'Magical damage', + Ranged = 'Ranged damage', +} + +// A `Warrior` only can output physical damage. +export class Warrior { + smash() { + return Damage.Physical; + } + + toString() { + return 'Warrior'; + } +} + +// A `Wizard` only can output magical damage. +export class Wizard { + burn() { + return Damage.Magical; + } + + toString() { + return 'Wizard'; + } +} + +// An `Archer` only can output ranged damage. +export class Archer { + shoot() { + return Damage.Ranged; + } + + toString() { + return 'Archer'; + } +} + +// We also have convenient type guards to help us differentiate between +// character types when given a `Character`. + +export const isWarrior = (character: Character): character is Warrior => { + return (character as Warrior).smash !== undefined; +}; + +export const isWizard = (character: Character): character is Wizard => { + return (character as Wizard).burn !== undefined; +}; + +export const isArcher = (character: Character): character is Archer => { + return (character as Archer).shoot !== undefined; +}; + +// Finally, we have convenient and expressive error types, defining what can +// go wrong in our game: +// - the player can try to perform an action with no character targeted +// - the player can try to perform the wrong action for a character class + +export enum Exo2FailureType { + NoTarget = 'Exo2FailureType_NoTarget', + InvalidTarget = 'Exo2FailureType_InvalidTarget', +} + +export type NoTargetFailure = Failure; +export const noTargetFailure = Failure.builder(Exo2FailureType.NoTarget); + +export type InvalidTargetFailure = Failure; +export const invalidTargetFailure = Failure.builder( + Exo2FailureType.InvalidTarget, +); + +/////////////////////////////////////////////////////////////////////////////// +// EITHER // +/////////////////////////////////////////////////////////////////////////////// + +// The next three function take the currently targeted unit by the player and +// return the expected damage type if appropriate. +// +// If no unit is selected, it should return +// `Either.left(noTargetFailure('No unit currently selected'))` +// +// If a unit of the wrong type is selected, it should return +// `Either.left(invalidTargetFailure(' cannot perform '))` +// +// Otherwise, it should return `Either.right()` +// +// HINT: These functions represent the public API. But it is heavily +// recommended to break those down into smaller private functions that can be +// reused instead of doing one big `pipe` for each. +// +// HINT: `Either` has a special constructor `fromPredicate` that can accept +// a type guard such as `isWarrior` to help with type inference. +// +// HINT: Sequentially check for various possible errors is one of the most +// common operations done with the `Either` type and it is available through +// the `chain` operator and its slightly relaxed variant `chainW`. + +export const checkTargetAndSmash: ( + target: Option.Option, +) => Either.Either< + NoTargetFailure | InvalidTargetFailure, + Damage +> = unimplemented; + +export const checkTargetAndBurn: ( + target: Option.Option, +) => Either.Either< + NoTargetFailure | InvalidTargetFailure, + Damage +> = unimplemented; + +export const checkTargetAndShoot: ( + target: Option.Option, +) => Either.Either< + NoTargetFailure | InvalidTargetFailure, + Damage +> = unimplemented; + +/////////////////////////////////////////////////////////////////////////////// +// OPTION // +/////////////////////////////////////////////////////////////////////////////// + +// The next three function take a `Character` and optionnaly return the +// expected damage type if the unit match the expected character type. +// +// HINT: These functions represent the public API. But it is heavily +// recommended to break those down into smaller private functions that can be +// reused instead of doing one big `pipe` for each. +// +// HINT: `Option` has a special constructor `fromEither` that discards the +// error type. +// +// BONUS POINTS: If you properly defined small private helpers in the previous +// section, they should be easily reused for those use-cases. + +export const smashOption: ( + character: Character, +) => Option.Option = unimplemented; + +export const burnOption: ( + character: Character, +) => Option.Option = unimplemented; + +export const shootOption: ( + character: Character, +) => Option.Option = unimplemented; + +/////////////////////////////////////////////////////////////////////////////// +// ARRAY // +/////////////////////////////////////////////////////////////////////////////// + +// We now want to aggregate all the attacks of a selection of arbitrarily many +// units and know how many are Physical, Magical or Ranged. +// +// HINT: You should be able to reuse the attackOption variants defined earlier +// +// HINT: `ReadonlyArray` from `fp-ts` has a neat `filterMap` function that +// perform mapping and filtering at the same time by applying a function +// of type `A => Option` over the collection. + +export interface TotalDamage { + [Damage.Physical]: number; + [Damage.Magical]: number; + [Damage.Ranged]: number; +} + +export const attack: ( + army: ReadonlyArray, +) => TotalDamage = unimplemented; diff --git a/src/exo2/index.ts b/src/exo2/index.ts new file mode 100644 index 0000000..f9ec29f --- /dev/null +++ b/src/exo2/index.ts @@ -0,0 +1 @@ +export * from './exo2';