Skip to content

Instantly share code, notes, and snippets.

@pphetra
Last active October 2, 2018 08:13
Show Gist options
  • Save pphetra/4365ea9ebb0e2bd10a4b21d071ea8ba8 to your computer and use it in GitHub Desktop.
Save pphetra/4365ea9ebb0e2bd10a4b21d071ea8ba8 to your computer and use it in GitHub Desktop.
import { Subject, Observable, interval, merge } from "rxjs";
import { map, scan, share } from 'rxjs/operators';
export const MAX_LIFE = 7
export const MAX_TIME = 5
export type State = {
gameId: string,
status: string,
knownSecretWord: Array<string>,
selectedLetters: Array<string>,
secretWordLength: number,
lifeLeft: number,
timeLeft: number
}
export type HangmanAction = {
type: string
data: any
}
const guessAction = (guessCh: string) => {
return {
type: 'guess',
data: guessCh
}
}
const tickAction = () => {
return {
type: 'tick',
data: ''
}
}
export class HangmanGame {
private _actionSource: Subject<HangmanAction>
private _state: Observable<State>
private _currentState: Subject<State>
constructor(secretWord: string, gameId: string) {
const initState = {
gameId: gameId,
status: 'in-progress',
knownSecretWord: secretWord.split('').map(_ => '_'),
selectedLetters: [],
secretWordLength: secretWord.length,
lifeLeft: MAX_LIFE,
timeLeft: MAX_TIME
}
this._actionSource = new Subject<HangmanAction>()
this._state = merge (
map(_ => tickAction())(interval(1000)),
this._actionSource
).pipe(
scan(this.createReducer(secretWord), initState)
) // cold observable
this._currentState = new Subject<State>()
this._state.subscribe(this._currentState) // make it hot
}
guess(letter: string): Observable<State> {
this._actionSource.next(guessAction(letter))
return this.currentState()
}
currentState(): Observable<State> {
return this._currentState
}
private createReducer(secretWord: string): (State, HangmanAction) => State {
const word = secretWord.split('');
return (state: State, action: HangmanAction): State => {
if (action.type === 'guess') {
const guessCh = action.data
const selectedLettersSet = new Set(state.selectedLetters)
if (selectedLettersSet.has(guessCh) || state.status === 'loss') {
return state
}
selectedLettersSet.add(guessCh)
const found = secretWord.indexOf(guessCh) >= 0
const lifeLeft = state.lifeLeft - (found ? 0 : 1)
const knownSecretWord = word.map(ch => selectedLettersSet.has(ch) ? ch : '_')
const status = lifeLeft <= 0 ? 'loss' : (
knownSecretWord.some(ch => ch == '_') ? 'in-progress' : 'win'
)
return {
...state,
selectedLetters: Array.from(selectedLettersSet),
lifeLeft,
timeLeft: found ? MAX_TIME : state.timeLeft,
knownSecretWord,
status
}
} else if(action.type === 'tick') {
if (state.status === 'loss') {
return state
}
let timeLeft = (state.status === 'in-progress') ? state.timeLeft - 1 : state.timeLeft
let lifeLeft = state.lifeLeft
if (timeLeft === 0) {
lifeLeft--
timeLeft = MAX_TIME
}
const status = state.lifeLeft <= 0 ? 'loss' : state.status
return {
...state,
timeLeft,
lifeLeft,
status
}
}
}
}
}
import { HangmanGame, MAX_LIFE, State } from './hangman'
import { Test } from '@nestjs/testing';
describe('Hangman', () => {
let hangman: HangmanGame
beforeEach(() => {
hangman = new HangmanGame('hello', '1')
})
describe('after create a new game', () => {
it('state.gameId = 1', () => {
hangman.currentState().subscribe(state=> {
expect(state.gameId).toBe('1')
})
})
it('state.status should be in-progress', () => {
hangman.currentState().subscribe(state=> {
expect(state.status).toBe('in-progress')
})
})
it('state.knownSecretWord size must equal to secretWordLength', () => {
hangman.currentState().subscribe(state=> {
expect(state.knownSecretWord.length).toBe(state.secretWordLength)
})
})
it('state.leftLeft should equal to MAX_LIFE', () => {
hangman.currentState().subscribe(state=> {
expect(state.lifeLeft).toBe(MAX_LIFE)
})
})
it('state.knonwSecretWord', () => {
hangman.currentState().subscribe(state=> {
expect(state.knownSecretWord).toEqual(['_', '_', '_', '_', '_'])
})
})
})
describe('when feed letters', () => {
it('-> h', () => {
hangman.guess('h')
hangman.currentState().subscribe(s => {
expect(s.selectedLetters.length).toBe(1)
expect(s.lifeLeft).toBe(MAX_LIFE)
expect(s.knownSecretWord).toEqual(['h', '_', '_', '_', '_'])
})
})
it ('-> h a', () => {
hangman.guess('h')
hangman.guess('a')
hangman.currentState().subscribe(s => {
expect(s.selectedLetters.length).toBe(2)
expect(s.lifeLeft).toBe(MAX_LIFE - 1)
expect(s.knownSecretWord).toEqual(['h', '_', '_', '_', '_'])
})
})
it ('-> h a a', () => {
hangman.guess('h')
hangman.guess('a')
hangman.currentState().subscribe(s => {
expect(s.selectedLetters.length).toBe(2)
expect(s.lifeLeft).toBe(MAX_LIFE - 1)
expect(s.knownSecretWord).toEqual(['h', '_', '_', '_', '_'])
})
})
it ('-> h a l', () => {
hangman.guess('h')
hangman.guess('a')
hangman.guess('l')
hangman.currentState().subscribe(s => {
expect(s.selectedLetters.length).toBe(3)
expect(s.lifeLeft).toBe(MAX_LIFE - 1)
expect(s.knownSecretWord).toEqual(['h', '_', 'l', 'l', '_'])
expect(s.status).toBe('in-progress')
})
})
it ('-> h a l x y z b c g', () => {
hangman.guess('h')
hangman.guess('a')
hangman.guess('l')
hangman.guess('x')
hangman.guess('y')
hangman.guess('z')
hangman.guess('b')
hangman.guess('c')
hangman.guess('m')
hangman.currentState().subscribe(s => {
expect(s.selectedLetters.length).toBe(9)
expect(s.lifeLeft).toBe(MAX_LIFE - 7)
expect(s.knownSecretWord).toEqual(['h', '_', 'l', 'l', '_'])
expect(s.status).toBe('loss')
})
})
})
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment