Skip to content

Instantly share code, notes, and snippets.

@imkrish
Created October 7, 2018 20:40
Show Gist options
  • Select an option

  • Save imkrish/4aaad94ac8ff4cc12ffc0f0e7d8ff485 to your computer and use it in GitHub Desktop.

Select an option

Save imkrish/4aaad94ac8ff4cc12ffc0f0e7d8ff485 to your computer and use it in GitHub Desktop.
import { v4 } from 'uuid'
import { random } from 'lodash'
import { merge } from 'ramda'
import { GameStatus, IGameState } from './model'
export class Helper {
static secretWords = [
'adventurous',
'courageous',
'extramundane',
'generous',
'intransigent',
'sympathetic',
'vagarious',
'witty',
]
static getInitGameState = (secretWord: string, lives: number) => {
const secretWordLength = secretWord.length
const { knownSecretWord, selectedLetters } = Helper.getInitKnownLetters(
secretWord
)
const initGameState: IGameState = {
id: v4(),
status: GameStatus.IN_PROGRESS,
selectedLetters,
lifeLeft: lives,
secretWordLength,
knownSecretWord,
timeLeft: 5,
}
return initGameState
}
static getSecretWord = () => {
const numSecretWords = Helper.secretWords.length
const randomIdx = random(0, numSecretWords - 1)
return Helper.secretWords[randomIdx]
}
static getNextGameState = (
currentGameState: IGameState,
secretWord: string,
letter: string,
timeCount: number
): IGameState => {
const {
id,
selectedLetters,
lifeLeft,
secretWordLength,
knownSecretWord,
timeLeft,
} = currentGameState
const newSelectedLetters = selectedLetters.concat(letter)
const correctLetter = secretWord.includes(letter)
const newLifeLeft = correctLetter ? lifeLeft : lifeLeft - 1
const newTimLeft = 5 - timeCount
const newKnownSecretWord = Helper.fillCorrectLetters(
secretWord,
knownSecretWord,
letter
)
const win = Helper.isWin(newKnownSecretWord)
const lose = newLifeLeft === 0
const newStatus = win
? GameStatus.WON
: lose
? GameStatus.LOSE
: GameStatus.IN_PROGRESS
return {
id,
status: newStatus,
selectedLetters: newSelectedLetters,
lifeLeft: newLifeLeft,
secretWordLength,
knownSecretWord: newKnownSecretWord,
timeLeft: newTimLeft,
}
}
static getNextGameStateOnlyDecreaseLife = (currentGameState: IGameState) => {
const { lifeLeft } = currentGameState
const newLifeLeft = lifeLeft - 1
const lose = newLifeLeft === 0
const newStatus = lose ? GameStatus.LOSE : GameStatus.IN_PROGRESS
return merge(currentGameState, {
status: newStatus,
lifeLeft: newLifeLeft,
timeLeft: 5,
})
}
static getNextGameStateOnlyDecreaseTimeLeft = (
currentGameState: IGameState,
timeCount: number
) => {
return merge(currentGameState, {
timeLeft: 5 - timeCount,
})
}
static fillCorrectLetters = (
word: string,
filledWord: string,
letter: string
) => {
const newFilledWord = [...word].reduce(
(filedWord, char, idx) => {
if (char === letter) {
filedWord[idx] = char
}
return filedWord
},
[...filledWord]
)
return newFilledWord.join('')
}
static isWin = (knowSecretWord: string) => {
return [...knowSecretWord].every(char => char !== '_')
}
static getInitKnownLetters = (word: string) => {
const numLetters = word.length
const numLettersToShow = Math.ceil(numLetters / 5)
const randomLetters = Helper.getRandomLetters(word, numLettersToShow)
const knownLetters = [...word].map(
letter => (randomLetters.includes(letter) ? letter : '_')
)
return {
knownSecretWord: knownLetters.join(''),
selectedLetters: randomLetters,
}
}
static getRandomLetters = (
word: string,
numLettersToShow: number
): string[] => {
const randomLetters: string[] = []
const numLetters = word.length
while (randomLetters.length < numLettersToShow) {
const randomIdx = random(0, numLetters - 1)
const letter = word[randomIdx]
const letterAlreadyExisted = randomLetters.includes(letter)
if (!letterAlreadyExisted) {
randomLetters.push(letter)
}
}
return randomLetters
}
}
export interface IGameState {
id: string
status: GameStatus
selectedLetters: string[]
lifeLeft: number
secretWordLength: number
knownSecretWord: string
timeLeft: number
}
export enum GameStatus {
IN_PROGRESS = 'in-progress',
WON = 'won',
LOSE = 'lose',
}
import { Helper } from './helper'
import { Observable, combineLatest, Subject } from 'rxjs'
import {
refCount,
scan,
startWith,
switchMapTo,
takeUntil,
publishBehavior,
} from 'rxjs/operators'
import { GameStatus, IGameState } from './model'
import { partial } from 'ramda'
export function reactiveHangman(
secretWord: string,
letter$: Observable<string>,
timer$: Observable<number>
) {
const initGameState = Helper.getInitGameState(secretWord, 7)
const resetTimer$ = new Subject<void>()
const gameDone$ = new Subject<void>()
const resetableTimer$ = resetTimer$.pipe(
startWith(0),
switchMapTo(timer$),
takeUntil(gameDone$)
)
const letter$timer$ = combineLatest(
letter$.pipe(startWith('')),
resetableTimer$
)
const updateState = partial(update, [secretWord, resetTimer$, gameDone$])
const gameState$: Observable<IGameState> = letter$timer$.pipe(
scan(updateState, initGameState)
)
const hotGameState$ = gameState$.pipe(
publishBehavior(initGameState),
refCount()
)
return {
gameState$: hotGameState$,
gameId: initGameState.id,
}
}
const update = (
secretWord: string,
resetTimer$: Subject<void>,
gameDone$: Subject<void>,
gameState: IGameState,
[letter, timeCount]: [string, number]
): any => {
const { selectedLetters, status } = gameState
// Complete all streams when the game is finished
const gameDone = status === GameStatus.WON || status === GameStatus.LOSE
if (gameDone) {
gameDone$.next()
return gameState
}
// 5 seconds passed
if (timeCount === 5) {
resetTimer$.next()
return Helper.getNextGameStateOnlyDecreaseLife(gameState)
}
// Game start with '' string
// Or the letter is already selected
const newLetter = !selectedLetters.includes(letter)
if (letter === '' || !newLetter) {
return Helper.getNextGameStateOnlyDecreaseTimeLeft(gameState, timeCount)
}
// Unselected letter, reset the time
resetTimer$.next()
return Helper.getNextGameState(gameState, secretWord, letter, timeCount)
}
import { GameStatus } from './hangman/model'
import { Helper } from './hangman/helper'
import { timer, Subject } from 'rxjs'
import express from 'express'
import { reactiveHangman } from './hangman/reactive-hangman'
import { StateManager } from './hangman/state-manager'
import { take, distinctUntilKeyChanged } from 'rxjs/operators'
const app = express()
const port = 3000
const hangmanRouter = express.Router()
hangmanRouter.get('/', (req, res) => {
const secretWord = Helper.getSecretWord()
const timer$ = timer(0, 1000)
const letter$ = new Subject<string>()
const { gameState$, gameId } = reactiveHangman(secretWord, letter$, timer$)
StateManager.setState$(gameId, gameState$)
StateManager.setLetter$(gameId, letter$)
// Subscribe hot observable (to maintain the refCount)
gameState$.subscribe()
gameState$.pipe(take(1)).subscribe(state => res.json(state))
})
hangmanRouter.get('/:id', (req, res) => {
const { id } = req.params
const gameState$ = StateManager.getState$(id)
gameState$.pipe(take(1)).subscribe(state => res.json(state))
})
hangmanRouter.get('/:id/timer', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
})
const { id } = req.params
const gameState$ = StateManager.getState$(id)
const subscription = gameState$
.pipe(distinctUntilKeyChanged('timeLeft'))
.subscribe(gameState => {
const { id, timeLeft, lifeLeft, status } = gameState
res.write(
`id: "${id}"\n, event: "time-spent"\n, data: {timeLeft: ${timeLeft}, lifeLeft: ${lifeLeft}, status: ${status}}\n\n`
)
if (status !== GameStatus.IN_PROGRESS) {
res.end()
}
})
req.on('close', () => subscription.unsubscribe())
})
hangmanRouter.get('/:id/:letter', (req, res) => {
const { id, letter } = req.params
const letterSubject$ = StateManager.letter$Dict[id]
letterSubject$.next(letter)
const gameState$ = StateManager.getState$(id)
gameState$.pipe(take(1)).subscribe(state => res.json(state))
})
app.use('/hangman', hangmanRouter)
app.listen(port, () => console.log(`Hangman app listening on port ${port}!`))
import { IGameState } from './model'
import { Subject, Observable } from 'rxjs'
export class StateManager {
static state$Dict: { [key: string]: Observable<IGameState> } = {}
static letter$Dict: { [key: string]: Subject<string> } = {}
static setState$ = (key: string, state: Observable<IGameState>) => {
StateManager.state$Dict[key] = state
}
static getState$ = (key: string) => {
return StateManager.state$Dict[key]
}
static setLetter$ = (key: string, letter$: Subject<string>) => {
StateManager.letter$Dict[key] = letter$
}
static getLetter$ = (key: string) => {
return StateManager.letter$Dict[key]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment