✨ Add exercice number 2
This commit is contained in:
parent
8e466d82f3
commit
faf9d64553
3 changed files with 425 additions and 0 deletions
232
src/exo2/exo2.test.ts
Normal file
232
src/exo2/exo2.test.ts
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
192
src/exo2/exo2.ts
Normal file
192
src/exo2/exo2.ts
Normal file
|
@ -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<Exo2FailureType.NoTarget>;
|
||||
export const noTargetFailure = Failure.builder(Exo2FailureType.NoTarget);
|
||||
|
||||
export type InvalidTargetFailure = Failure<Exo2FailureType.InvalidTarget>;
|
||||
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('<unit_type> cannot perform <action>'))`
|
||||
//
|
||||
// Otherwise, it should return `Either.right(<expected_damage_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: `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<Character>,
|
||||
) => Either.Either<
|
||||
NoTargetFailure | InvalidTargetFailure,
|
||||
Damage
|
||||
> = unimplemented;
|
||||
|
||||
export const checkTargetAndBurn: (
|
||||
target: Option.Option<Character>,
|
||||
) => Either.Either<
|
||||
NoTargetFailure | InvalidTargetFailure,
|
||||
Damage
|
||||
> = unimplemented;
|
||||
|
||||
export const checkTargetAndShoot: (
|
||||
target: Option.Option<Character>,
|
||||
) => 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<Damage> = unimplemented;
|
||||
|
||||
export const burnOption: (
|
||||
character: Character,
|
||||
) => Option.Option<Damage> = unimplemented;
|
||||
|
||||
export const shootOption: (
|
||||
character: Character,
|
||||
) => Option.Option<Damage> = 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<B>` over the collection.
|
||||
|
||||
export interface TotalDamage {
|
||||
[Damage.Physical]: number;
|
||||
[Damage.Magical]: number;
|
||||
[Damage.Ranged]: number;
|
||||
}
|
||||
|
||||
export const attack: (
|
||||
army: ReadonlyArray<Character>,
|
||||
) => TotalDamage = unimplemented;
|
1
src/exo2/index.ts
Normal file
1
src/exo2/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './exo2';
|
Reference in a new issue