• Some more details about the setup:

    Choice of Espruino board: see Espruino-WiFi - is a nice, fully featured board with integrated Wifi connectivity thru espressif's ESP8266 ESP-12, mounted on the bottom of Espruino-Wifi board, prewired, and with blue LED3 on top showing Wifi status/activities. Can work as connecting station and also as ACCESS station that can be connected to. Espruino can run Web client AND/OR http Web server on http(s) protocol level and on socket level. (Wifi could be used to implement Web Server for publishing game status or playing distributed game over the Web. :O....... 21 GPIO 5V tolerant pins are plenty for a lot to do... Further on board: red LED1 and green LED2 and Btn/Btn1 for application. Power supply is thru USB or Bat_in pin (3.3..5V) and onboard w/ LDO 3.3V voltage regulator. --- Any other Espruiono board would do well as well... even Espruino Puck.js... ...with some soldering to get enough pins connected.

    Neopixel are individual, 4-lead, 5mm round, diffused LEDs w/ red, green and blue LEDs and electronic built in:

    1. longer lead - Data Out - DO
    2. longer lead - Ground - GND
    3. shorter lead - 5V - VDD
    4. shorter lead - Data In - DI

    Neopixels are daisy chained in zig-zag pattern: even lanes - counting from 0 - (top lane) runs from left to right, odd lanes (bottom lane) run from right to left. I intentionally made it this way to develop the software for the prewired string on the market. Top lane is fed with 2*5*3 bytes representing the RGB colors for each of the neopixels.

    Buttons are connected in "pull pin to Ground" manner to one pin of Espruino-Wifi set in "input_pullup" pin mode, pulled up by mc chip internal 30..40k resistor. Some buttons are watched directly and individually and some only summarily and indirectly and and only read together.

    The game host's button is at times watched with rising flank detection to initiate race step and a new game. The user buttons are not watched, but a or-logic with diodes creates a combined signal for a watched pin with falling flank detection to trigger prematurely or timely read of all user inputs at once - (virtually) at the very same time by value = digitalInput(<arrayOfPins>); ( @Gordon would know the ns between the reads, if there are... I'm sure not relevant for the game... lest the precision of the buttons... or their tolerances).

    Neopixel signal time precision is crucial because Neopixel LEDs are picky... and it is noticeable, especially when running rainbow wave through the LEDs before each game: sometimes some LEDs show messed up or no color at all... The 'fat' capacitor helped, but there are still some glitches.

    Below is The Code. To be upfront: I'm not a game developer... this is about the 2nd or 3rd game in my career - so not much treasure to find. Nor is the code structured to my desire or performance optimized: so far it is a big pile of functions - close to a treason to my screen name (sure, I was thinking in objects, but the functions have to still find their 'class' home... and some have to split for that). The code came together as I went and explored aspect by aspect. For certain things a nice oo-refactoring with consideration of performance is advised - especially when thinking of driving large(r) boards. On the other hand I'm pleased by t as a prototype. It works as originally intended... and even ended up as a simpler implementation then I had anticipated, especially in regard of game control and game status communication. BUT: UX's writing is already on the wall: allow users to pick their lane color... and let them keep it across games...

    // derby9.js
    // 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' (but not punished yet) 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.
    
    // 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");
    
    // --- 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 =  A1  // any player input ('or' irq by diodes)
      , playerInputP0   =  A4  // 1st player input direct
      , playerInputP1   =  A5  // 2nd player input direct
      , playerInputPs   =      // all player inputs (LSB last)...
        [ playerInputP1        // ...to capture all at once when...
        , playerInputP0        // ...any player input watch fires
        ]
      ;
    // --- some declarations to make it flexible
    var maxPos    =  5 // reaching this position means win
      , maxLanes  =  2 // number of horses / lanes
      , playerCnt =  maxLanes // for now, later can be less
      ;
    
    // --- player data
    var players; // array of players w/ details - see raceInit()
    
    // --- game parameters
    var racesToWin  =    3 // win this races to win game
      , flickerFix  =  500 // flicker fix for this ms with additional 
      , flickerVary = 5000 // 0..this ms random range variably
      , flickerLEDV =  300 // flicker individual LED random range
      , blameTime   = 3000 // show/blame players pressed too ealry ms
      , stepWinTime = 2000 // 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.05 // 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
          ,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
          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 >= maxPos-1) { // race win?
          raceWin = true; // update win count indicator pixel:
          if (p.winCnt>0) setPos(ovrb,pdx,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,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,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,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
    var gameCntrlWId = null;
    function gameCtrl(action) {
      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
    // rainbowk, 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;
      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");
      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(500,6,0,32,16,initGame);
    }
    
    setTimeout(onInit,500); // comment before upload for saving
    

    Shot below shows blaming of to lane user because of prematurely pressing the button!


    1 Attachment

    • topLaneUserBlamingWhileStillFlickering.jpg
About

Avatar for allObjects @allObjects started