Commit 76bef70b authored by Colin DAMON's avatar Colin DAMON

Merge branch '101-mastermind' into 'master'

Resolve "Mastermind"

Closes #101

See merge request !77
parents 54ef0b49 5e00592b
......@@ -3,6 +3,7 @@ stages:
include:
- local: "/.gitlab-common-ci.yml"
- local: "/.gitlab-common-node-ci.yml"
- local: "/word-wrap/.gitlab-ci.yml"
- local: "/roman-numerals/.gitlab-ci.yml"
- local: "/string-calculator/.gitlab-ci.yml"
......@@ -32,3 +33,4 @@ include:
- local: "/tennis/refactoring/.gitlab-ci.yml"
- local: "/bowling-game/.gitlab-ci.yml"
- local: "/tennis/greenfield-2/.gitlab-ci.yml"
- local: "/mastermind/.gitlab-ci.yml"
......@@ -60,23 +60,3 @@ stages:
- $PROJECT_FOLDER/target/jacoco-aggregate
expire_in: 1 day
coverage: "/([^%]+) %covered/"
.node:
image: node
stage: build
tags:
- docker
before_script:
- cd $PROJECT_FOLDER
script:
- npm i
- npm test
coverage: "/Statements.*?(.+)%/"
.node12:
extends: .node
image: node:12.19.0
.node14:
extends: .node
image: node:14.14.0
stages:
- build
.node:
image: node
stage: build
tags:
- docker
before_script:
- cd $PROJECT_FOLDER
script:
- npm ci
- npm test
coverage: "/Statements.*?(.+)%/"
.node12:
extends: .node
image: node:12.19.0
.node14:
extends: .node
image: node:14.16.1
......@@ -59,6 +59,7 @@ Un kata de code est un petit exercice pensé pour s'entrainer jusqu'à maitriser
- [Trip service (TypeScript)](/trip-service-kata-typescript)
- [Roman calculator](/roman-calculator)
- [H2G2](/h2g2)
- [Mastermind](/mastermind)
## Front
......
......@@ -9,4 +9,5 @@ package-a-brief-history-of-date:
- merge_requests
changes:
- ".gitlab-common-ci.yml"
- ".gitlab-common-node-ci.yml"
- "a-brief-history-of-date/**/*"
......@@ -8,4 +8,5 @@ h2g2:
- merge_requests
changes:
- ".gitlab-common-ci.yml"
- ".gitlab-common-node-ci.yml"
- "h2g2/**/*"
This diff is collapsed.
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
],
plugins: ['prettier', '@typescript-eslint'],
rules: {
quotes: ['error', 'single', { avoidEscape: true }],
'comma-dangle': [
'error',
{
arrays: 'always-multiline',
objects: 'always-multiline',
imports: 'always-multiline',
exports: 'always-multiline',
functions: 'never',
},
],
'import/order': [
'error',
{
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
'newlines-between': 'always',
},
],
'prettier/prettier': [
'error',
{
singleQuote: true,
trailingComma: 'es5',
printWidth: 140,
},
],
},
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
},
},
'import/core-modules': ['sinon'],
},
parserOptions: {
parser: '@typescript-eslint/parser',
},
overrides: [
{
files: ['**/__tests__/*.{j,t}s?(x)', '**/*.spec.{j,t}s?(x)'],
env: {
jest: true,
},
},
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'no-unused-vars': ['off'],
'no-undef': ['off'],
},
},
],
};
mastermind:
variables:
PROJECT_FOLDER: "mastermind"
extends: .node14
only:
refs:
- master
- merge_requests
changes:
- ".gitlab-common-ci.yml"
- ".gitlab-common-node-ci.yml"
- "mastermind/**/*"
# Mastermind
Résolution en TDD et avec explications du kata [Mastermind](https://codingdojo.org/kata/Mastermind/).
- **Auteurs** : Julie CONTE et Anthony REY
- **Date** : 14/04/2021
- **Langage** : TypeScript
- **Niveau** : Moyen
module.exports = {
roots: ['<rootDir>/src'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testEnvironment: 'node',
};
This diff is collapsed.
{
"name": "mastermind",
"version": "1.0.0",
"description": "Mastermind kata",
"main": "src/index.ts",
"dependencies": {
"@types/jest": "^26.0.22",
"@typescript-eslint/eslint-plugin": "^4.21.0",
"@typescript-eslint/parser": "^4.21.0",
"eslint": "^7.23.0",
"eslint-import-resolver-typescript": "^2.4.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.3.1",
"jest": "^26.6.3",
"prettier": "^2.2.1",
"ts-jest": "^26.5.4",
"typescript": "^4.2.4"
},
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:watch:all": "jest --watchAll",
"eslint:ci": "eslint './**/*.{ts,js}'",
"eslint": "eslint './**/*.{ts,js}' --fix"
},
"author": "Anthony REY",
"license": "MIT"
}
type Color = 'purple' | 'pink' | 'red' | 'blue' | 'yellow' | 'green' | 'white' | 'brown';
type Colors = [Color, Color, Color, Color];
type State = 'playing' | 'lost' | 'win';
interface Report {
wellPlaced: number;
misplaced: number;
}
const difference = (colors: Color[], substracts: Color[]) => {
const remaining = [...substracts];
return colors.filter((current) => {
if (remaining.includes(current)) {
const toRemove = remaining.indexOf(current);
remaining.splice(toRemove, 1);
return false;
}
return true;
});
};
const intersect = (secret: Color[], guess: Color[]) => {
const remaining = [...guess];
return secret.filter((current) => {
if (remaining.includes(current)) {
const toRemove = remaining.indexOf(current);
remaining.splice(toRemove, 1);
return true;
}
return false;
});
};
class Mastermind {
private currentState: State = 'playing';
private remaining = 12;
constructor(private secret: Colors) {}
evaluate(guess: Colors): Report {
this.remaining--;
const wellPlaced = this.listWellPlaced(guess);
const rest = difference(this.secret, wellPlaced);
const guessRest = difference(guess, wellPlaced);
const misplaced = intersect(rest, guessRest).length;
const wellPlacedCount = this.countWellPlaced(guess);
this.currentState = this.evaluateState(wellPlacedCount);
return {
wellPlaced: wellPlacedCount,
misplaced,
};
}
private evaluateState(wellPlacedCount: number): State {
if (this.remaining <= 0) {
return 'lost';
}
if (wellPlacedCount === 4) {
return 'win';
}
return 'playing';
}
private listWellPlaced(guess: Color[]): Color[] {
return this.secret.filter((current, index) => current === guess[index]);
}
private countWellPlaced(guess: Color[]): number {
return this.listWellPlaced(guess).length;
}
get state(): State {
return this.currentState;
}
}
const defaultMastermind = () => new Mastermind(['purple', 'pink', 'red', 'blue']);
const evaluateToLose = (mastermind: Mastermind) => {
for (let i = 0; i < 12; i++) {
mastermind.evaluate(['red', 'red', 'red', 'red']);
}
};
describe('Mastermind', () => {
describe('Evaluation', () => {
it('Should have all color well placed', () => {
const mastermind = defaultMastermind();
expect(mastermind.evaluate(['purple', 'pink', 'red', 'blue'])).toEqual<Report>({
wellPlaced: 4,
misplaced: 0,
});
});
it('Should not have any well placed', () => {
const mastermind = defaultMastermind();
expect(mastermind.evaluate(['yellow', 'green', 'white', 'brown'])).toEqual<Report>({
wellPlaced: 0,
misplaced: 0,
});
});
it('Should have only misplaced', () => {
expect(defaultMastermind().evaluate(['pink', 'purple', 'blue', 'red'])).toEqual<Report>({
wellPlaced: 0,
misplaced: 4,
});
});
it('Should have two colors misplaced and two colors well placed', () => {
const mastermind = new Mastermind(['purple', 'pink', 'purple', 'yellow']);
expect(mastermind.evaluate(['purple', 'pink', 'yellow', 'purple'])).toEqual<Report>({
wellPlaced: 2,
misplaced: 2,
});
});
it('Should not misplace repeated secret', () => {
const mastermind = new Mastermind(['red', 'red', 'red', 'red']);
expect(mastermind.evaluate(['red', 'yellow', 'yellow', 'yellow'])).toEqual<Report>({
wellPlaced: 1,
misplaced: 0,
});
});
});
describe('State', () => {
it('Should be pending by default', () => {
expect(defaultMastermind().state).toBe<State>('playing');
});
it('Should lost after 12 tries', () => {
const mastermind = defaultMastermind();
for (let i = 0; i < 12; i++) {
mastermind.evaluate(['red', 'red', 'red', 'red']);
}
expect(mastermind.state).toBe<State>('lost');
});
it('Should win when already well placed', () => {
const mastermind = defaultMastermind();
mastermind.evaluate(['purple', 'pink', 'red', 'blue']);
expect(mastermind.state).toBe<State>('win');
});
it("Can't win when already lost", () => {
const mastermind = defaultMastermind();
evaluateToLose(mastermind);
mastermind.evaluate(['purple', 'pink', 'red', 'blue']);
expect(mastermind.state).toBe<State>('lost');
});
});
describe('Difference', () => {
it('Should not remove duplicate items', () => {
expect(difference(['purple', 'purple'], ['purple', 'pink'])).toEqual(['purple']);
});
});
});
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"outFile": "dist/main.js",
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}
......@@ -8,4 +8,5 @@ roman-calculator:
- merge_requests
changes:
- ".gitlab-common-ci.yml"
- ".gitlab-common-node-ci.yml"
- "roman-calculator/**/*"
......@@ -9,4 +9,5 @@ trip-service-kata-typescript:
- merge_requests
changes:
- ".gitlab-common-ci.yml"
- ".gitlab-common-node-ci.yml"
- "trip-service-kata-typescript/**/*"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment