Last active
August 5, 2019 20:24
-
-
Save dtinth/383277d0b8a52d13f8214042b256d6fb to your computer and use it in GitHub Desktop.
Reactive Hangman Kata in TypeScript + RxJS — https://forums.bigbears.io/t/hangman-the-series-2-reactive-hangman/391
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as Immutable from 'immutable' | |
import * as rxjs from 'rxjs' | |
import { scan, startWith, map } from 'rxjs/operators' | |
export function reactiveHangman( | |
secretWord: string, | |
letters: rxjs.Observable<string> | |
): rxjs.Observable<Output> { | |
const initialState = initialize(secretWord) | |
return letters.pipe( | |
scan(update, initialState), | |
startWith(initialState), | |
map(exportState) | |
) | |
} | |
export type Output = { | |
status: Status | |
selectedLetters: string[] | |
lifeLeft: number | |
secretWordLength: number | |
knownSecretWord: string | |
} | |
type State = { | |
lifeLeft: number | |
secretWordLength: number | |
selectedLetters: Immutable.OrderedSet<string> | |
remainingSecretLetters: Immutable.Map<string, number[]> | |
knownSecretWord: string | |
} | |
type Status = 'in-progress' | 'win' | 'lose' | |
function initialize(secretWord: string): State { | |
return { | |
lifeLeft: 7, | |
secretWordLength: secretWord.length, | |
selectedLetters: Immutable.OrderedSet(), | |
remainingSecretLetters: Immutable.Map<string, number[]>().withMutations( | |
m => { | |
for (const [index, char] of [...secretWord].entries()) { | |
if (!m.has(char)) m.set(char, []) | |
m.get(char).push(index) | |
} | |
} | |
), | |
knownSecretWord: '_'.repeat(secretWord.length) | |
} | |
} | |
function getStatus(state: State): Status { | |
return state.remainingSecretLetters.isEmpty() | |
? 'win' | |
: state.lifeLeft <= 0 | |
? 'lose' | |
: 'in-progress' | |
} | |
function update(state: State, guess: string): State { | |
if (getStatus(state) !== 'in-progress') return state | |
if (state.selectedLetters.has(guess)) return state | |
if (!state.remainingSecretLetters.has(guess)) { | |
return { | |
...state, | |
lifeLeft: state.lifeLeft - 1, | |
selectedLetters: state.selectedLetters.add(guess) | |
} | |
} | |
// correct guess | |
const indices = new Set(state.remainingSecretLetters.get(guess)) | |
return { | |
...state, | |
remainingSecretLetters: state.remainingSecretLetters.delete(guess), | |
knownSecretWord: [...state.knownSecretWord] | |
.map((current, i) => (indices.has(i) ? guess : current)) | |
.join(''), | |
selectedLetters: state.selectedLetters.add(guess) | |
} | |
} | |
function exportState(state: State): Output { | |
return { | |
status: getStatus(state), | |
selectedLetters: state.selectedLetters.toArray(), | |
lifeLeft: state.lifeLeft, | |
secretWordLength: state.secretWordLength, | |
knownSecretWord: state.knownSecretWord | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { reactiveHangman } from './Hangman' | |
import * as rxjs from 'rxjs' | |
import { toArray } from 'rxjs/operators' | |
// @ts-ignore | |
import markdownTable from 'markdown-table' | |
test('win', async () => { | |
expect( | |
await OutputList.of(reactiveHangman('bigbear', rxjs.of(...'boigaeyr'))) | |
).toMatchInlineSnapshot(` | |
| status | selectedLetters | lifeLeft | secretWordLength | knownSecretWord | | |
| ------------- | --------------------------------- | -------- | ---------------- | --------------- | | |
| "in-progress" | [] | 7 | 7 | "_______" | | |
| "in-progress" | ["b"] | 7 | 7 | "b__b___" | | |
| "in-progress" | ["b","o"] | 6 | 7 | "b__b___" | | |
| "in-progress" | ["b","o","i"] | 6 | 7 | "bi_b___" | | |
| "in-progress" | ["b","o","i","g"] | 6 | 7 | "bigb___" | | |
| "in-progress" | ["b","o","i","g","a"] | 6 | 7 | "bigb_a_" | | |
| "in-progress" | ["b","o","i","g","a","e"] | 6 | 7 | "bigbea_" | | |
| "in-progress" | ["b","o","i","g","a","e","y"] | 5 | 7 | "bigbea_" | | |
| "win" | ["b","o","i","g","a","e","y","r"] | 5 | 7 | "bigbear" | | |
`) | |
}) | |
test('lose', async () => { | |
expect( | |
await OutputList.of(reactiveHangman('bigbear', rxjs.of(...'boaenutzxv'))) | |
).toMatchInlineSnapshot(` | |
| status | selectedLetters | lifeLeft | secretWordLength | knownSecretWord | | |
| ------------- | ----------------------------------------- | -------- | ---------------- | --------------- | | |
| "in-progress" | [] | 7 | 7 | "_______" | | |
| "in-progress" | ["b"] | 7 | 7 | "b__b___" | | |
| "in-progress" | ["b","o"] | 6 | 7 | "b__b___" | | |
| "in-progress" | ["b","o","a"] | 6 | 7 | "b__b_a_" | | |
| "in-progress" | ["b","o","a","e"] | 6 | 7 | "b__bea_" | | |
| "in-progress" | ["b","o","a","e","n"] | 5 | 7 | "b__bea_" | | |
| "in-progress" | ["b","o","a","e","n","u"] | 4 | 7 | "b__bea_" | | |
| "in-progress" | ["b","o","a","e","n","u","t"] | 3 | 7 | "b__bea_" | | |
| "in-progress" | ["b","o","a","e","n","u","t","z"] | 2 | 7 | "b__bea_" | | |
| "in-progress" | ["b","o","a","e","n","u","t","z","x"] | 1 | 7 | "b__bea_" | | |
| "lose" | ["b","o","a","e","n","u","t","z","x","v"] | 0 | 7 | "b__bea_" | | |
`) | |
}) | |
test('it ignores duplicated letters', async () => { | |
expect(await OutputList.of(reactiveHangman('bigbear', rxjs.of(...'bbbqqq')))) | |
.toMatchInlineSnapshot(` | |
| status | selectedLetters | lifeLeft | secretWordLength | knownSecretWord | | |
| ------------- | --------------- | -------- | ---------------- | --------------- | | |
| "in-progress" | [] | 7 | 7 | "_______" | | |
| "in-progress" | ["b"] | 7 | 7 | "b__b___" | | |
| "in-progress" | ["b"] | 7 | 7 | "b__b___" | | |
| "in-progress" | ["b"] | 7 | 7 | "b__b___" | | |
| "in-progress" | ["b","q"] | 6 | 7 | "b__b___" | | |
| "in-progress" | ["b","q"] | 6 | 7 | "b__b___" | | |
| "in-progress" | ["b","q"] | 6 | 7 | "b__b___" | | |
`) | |
}) | |
test('it stops processing when won', async () => { | |
expect( | |
await OutputList.of(reactiveHangman('bigbear', rxjs.of(...'bigearzxcv'))) | |
).toMatchInlineSnapshot(` | |
| status | selectedLetters | lifeLeft | secretWordLength | knownSecretWord | | |
| ------------- | ------------------------- | -------- | ---------------- | --------------- | | |
| "in-progress" | [] | 7 | 7 | "_______" | | |
| "in-progress" | ["b"] | 7 | 7 | "b__b___" | | |
| "in-progress" | ["b","i"] | 7 | 7 | "bi_b___" | | |
| "in-progress" | ["b","i","g"] | 7 | 7 | "bigb___" | | |
| "in-progress" | ["b","i","g","e"] | 7 | 7 | "bigbe__" | | |
| "in-progress" | ["b","i","g","e","a"] | 7 | 7 | "bigbea_" | | |
| "win" | ["b","i","g","e","a","r"] | 7 | 7 | "bigbear" | | |
| "win" | ["b","i","g","e","a","r"] | 7 | 7 | "bigbear" | | |
| "win" | ["b","i","g","e","a","r"] | 7 | 7 | "bigbear" | | |
| "win" | ["b","i","g","e","a","r"] | 7 | 7 | "bigbear" | | |
| "win" | ["b","i","g","e","a","r"] | 7 | 7 | "bigbear" | | |
`) | |
}) | |
test('it stops processing when lose', async () => { | |
expect( | |
await OutputList.of(reactiveHangman('bigbear', rxjs.of(...'qazwsxedcrfv'))) | |
).toMatchInlineSnapshot(` | |
| status | selectedLetters | lifeLeft | secretWordLength | knownSecretWord | | |
| ------------- | ------------------------------------- | -------- | ---------------- | --------------- | | |
| "in-progress" | [] | 7 | 7 | "_______" | | |
| "in-progress" | ["q"] | 6 | 7 | "_______" | | |
| "in-progress" | ["q","a"] | 6 | 7 | "_____a_" | | |
| "in-progress" | ["q","a","z"] | 5 | 7 | "_____a_" | | |
| "in-progress" | ["q","a","z","w"] | 4 | 7 | "_____a_" | | |
| "in-progress" | ["q","a","z","w","s"] | 3 | 7 | "_____a_" | | |
| "in-progress" | ["q","a","z","w","s","x"] | 2 | 7 | "_____a_" | | |
| "in-progress" | ["q","a","z","w","s","x","e"] | 2 | 7 | "____ea_" | | |
| "in-progress" | ["q","a","z","w","s","x","e","d"] | 1 | 7 | "____ea_" | | |
| "lose" | ["q","a","z","w","s","x","e","d","c"] | 0 | 7 | "____ea_" | | |
| "lose" | ["q","a","z","w","s","x","e","d","c"] | 0 | 7 | "____ea_" | | |
| "lose" | ["q","a","z","w","s","x","e","d","c"] | 0 | 7 | "____ea_" | | |
| "lose" | ["q","a","z","w","s","x","e","d","c"] | 0 | 7 | "____ea_" | | |
`) | |
}) | |
class OutputList { | |
constructor(public items: any[]) {} | |
static async of(observable: rxjs.Observable<any>) { | |
return new this(await observable.pipe(toArray()).toPromise()) | |
} | |
} | |
expect.addSnapshotSerializer({ | |
test(val) { | |
return val instanceof OutputList | |
}, | |
print(val, serialize, indent) { | |
const list = (val as OutputList).items | |
const keys = Object.keys(list[0]) | |
return markdownTable([ | |
keys, | |
...list.map((o: any) => keys.map(k => JSON.stringify(o[k]))) | |
]) | |
} | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment