You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Letter z would never be generated.
1.1 This is easily fixable by adding +1 inside multiplication with Math.random() ×( ... + 1)
If the same letter appears multiple times in sequence, user can delete them all by a single keystroke
2.1 This can be simulated by making randomLetter return one character: randomLetter = () => 'a'
2.2 This is not easy to fix, as that would require major refactoring. The problem comes from using combineLatest in the main logic. combineLatest cannot determine, which stream triggered the new emission - new key pressed or new letter arriving.
Here is a quick-and-dirty solution that should fix the problems (Also on StackBlitz):
import{asyncScheduler,BehaviorSubject,defer,EMPTY,fromEvent,iif,merge,Observable,of,timer}from"rxjs"import{catchError,filter,map,observeOn,repeatWhen,retryWhen,scan,switchMap,switchMapTo,takeWhile,tap}from"rxjs/operators"constDEFAULTS={boardWidth: 30,boardHeight: 30,levelHits: 10,maxLevels: 8,newLetterProbability: 0.5,initialDelay: 400,boardId: 'AlphabetInvasionBoard',levelAccelerationFactor: 0.8,delayBetweenLevels: 2000,}typeCSSStyleName=Exclude<keyofCSSStyleDeclaration,'length'|'parentRule'>&stringtypeCSSStyle=Partial<Record<CSSStyleName,string>>/** Letter is a character with position information */interfaceLetter{char: stringoffset: number// Horizontal offset from left side of the boardheight: number// Height from bottom of the boardupper?: boolean// Whether the letter is uppercase}interfaceLetterSequence{level: numberletters: Letter[]count: number// Number of letters hit - typed correctly}interfaceLetterSequenceWithUpdateextendsLetterSequence{update: boolean// Does this sequence contains any new information or not}exportfunctionmakeGame({
boardWidth =DEFAULTS.boardWidth,
boardHeight =DEFAULTS.boardHeight,
levelHits =DEFAULTS.levelHits,
newLetterProbability =DEFAULTS.newLetterProbability,
initialDelay =DEFAULTS.initialDelay,
boardId =DEFAULTS.boardId,
maxLevels =DEFAULTS.maxLevels,
levelAccelerationFactor =DEFAULTS.levelAccelerationFactor,
delayBetweenLevels =DEFAULTS.delayBetweenLevels,}={}){/** Pseudo-random integer from a specific range */functionrandom(from: number,to: number){returnfrom+Math.floor(Math.random()*(to-from+1))}/** Random character - without position information */functionrandomChar(){returnMath.random()<.3 ? {char: String.fromCharCode(random('A'.charCodeAt(0),'Z'.charCodeAt(0))),upper: true} : {char: String.fromCharCode(random('a'.charCodeAt(0),'z'.charCodeAt(0))),upper: false}}/** Random letter - includes position information */functionrandomLetter(): Letter{constchar=randomChar()return{char: char.char,upper: char.upper,offset: random(0,boardWidth-1),height: boardHeight}}/** Move a single letter down by a single row */functionmoveLetter(letter: Letter): Letter{return{
...letter,height: letter.height-1}}/** Emits keystrokes */constkeydown$=fromEvent(document,'keydown')asunknownasObservable<KeyboardEvent>constkeystrokes$=keydown$.pipe(map(e=>e.key))functionlettersRemaining(gap: number,level: number){returnmerge(keystrokes$,timer(delayBetweenLevels,gap)).pipe(scan((sequence,keyOrTick)=>{if(typeofkeyOrTick==='string'){constfound=sequence.letters[0]?.char===keyOrTickreturn{
level,letters: sequence.letters.slice(+found),count: sequence.count+(+found),update: found}}else{constneedMoreLetters=sequence.count+sequence.letters.length<levelHitsconstletters=sequence.letters.map(moveLetter).concat(Math.random()<newLetterProbability&&needMoreLetters ? [randomLetter()] : [])return{
level,
letters,count: sequence.count,update: true}}},{letters: [],count: 0,level: 0,update: false}asLetterSequenceWithUpdate),filter(state=>state.update),// Filter out sequences without any new information to avoid unnecessary redrawmap<LetterSequenceWithUpdate,LetterSequence>(({level, letters, count})=>({level, letters, count})),// Get rid of `update` - no longer neededtakeWhile(state=>!state.letters.length||state.letters[0].height>=0,false))}functiongetBoard(){functionmakeNewBoard(){constboard=document.createElement('div')board.setAttribute('id',boardId)constboardStyle: CSSStyle={border: '2px solid grey',margin: '16px',padding: '16px',whiteSpace: 'pre',fontFamily: 'monospace',overflow: 'hidden',display: 'inline-block',fontSize: '24px',}constboardStyleDOM=board.styleasunknownasCSSStyleObject.entries(boardStyle).forEach(([styleName,styleValue])=>boardStyleDOM[styleNameasCSSStyleName]=styleValue)document.body.appendChild(board)returnboard}returndocument.getElementById(boardId)??makeNewBoard()}functionletterHtml(letter: Letter){if(letter.upper){return`<span style="color:red;font-weight:700">${letter.char}</span>`}returnletter.char}functiondrawBoard(board: HTMLElement){returnfunction(letterset: Letter[],boardWidth: number,gameProgress: number,levelPprogress: number){constgameProgressString='GAME',levelProgressString='Level'consttoGoPrefix='<span style="opacity:.5;color:#ccc">',toGoSuffix='</span>'constgameDone=gameProgressString.repeat(boardWidth).slice(0,boardWidth*gameProgress)constgameToGo=gameProgressString.repeat(boardWidth).slice(0,boardWidth-gameDone.length)constgameProg=gameDone+toGoPrefix+gameToGo+toGoSuffix+'\n'constlevelDone=levelProgressString.repeat(boardWidth).slice(0,boardWidth*levelPprogress)constlevelToGo=levelProgressString.repeat(boardWidth).slice(0,boardWidth-levelDone.length)constlevelProg=levelDone+toGoPrefix+levelToGo+toGoSuffix+'\n'lethtml=gameProg+levelProgconstletters: Letter[]=[{char: ' ',offset: boardWidth,height: 0}, ...letterset]for(letheight=boardHeight,l=letters.length-1;l>=0;--l,--height){while(height>letters[l].height){html+='\n';--height;}constletter=letters[l]if(l)html+=' '.repeat(letter.offset)+letterHtml(letter)+'\n'}board.innerHTML=html}}functionpromptUser(question: string){returnfunction(notifier: Observable<unknown>){returnnotifier.pipe(observeOn(asyncScheduler),switchMapTo(iif(()=>confirm(question),of(true),EMPTY)))}}functionlevelFlashScreen(text: string): Letter[]{return[{char: text,height: Math.ceil(boardHeight/2),offset: Math.floor((boardWidth-text.length)/2),}]}returndefer(()=>{constboard=getBoard()constupdateBoard=drawBoard(board)/** Time gap between each tick. One tick means letters drop by a single row */constgap$=newBehaviorSubject(initialDelay)returngap$.pipe(switchMap((gap,level)=>defer(()=>{updateBoard(levelFlashScreen(`Level ${level}`),boardWidth,Math.max(0,level-1)/maxLevels,Number(level>0));returnlettersRemaining(gap,level)})),tap(state=>{if(state.count>=levelHits&&state.level<maxLevels-1)gap$.next(levelAccelerationFactor*gap$.getValue())if(state.letters[0]?.height<=0)thrownewError('Player lost')}),takeWhile(state=>state.level<maxLevels-1||state.count<levelHits),tap({next: state=>state.count<levelHits ? updateBoard(state.letters,boardWidth,state.level/maxLevels,state.count/levelHits) : null,complete: ()=>updateBoard(levelFlashScreen('Game Over'),boardWidth,1,1)}),)}).pipe(repeatWhen(promptUser('You win. Play again?')),retryWhen(promptUser('You loose. Play again?')),catchError(()=>EMPTY)// Ignore error)}makeGame().subscribe()
The text was updated successfully, but these errors were encountered:
z
would never be generated.1.1 This is easily fixable by adding
+1
inside multiplication withMath.random() ×( ... + 1)
2.1 This can be simulated by making
randomLetter
return one character:randomLetter = () => 'a'
2.2 This is not easy to fix, as that would require major refactoring. The problem comes from using
combineLatest
in the main logic.combineLatest
cannot determine, which stream triggered the new emission - new key pressed or new letter arriving.Here is a quick-and-dirty solution that should fix the problems (Also on StackBlitz):
The text was updated successfully, but these errors were encountered: