Last active
October 2, 2018 07:41
-
-
Save pphetra/6de55af3d9113d907b5bd6af1beada66 to your computer and use it in GitHub Desktop.
This file contains 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 { Controller, Post, Get, Param, Put, Res } from "@nestjs/common"; | |
import { HangmanService } from "hangman.service"; | |
@Controller('hangman') | |
export class HangmanController { | |
constructor(private readonly hangmanService: HangmanService) {} | |
@Post() | |
newGame() { | |
return this.hangmanService.createNewGame() | |
} | |
@Get(':id') | |
currentState(@Param('id') gameId) { | |
return this.hangmanService.currentState(gameId) | |
} | |
@Put(':id/:letter') | |
guess(@Param('id') gameId, @Param('letter') letter) { | |
return this.hangmanService.guess(gameId, letter) | |
} | |
@Get(':id/timer') | |
stream(@Param('id') gameId, @Res() res) { | |
this.hangmanService.subscribe(gameId, s => { | |
if (s.status === 'in-progress') { | |
res.write(`{ data: {lifeLeft: ${s.lifeLeft}, timeLeft: ${s.timeLeft}, status: ${s.status}}}\n`) | |
} else { | |
res.end() | |
} | |
}) | |
} | |
} |
This file contains 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 { Injectable } from "@nestjs/common"; | |
import { DictionaryService } from "dictionary.service"; | |
import { State, HangmanGame } from "hangman"; | |
import { Observable } from "rxjs"; | |
import { first } from "rxjs/operators"; | |
const games = new Map<string, HangmanGame>() | |
var currentGameId = 1 | |
@Injectable() | |
export class HangmanService { | |
constructor(private readonly dictService: DictionaryService) {} | |
createNewGame(): Observable<State> { | |
const secretWord = this.dictService.randomWord() | |
const gameId = `${currentGameId++}` | |
const game = new HangmanGame(secretWord, gameId) | |
games.set(gameId, game) | |
return game.state.pipe( | |
first() | |
) | |
} | |
getGameState(gameId: string): Observable<State> { | |
return games.has(gameId) ? games.get(gameId).currentState() : null | |
} | |
getGame(gameId: string): HangmanGame { | |
return games.get(gameId) | |
} | |
currentState(gameId: string): Observable<State>{ | |
if (this.isExist(gameId)) { | |
return games.get(gameId).currentState().pipe(first()) | |
} | |
return null | |
} | |
guess(gameId: string, letter: string): Observable<State>{ | |
if (games.has(gameId)) { | |
const game = games.get(gameId) | |
return game.guess(letter).pipe(first()) | |
} | |
return null | |
} | |
isExist(gameId: string): boolean { | |
return games.has(gameId) | |
} | |
subscribe(gameId: string, cb: (State)=>void) { | |
if (games.has(gameId)) { | |
this.getGame(gameId).currentState().subscribe(cb) | |
} else { | |
cb({ | |
status: '-' | |
}) | |
} | |
} | |
} |
This file contains 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 { 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') | |
}) | |
}) | |
}) | |
}) |
This file contains 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 { Subject, Observable, interval, merge } from "rxjs"; | |
import { map, scan } 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 { | |
actionSource: Subject<HangmanAction> | |
state: Observable<State> | |
currentStateSubject: 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.currentStateSubject = new Subject() // hot observable | |
this.state.subscribe(this.currentStateSubject) // so current state can be multicast | |
} | |
guess(letter: string): Observable<State> { | |
this.actionSource.next(guessAction(letter)) | |
return this.currentState() | |
} | |
currentState(): Observable<State> { | |
return this.currentStateSubject | |
} | |
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 | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment