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:
longer lead - Data Out - DO
longer lead - Ground - GND
shorter lead - 5V - VDD
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.random()*flickerLEDV));
}
function flicker2() {
flicker2TId = null;
if (digitalRead(flickerLED2P)) {
if (!digitalRead(flickerLED1P)) flickerLED1P.set();
flickerLED2P.reset();
} else {
flickerLED2P.set();
}
flicker2TId = setTimeout(flicker2,Math.floor(Math.random()*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],clrs[++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,clrSpeed,next) {
rainbowOn = true;
rainbow0(every,count,startClr,clrGrade,clrSpeed,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!
Espruino is a JavaScript interpreter for low-power Microcontrollers. This site is both a support community for Espruino and a place to share what you are working on.
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:
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...
Shot below shows blaming of to lane user because of prematurely pressing the button!
1 Attachment