Skip to content

Instantly share code, notes, and snippets.

@pphetra
Last active October 2, 2018 07:41
Show Gist options
  • Save pphetra/6de55af3d9113d907b5bd6af1beada66 to your computer and use it in GitHub Desktop.
Save pphetra/6de55af3d9113d907b5bd6af1beada66 to your computer and use it in GitHub Desktop.
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()
}
})
}
}
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: '-'
})
}
}
}
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')
})
})
})
})
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