• 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: gam / 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 into 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

  • ...sure... just returned from a tournament. Had to enforce a break to calm down the gaming seas... - will take a clip on text one.

  • @Robin, as promised: attached a short clip of the game while it is going on... paddles connected to / active on / playing 2nd and 4th lanes...

    Stills show start, in progress, and win - win by player in 4th lane!


    4 Attachments

  • Sun 2019.03.03

    Nice completed project!

    From #26

    game paddles on scrap prototype PCB w/ mounted micro switch
    w/ every fourth early press player uses a step

    and my #27

    e.g. Paddles in foreground with light display in background

    I was attempting to visualize the actual player interface mechanics. I do hear the tapping and as I realize the prototyping need for a simplified pin ball lane equivalent, is the micro switch one of those with a springy extender arm, and the effect is to trip/time during a set interval, but not too fast in succession?

  • @Robin, yes the switches are for emulating the pin ball exit lanes. @matencio is on the pin ball boards, as far as I know. Since this display work went pretty well, I'm thinking about something that has 5 times the pixels for a display and then build a two player Tetris.... one where two shoots drop tiles and the faster player can pass the slower one and faster points. So far it is only a thought.

  • Fri 2019.03.08

    I like the idea! @allObjects Can't wait to see how you contrive your super-duper innovation over the first link in #1 tetris.js from:

    see #1 http://forum.espruino.com/conversations/­265028/

    and wireless joysticks:

    images #6 http://forum.espruino.com/conversations/­299085/

    Maybe with the MDBT42Q perhaps?   Think On!

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

Retro Horse Derby Game Board with Neopixels showing Reaction Game Status

Posted by Avatar for allObjects @allObjects

Actions