• After playing with/as (grand) kids on the real 20 x 7 board, the software needed some serious upgrade to keep the momentum of the game going and increase the pressure:

    • shorter races
    • eliminate game control and replace w/ short timeout to increase game pressure
    • loose a step for every four early presses
    • shorten all times to increase pressure

    Enhancements since breadboard prototype:

    • a real size board - 20 x 7 - columns/positions x rows/lanes
    • 4 player
    • game paddles on scrap prototype PCB w/ mounted micro switch
    • w/ every fourth early press player uses a step
    • because of size of board (display panel)
      • only part of lanes uses for stepping through race
      • tournament status - won races - displayed separately
    • running disconnected
    • running powered from board 5V
    • replace use of neopixel module with bare SPIX.write4(... to eliminate display issues of 2v01

      // derby9OW.js
      var c=`
      derby race board w/ neopixels:
      - shows in bright current race pos
      - shows in low race wins
      game is a reaction game where users have to press button
      when flickering of two LEDs stops; first pressing player
      advances one step in race; if pressed too early, user is
      'blamed' and loses for every fourth a step in race, and 
      race step is repeated; race steps are started by game host
      w/ game control button; number of users, lanes, positions,
      races per game are all configurable, and so are the times
      for the game states, mainly times for displaying a particular
      game state; optionally game button press can be replaced w/
      defaultDelay timeout for increased game pressure`;
      
      // dev stuff - lgo = logging on
      var lgo = false; // set to false before upload for saving
      function log() { console.log.apply(console,arguments); }
      
      // get hold of neopixel string handler
      var npxs = require("neopixel");
      var spiX = SPI2; // if set, spi write4 is used instead of neopixel module
      
      // --- wireings setup externalized to easy adjust to board in use
      // Espruino-Wifi wireings for board
      var npxp            = B15  // neopixel feed pin
      , gameControlBtn  =  A0  // game flow control
      // Espruino-Wifi wireings for game
      , flickerLED1P    = LED1 // pin flickering LED 1
      , flickerLED2P    = LED2 // pin flickering LED 2
      , playerInputSumP = B14  // any player input ('or' irq by diodes)
      , playerInputP0   = B10  // 1st player input direct
      , playerInputP1   =  A7  // 2nd player input direct
      , playerInputP2   =  A5  // 1st player input direct
      , playerInputP3   =  A1  // 2nd player input direct
      , playerInputPs   =      // all player inputs (LSB last)...
      [ playerInputP1        // ...to capture all at once when...
      , playerInputP0        // ...any player input watch fires
      , playerInputP2
      , playerInputP3
      ]
      ;
      // --- some declarations to make it flexible
      var maxPos     = 20 // maximum position of lane on of display
      , finishLine = 11 // 20; // reaching this position means win
      , winBasePos = 17 // where race wins are shown if there are
      , maxLanes   =  4 // number of horses / lanes
      , playerCnt  =  maxLanes // for now, later can be less
      , maxEarlies =  3 // after 3rd early, 1 step back and resets earlyCnt
      , defaultDelay = 500 // if > 0, auto press of game control button
      ;
      
      // --- player data
      var players; // array of players w/ details - see raceInit()
      
      // --- game parameters
      var racesToWin  =    3 // win this races to win game
      , flickerFix  =  300 // flicker fix for this ms with additional 
      , flickerVary = 1200 // 0..this ms random range variably
      , flickerLEDV =  100 // flicker individual LED random range
      , blameTime   = 2000 // show/blame players pressed too ealry ms
      , stepWinTime = 1000 // show step win for ms
      , raceWinTime = 4000 // show race win incl step win for ms
      ;
      
      // --- setup neopixel data buffers (3 bytes per pixle)
      var npxb = new Uint8ClampedArray(maxLanes*maxPos*3)
      // --- overlay buffer and control
      , ovrb = new Uint8ClampedArray(maxLanes*maxPos*3)
      , ovrc = new Uint8ClampedArray(maxLanes*maxPos)
      ;
      
      // --- 256 colors from wheel for effects / other things
      //     3 bytes for each color, which means 768 bytes total
      //     colors / bytes picked by index (0..255*3) +0, ..+1, ..+2
      var clrs = new Uint8ClampedArray(256*3)
      , dimd = 0.10 // dimmed
      ;
      
      var raceCnt;
      function initGame() {
      flickerStop();
      if (gameCntrlWId) gameCntrlWId=clearWatch(gameCntrlWId);
      if (playerWId   ) playerWId   =clearWatch(playerWId   );  
      raceCnt = 0;
      players = []; 
      for (var pdx=0;pdx<playerCnt;pdx++) {
      var b = Math.floor(Math.random()*256);
      players.push(  // Player detail:
        {idx:pdx     // - player index 0...playerCnt-1
        ,racePos:-1  // - pos in current race 0..maxPos-1
        ,earlyCnt:0  // - earlies count for punishment
        ,winCnt:0    // - race wins
        ,negPoints:0 // - sum of negative points / not used yet
        ,rClrs:[clrs[b],clrs[b+1],clrs[b+2]] // - race pos clrs
        ,gClrs:[dim(clrs[b])   // - game clrs (dimmed pos clrs)
               ,dim(clrs[b+1]) //   show race wins (winCnt)
               ,dim(clrs[b+2])
               ]
        }); }
      initRace();
      }
      
      function initRace() {
      raceCnt++;
      for (var pdx=0;pdx<playerCnt;pdx++) {
      var player = players[pdx];
      player.racePos = 0;
      }
      setBoard();
      gameCtrl(raceStep);
      }
      
      // raceStep consists of activating player input and - for this
      // reactin game - stimulate the user for quick pressing after 
      // flickering of LED! and LED2 stop.
      function raceStep() {
      flicker();     // start flickering for a random time
      watchPlayers(); // start watching player inputs
      }
      
      // player(s) pressed too early - indicate players, then raceStep
      // for now no punishing of the player, just blaming publicly...
      function pressedTooEarly(playerIdxs) {
      if (lgo) log("tooEarly",playerIdxs);
      clear(ovrb); // prep blame buffer: all pixls in raceClrs
      playerIdxs.forEach(function(playerIdx) {
      var player = players[playerIdx];
      for (var pos=0;pos<maxPos;pos++)
        setPos(ovrb,playerIdx,pos,player.rClrs);­
      });
      pixle(ovrb,function(){
      pixle(); // restore board display
      playerIdxs.forEach(function(playerIdx) {
        var player = players[playerIdx];
        if (++player.earlyCnt > maxEarlies) {
          player.earlyCnt = 0; // reset earely count
          if (player.racePos>0) player.racePos--; // one pos back
            setBoard(false); // ...lazy ;-)
          }
        });
      gameCtrl(raceStep); // about next raceStep
      },blameTime);
      }
      
      // player(s) pressed right
      function pressedRight(playerIdxs) {
      if (lgo) log("right",playerIdxs);
      var raceWin = false;
      var gameOver = false;
      copyClrs(npxb,0,ovrb,0,maxLanes*maxPos);­ // copy board
      playerIdxs.forEach(function(pdx) { // update player / board copy
      var p = players[pdx]; // get player
      if (++p.racePos >= finishLine-1) { // race win?
        raceWin = true; // update win count indicator pixel:
        if (p.winCnt>0) setPos(ovrb,pdx,winBasePos+p.winCnt-1); // clear old
        gameOver |= (++p.winCnt >= racesToWin); // game over?
      }
      if (p.winCnt) // set new winCnt if it was new else set...
        setPos(ovrb,pdx,winBasePos+p.winCnt-1,p.­gClrs); // existing again
      setPos(ovrb,pdx,p.racePos-1,p.rClrs); // set old n new...
      setPos(ovrb,pdx,p.racePos  ,p.rClrs); // ...race pos pixel
      });
      pixle(ovrb,function(){
      if        (gameOver) {
        setBoard(true); // skip show
        players.forEach(function(p){
          setPos(null,p.idx,p.racePos);
          if (p.winCnt) setPos(null,p.idx,winBasePos+p.winCnt-1,­p.gClrs);
        });
        pixle(); // show win counts
        gameCtrl(function(){ rainbow(500,6,0,32,16,initGame); });
      } else if (raceWin) {
        initRace();
      } else { // just next race step
        setBoard(); // ...w/ new settings
        gameCtrl(raceStep); // about next raceStep
      }
      }, (raceWin) ? raceWinTime : stepWinTime );
      }
      
      // --- set board w/ current player information and diplay it
      function setBoard(skipShow) {
      clear();
      for (var pdx=0;pdx<playerCnt;pdx++) {
      var p = players[pdx];
      if (p.winCnt) setPos(null,pdx,winBasePos+p.winCnt-1,p.­gClrs);
      setPos(null,pdx,p.racePos,p.rClrs);
      }
      if ( ! skipShow) pixle();
      }
      // --- flicker two LEDs irregularerly for a randim time
      var flickerTId  = null;
      var flicker1TId = null;
      var flicker2TId = null;
      // start flickering irregularely 2 LEDs. One is always on.
      // if one is about to turn off and the other one is not on,
      // the other one is turned on so that always one is on.
      function flicker() {
      flickerTId = setTimeout(flickerStop // stop flickering
      , flickerFix + Math.floor(Math.random()*flickerVary));
      flicker1(); flicker2();
      }
      function flickerStop() {
      if (flickerTId ) flickerTId  = clearTimeout(flickerTId );
      if (flicker1TId) flicker1TId = clearTimeout(flicker1TId);
      if (flicker2TId) flicker2TId = clearTimeout(flicker2TId);
      flickerLED1P.reset(); flickerLED2P.reset();
      }
      function flicker1() {
      flicker1TId = null;
      if (digitalRead(flickerLED1P)) {
      if (!digitalRead(flickerLED2P)) flickerLED2P.set();
      flickerLED1P.reset();
      } else {
      flickerLED1P.set();
      } 
      flicker1TId = setTimeout(flicker1,Math.floor(Math.rand­om()*flickerLEDV));
      }
      function flicker2() {
      flicker2TId = null;
      if (digitalRead(flickerLED2P)) {
      if (!digitalRead(flickerLED1P)) flickerLED1P.set();
      flickerLED2P.reset();
      } else {
      flickerLED2P.set();
      } 
      flicker2TId = setTimeout(flicker2,Math.floor(Math.rand­om()*flickerLEDV));
      }
      
      // watch players by watching playerInputSum
      // on press calls pressedXXX w/ XXX=TooEarly or Right w/ playerIdxs
      var playerWId = null;
      function watchPlayers() {
      playerWId = setWatch(function(watchEvent){ playerWId = null;
      var playerInputs = digitalRead(playerInputPs) // capture all input
        , playerIdxs = [], playerBit = 1; // start w/ LSB for player 0
      for (var pdx=0;pdx<playerCnt;pdx++) { // collect players' presses
        if ( ! (playerInputs & playerBit)) playerIdxs.push(pdx);
          playerBit <<= 1;
        }
        if (flickerTId) { pressedTooEarly(playerIdxs); 
        } else          { pressedRight(playerIdxs)   ; }
      },playerInputSumP,{repeat:false, edge:"falling"});
      }
      
      // gameCtrl awaits press and release of gameControlBtn to fire action
      // ...but if defaultDelay present (number), game advances after delay ms
      // without pressing game control button
      var gameCntrlWId = null;
      function gameCtrl(action) {
      if (defaultDelay) {
      setTimeout(action,defaultDelay);
      } else {
      gameCntrlWId = setWatch(function(){
        gameCntrlWId = null;
        action();
      },gameControlBtn,{repeat:false, edge:"rising"});
      }
      }
      
      // set player pos in passed or default neopixel buffer from
      // prepped colors buffer if passed, else clear
      function setPos(buf, playerIdx, pos, pClrs) {
      buf = (buf) ? buf : npxb;
      pClrs = (pClrs) ? pClrs : [0,0,0];
      var cPos = (playerIdx * maxPos + pos)*3;
      if (playerIdx%2 === 1) cPos += (maxPos - 2*pos - 1)*3;
      copyClrs(pClrs,0,buf,cPos);
      }
      
      // utility to dim a color (so-la-la... not strictly :/ )
      function dim(val) {
      if (val>0) val = Math.max(1,Math.floor(val*dimd));
      return val;
      }
      
      // utility to copy colors from one array/buffer to other one
      // for pixles. If pixles undefined, it is 1 (3 bytes);
      function copyClrs(fromB,fromBdx,toB,toBdx,pixels)­ {
      var bMax = (pixels===undefined) ? 3 : pixels * 3;
      for (var b=0;b<bMax;b++) toB[toBdx++] = fromB[fromBdx++];
      }
      
      // clear passed or default neopixel buffer (set all off)
      function clear(buf) {
      buf = (buf) ? buf : npxb;
      buf.fill(0,0);
      }
      
      // initialize 256 rainbow colors for later use
      function initClrs() {
      // from Nick Gammon, AU - http://www.gammon.com.au/forum/?id=13357­
      var x=-1, w; // index in buffer, wheel position, r>g>b>r
      for (var c=0; c<256; c++) { w=c;
      if        (w< 85) {         clrs[++x]=255-w*3; clrs[++x]=w*3    ; clrs[++x]=0;
      } else if (w<170) { w-= 85; clrs[++x]=0      ; clrs[++x]=255-w*3; clrs[++x]=w*3;
      } else            { w-=170; clrs[++x]=w*3    ; clrs[++x]=0      ; clrs[++x]=255-w*3;
      }
      }
      }
      function dumpClrs() { x=-1;
      while(++x<768) log(Math.floor(x/3),clrs[x],clrs[++x],cl­rs[++x]);
      }
      
      var rainbowOn = false;
      // displaying rainbow, changing every ms, for count times
      // (0=indefinite until rainbowOn set false), starting w/
      // color startClr (0..255), clrGrade (>0 colors to next LED),
      // avancing clrSpeed (>0) colors for each display, and after
      // rainbow, do next (callback) if next defined.
      function rainbow(every,count,startClr,clrGrade,cl­rSpeed,next) {
      rainbowOn = true;
      rainbow0(every,count,startClr,clrGrade,c­lrSpeed,next);
      }
      function rainbow0(iv,cnt,sClr,cGrade,cSpeed,nx) {
      var sc = sClr*3, xm=ovrb.length, x=-1, b=3; // pre rainbow load
      while (++x<xm) { ovrb[x]=clrs[sc]; sc++; // load buf w/ rainbow
      if (--b===0) { b=3; sc=(sc+cGrade*3)%768; } }
      pixle(ovrb); // display rainbow
      if ((cnt === 1) || ! rainbowOn) { // check last / end indefinite
      rainbowOn = false; // after counted down to 1
      if (nx) setTimeout(nx,iv); // do next
      } else { // keep current rainbow shown until next one or end
      setTimeout(rainbow0,iv,iv,(cnt)?--cnt:0,­(sClr+cSpeed)%256,cGrade,cSpeed,nx);
      }
      }
      
      function pixle(buf,next,after) { // pixle it - write to neopixs 
      // with opitonal next action; if defined, invoke it after ms
      buf = (buf) ? buf : npxb;
      if (spiX) { spiX.send4bit(buf,0b0001,0b0011);
      } else { npxs.write(npxp,buf); }
      if (next) setTimeout(next,after);
      }
      
      function dump(buf) { // dump buf to console for debug
      buf = (buf) ? buf : npxb;
      var x = -1, plr;
      for (var i=0;i<maxLanes;i++) { plr = players[i];
      console.log("--- player ",i," -",plr.racePos,"(",plr.winCnt,"):");
      for (var j=0;j<maxPos;j++) {
        console.log(j,"-",buf[++x],buf[++x],buf[­++x]);
      }
      }
      }
      
      function onInit() {
      // seed randmom
      var r = Math.abs(Math.floor(E.hwRand()/1000000))­; while (--r>0) Math.random();
      // board GPIO pins - set mode
      pinMode(npxp,"output");
      if (spiX) { // if spiX is set, old way is used and not neopixels module
      spiX.setup({baud:3200000, mosi: npxp});
      }
      pinMode(gameControlBtn,"input_pullup"); // for game host to start race
      // game GPIO pins - set mode
      pinMode(flickerLED1P,"output"); // set to output so on/off can be read
      pinMode(flickerLED2P,"output"); // set to output so on/off can be read
      pinMode(playerInputSumP,"input_pullup");­ // watched to read playerInputs[]
      playerInputPs.forEach(function(p){ pinMode(p,"input_pullup"); });
      // init sequence and start to get ready for first race
      clear();
      pixle();
      initClrs();
      rainbow(100,10,0,8,32,initGame);
      }
      
      c="";
      
      setTimeout(onInit,500); // comment before upload for saving
      

    Btw, excitement of players and watchers is easily to boost: game / tournament host has to cheer and document like a sports reporter in the ball bark. I can tell you this had the grand kids out of morning sleep onto high alert in no time!

    Last pic shows my version of rainbow colors from color wheel.

    Board status as shown in attached picture:

    • light green, blue and yellow players have already won some races: 1, 2 and 2
    • red player has not won a race yet but has good changes to win this time: 1 step away from winning the race
    • blue and yellow players are one race win away from winning the game / tournament

    3 Attachments

    • separete display of race and races won.jpg
    • setup w game paddles and board powering.jpg
    • rainbowColorWheelColorsOnBoard.jpg
  • Sat 2019.02.09

    Nicely done @allObjects

    Peppered with many good examples of accessing arrays and program flow logic - Bonus!!

    Any chance of getting a short 20 sec video of the excitement during actual game play?
    e.g. Paddles in foreground with light display in background

About

Avatar for allObjects @allObjects started