Case Study: Cheat Code Entry - deanrad/rx-helper GitHub Wiki

Detecting a Cheat Code Was Entered

Given that we have two buttons, A and B, we want to print a message if a “secret combination” (ABBABA) is clicked within 5 seconds. To make things a bit harder the following must also be true:

  • The message should be printed immediately when the secret combination is clicked. We should not wait until the end of the 5 second interval before the message is printed.
  • As soon as the secret combination is fulfilled (within 5 seconds) the message should be displayed regardless of what has been clicked before. For example a combination like “BBABBABA” is ok as long as “ABBABA” is clicked within 5 seconds.

The Rx-Helper Way

The primary logic is in this Handler, which returns an Observable of events of type: 'won', corresponding to successful completions of the cheat code sequence in a short enough amount of time:

agent.on(({event: {payload: key}}) => (key==="A"), 
    seeIfNext5AreCorrectAndFastEnough,
    { processResults: true }
)
 
 function seeIfNext5AreCorrectAndFastEnough({ event: { payload: key } }) {
   const startedAt = new Date().getTime()
   // Get the next 5 non-empty as [{value, timestamp}]
   const next5Key$ = agent.getAllEvents('key').pipe(
       map(a => a.payload),
       filter(Boolean),
       take(5),
       timestamp(),
       toArray())

   // Return winning messages
   return next5Key$.pipe(
       // for batches which complete us
       filter(batch => theCode.substr(1) === (batch.map(i => i.value).join(""))),
       // if they are within the timeout
       filter(batch => (batch[4].timestamp - startedAt) < codeTimeout),
       map(batch => ({ type: 'win', payload: `${batch[4].timestamp - startedAt} msec`}))
    )
 }

Additionally we have some imports and constants:

const { agent } = require('rx-helper')
const { of, empty, from, interval, zip, race, timer } = require('rxjs')
const { map, take, toArray, filter, tap, timestamp } = require('rxjs/operators')

const codeTimeout = 5000
const keyInterval = 500

const theCode = "ABBABA"         // the code
const testKeys  = [
 // "B",           // spurious letter
 // "A", "B", "B", // 3 that are good
 // "B",           // bad
 "A", "B",         // 2 good
 "A",              // bad, but starting its own
 "B", "B",         // up to 3 good ones
 "", "", "",       // should time out
 "A", "B",         // false start (tricky!)
 "A", "B", "B",    // getting there
 "A", "B", "A",    // finally!
 ]

Lastly, we show all events in the console, and subscribe to these test keystrokes for testing, spacing them out over time:

 agent.filter(true, ({ event }) => console.log(event.payload || "/"))

 agent.subscribe(zip(interval(keyInterval), from(testKeys), (_, k) => k), {type: 'key'})
 null

And with an 800 msec keyInterval our log will show:

A B A B B / / / A B A B B A B A (win!)

While with an 500 msec keyInterval our log will show:

A B A B B / / / A (win!) B A B B A B A (win!)

Q.E.D.

Original Problem: https://blog.jayway.com/2014/09/16/comparing-core-async-and-rx-by-example/

Extended Discussion: https://github.com/Reactive-Extensions/RxJS/issues/276

Live Rx-Helper Solution on RunKit