From 2e5225bfac117f286675b5badb96a088fc7b5f18 Mon Sep 17 00:00:00 2001 From: Kai Mosebach Date: Sat, 13 Aug 2022 15:31:29 +0100 Subject: [PATCH] switch to simple version --- game-full.js | 439 +++++++++++++++++++++++++++++++++++++++++++++++++++ game.js | 105 +++++++++--- 2 files changed, 524 insertions(+), 20 deletions(-) create mode 100644 game-full.js diff --git a/game-full.js b/game-full.js new file mode 100644 index 0000000..42b280f --- /dev/null +++ b/game-full.js @@ -0,0 +1,439 @@ +const { QLog, undefinedOrNull } = require('quanto-commons'); +const player = require('node-wav-player'); +const TelegramBot = require('node-telegram-bot-api'); +const fs = require('fs'); +const { exec } = require("child_process"); + +if(process.platform !== 'darwin') var gpio = require('rpi-gpio'); + +const telegramBotToken = '5472909337:AAGH269wNGa9u99ekJqHqXEHfi0knpV7IFU'; +//const groupid='@HotSoup'; +const groupid='-1001703800044'; +const GlobalLog = QLog.scope('Global'); +const TelLog = QLog.scope('Telegram'); + +GlobalLog.enableLogs(['debug']); +TelLog.enableLogs(['debug']); + +GlobalLog.headPadding = 30; +TelLog.headPadding = 30; + +// const bot = new Telegraf(telegramBotToken); +const bot = new TelegramBot(telegramBotToken, {polling: true}); + +// debounce der buttons in ms +const bounceMS = 800; + +// Game Reset Start after ms +const startReset = 4000; + +// ist an wenn im kind mode (erw LED is aus) +const childLED = 37; +// ist and wenn im erwachsenen Mode (kind LED ist aus) +const erwLED = 38; +// status : ist an wenn in ruhe position, blinkt, wenn gestartet, ist aus bei fail +const statusLED = 40; +// setze auf HIGH wenn fail (klingel relais) +const failSwitch = 35; + +// warte auf spielstart wenn auf HIGH +const startButton = 32; +const startButton_GPIO = 12; +// beende spiel wenn auf HIGH +// const finishButton = 31; +const finishButton = 22; +const finishButton_GPIO = 25; +// faile spiel wenn auf HIGH +// const failButton = 29; +const failButton = 21; +const failButton_GPIO = 9; +// wechsle kind/erwachen Modus + LED +const toggleButton = 33; +const toggleButton_GPIO = 13; + +// wird je nach modus gesetzt und abgespielt wenn start +const backgroundGrown = "jaws.wav"; +const jeopardy = "jeopardy.wav"; +const backgroundChild = "northsouth.wav"; +// wird zufallig zu zufaelligen momenten abgespielt (erw mode) +const erschreckSounds = [ "ziege.wav", "scream.wav", "laugh.wav", "cavallerie.wav", "gulp.wav" ]; +// wird nach fester zeit + random oder spaeter nach der besten zeit abgespielt (erw mode) +// const stresserSound = "sharks.wav"; +// wird bei fail abgespielt (zonk) +const failSound = "zonk.wav"; +// hurra wird bei finish abgespielt +const finishSound = "kidscheering.wav"; + +const GROWN=1; +const CHILD=2; + +// LOW EDGE +let preStart = true; + +// game running? +let isRunning = false; + +const STATES = { + STARTED: 'started', + FINISH: 'finshed', + FAIL: 'failed' +}; + +let best; + +const bounce = {}; + +var STATE = STATES.FINISH; +var MODE = GROWN; +var GAME = { + events: [], + runTime: 0 +}; + +var chatId; + +function sendBotGroupImg(chat_id, photo, caption) { + GlobalLog.info( chat_id, photo); + bot.sendPhoto(chat_id, photo, { + caption: caption + }); +} +function sendBotMsg(msg) { + if(chatId) { + bot.sendMessage(chatId, msg); + } +} + +function setup() { + + // bot.start((ctx) => { + // let message = `Bot aktiv und beobachtet. Benutze /stats um die Bestenliste zu sehen`; + // ctx.reply(message); + // TelLog.note('Bot started'); + // botCTX=ctx; + // }); + // bot.command('stats', async (ctx) => { + // await ctx.replyWithPhoto({ source: '/root/hotwire-js/img/bestgrown.png' }); + // await ctx.reply(`Bester Erwachsener mit nur ${best.grown} sek`); + // await ctx.replyWithPhoto({ source: '/root/hotwire-js/img/bestchild.png' }) + // await ctx.reply(`Bestes Kind mit nur ${best.child} sek`); + // }); + // bot.launch(); + fs.readFile('stats.json', 'utf8', (err, res) => { + if(err) { + GlobalLog.info("No stats found. Using defaults"); + best = { + grown: 25, + child: 37 + } + } else { + GlobalLog.info("Stats loaded."); + best = JSON.parse(res); + } + }); + + GlobalLog.note('Setting up GPIOs'); + + if(process.platform === 'darwin') { + setTimeout(start, 500); + setTimeout(fail, 3500); + setTimeout(start, 4000); + setTimeout(finish, 9000); + setTimeout(fail, 10000); + setTimeout(finish, 11000); + return; + } + gpio.setup(failSwitch, gpio.OUT, () => { + gpio.output(failSwitch, false); + }); + gpio.setup(statusLED, gpio.OUT, () => { + gpio.output(statusLED, true); + }); + gpio.setup(childLED, gpio.OUT, () => { + gpio.output(childLED, false); + }); + gpio.setup(erwLED, gpio.OUT, () => { + gpio.output(erwLED, true); + }); + + gpio.setup(finishButton, gpio.DIR_IN, gpio.EDGE_FALLING, () => { + exec(`raspi-gpio set ${finishButton_GPIO} pu`); + }); + gpio.setup(toggleButton, gpio.DIR_IN, gpio.EDGE_FALLING, () => { + exec(`raspi-gpio set ${toggleButton_GPIO} pu`); + }); + gpio.setup(startButton, gpio.DIR_IN, gpio.EDGE_BOTH, () => { + exec(`raspi-gpio set ${startButton_GPIO} pu`); + }); + gpio.setup(failButton, gpio.DIR_IN, gpio.EDGE_FALLING, () => { + exec(`raspi-gpio set ${failButton_GPIO} pu`); + }); + + gpio.on('change', (channel, value) => { + if(bounce[channel] === true) return; + GlobalLog.debug('Change',channel, value); + bounce[channel] = true; + setTimeout(() => { bounce[channel] = false; GlobalLog.debug('Debounced', channel); }, bounceMS); + switch(channel) { + case startButton: + start(value); + break; + case finishButton: + finish(value); + break; + case failButton: + fail(value); + break; + case toggleButton: + toggle(value); + break; + default: + GlobalLog.warn('Alert! Unknown button change',channel); + } + }); + GlobalLog.note('Done. Game logic started. Waiting for input'); +} + +function gameTimer() { + GlobalLog.debug('game timer called'); + GAME.events.forEach((el) => { + if(el.start === GAME.runTime) { + GlobalLog.debug('Event :', el); + playSound(el.sound); + } + }); + GAME.runTime++; + GAME.led = !GAME.led; + gpio.output(statusLED, GAME.led); +} + +function failTimer() { + GAME.led = !GAME.led; + gpio.output(statusLED, GAME.led); + gpio.output(childLED, GAME.led); + gpio.output(erwLED, GAME.led); +} +function successTimer() { + GAME.led = !GAME.led; + gpio.output(statusLED, GAME.led); + gpio.output(childLED, !GAME.led); + gpio.output(erwLED, GAME.led); +} + +function getRandomInt(max) { + return Math.floor(Math.random() * max); +} + +function start(value) { + if(preStart === false || isRunning) { + GlobalLog.note('Prestart detect.'); + preStart = true; + if(GAME.startplayer) GAME.startplayer.stop(); + if(GAME.failTimer) clearInterval(GAME.failTimer); + if(GAME.successTimer) clearInterval(GAME.successTimer); + if(GAME.countTimer) clearInterval(GAME.countTimer) + if(player) player.stop(); + if(MODE === GROWN) { + gpio.output(childLED, false); + gpio.output(erwLED, true); + } else { + gpio.output(childLED, true); + gpio.output(erwLED, false); + } + if(preStart === false) return; + } + if(isRunning === true) { + GlobalLog.note('Game reset.'); + if(GAME.startplayer) GAME.startplayer.stop(); + if(player) player.stop(); + STATE = STATES.FINISH; + clearInterval(GAME.countTimer) + preStart = false; + isRunning = false; + if(GAME.failTimer) clearInterval(GAME.failTimer); + if(GAME.successTimer) clearInterval(GAME.successTimer); + gpio.output(statusLED, true); + GAME = { + events: [], + runTime: 0 + } + } + if(GAME.failTimer) clearInterval(GAME.failTimer); + if(GAME.successTimer) clearInterval(GAME.successTimer); + if(MODE === GROWN) { + gpio.output(erwLED, true); + } else { + gpio.output(childLED, true); + } + + GAME.runTime = 0; + + if(STATE === STATES.FINISH || STATE === STATES.FAIL) { + STATE = STATES.STARTED; + // set all params for next game, then start counter and event timer + if(MODE === GROWN) { + if(getRandomInt(7) > 2) { + GAME.events.push({ sound: jeopardy, start: 1}); + } else { + GAME.events.push({ sound: backgroundGrown, start: 1}); + } + GAME.events.push({ sound: erschreckSounds[getRandomInt(5)], start: 15+getRandomInt(10) }); + GAME.events.push({ sound: erschreckSounds[getRandomInt(5)], start: 30+getRandomInt(10) }); + GAME.events.push({ sound: erschreckSounds[getRandomInt(5)], start: 50+getRandomInt(10) }); + GAME.mode = GROWN; + GAME.fail = failSound; + GAME.finish = finishSound; + GAME.runtime = 0; + } + if(MODE === CHILD) { + GAME.events.push({ sound: backgroundChild, start: 0}); + GAME.events.push({ sound: erschreckSounds[getRandomInt(2)], start: 30+getRandomInt(10)}); + GAME.mode = CHILD; + GAME.fail = failSound; + GAME.finish = finishSound; + GAME.runtime = 0; + } + GlobalLog.note('Starting new game', JSON.stringify(GAME, 2, null)); + isRunning = true; + GAME.countTimer = setInterval(gameTimer, 1000); + sendBotMsg('new game startet'); + + } else { + GlobalLog.warn('START not in state', STATE); + } +} + +function finish(value) { + GlobalLog.debug('Finish'); + if(STATE === STATES.STARTED) { + GlobalLog.note('You made it in', GAME.runTime, 'seconds'); + GlobalLog.note(GAME); + player.stop(); + playSound(GAME.finish); + STATE = STATES.FINISH; + clearInterval(GAME.countTimer) + GAME.successTimer = setInterval(failTimer, 600); + GlobalLog.note('Getting new image!') + exec(`fswebcam -r 1024x768 /tmp/last.png`, (err) => { + let rt = GAME.runTime; + if(err) { + GlobalLog.error('Could not get image', err); + } + GAME.events = []; + var msg; + var img; + if(MODE === GROWN) { + if(GAME.runTime < best.grown) { + best.grown = rt; + msg = `Gewinner in nur ${rt} Sekunden!\nNeue Bestzeit bei den Erwachsenen!`; + fs.copyFile('/tmp/last.png', '/tmp/bestgrown.png', (err) => {}); + } else { + msg = `Gewinner in nur ${rt} Sekunden!`; + } + } else { + if(GAME.runTime < best.child) { + sendBotMsg(`Neue Bestzeit bei den Kindern!`); + best.child = rt; + msg = `Gewinner in nur ${rt} Sekunden!\nNeue Bestzeit bei den Kindern!`; + fs.copyFile('/tmp/last.png', '/tmp/bestchild.png', (err) => {}); + } else { + msg = `Gewinner in nur ${rt} Sekunden!`; + } + } + sendBotGroupImg(groupid, '/tmp/last.png', msg); + GAME.led = true; + }); + preStart = false; + isRunning = false; + GAME = { + events: [], + runTime: 0 + } + } else { + GlobalLog.warn('FINISH not in state', STATE); + } + // exec(`killall aplay`); +} + +function fail(value) { + if(STATE === STATES.STARTED) { + GlobalLog.note('You failed after', GAME.runTime, 'seconds'); + GAME.startplayer.stop(); + player.stop(); + playSound(GAME.fail); + STATE = STATES.FAIL; + clearInterval(GAME.countTimer) + GAME.failTimer = setInterval(failTimer, 100); + GAME.events = []; + preStart = false; + isRunning = false; + GAME = { + events: [], + runTime: 0 + } + } else { + GlobalLog.debug('FAIL not in state', STATE); + } +} + +function toggle(value) { + if(STATE === STATES.FINISH || STATE === STATES.FAIL) { + if(GAME.failTimer) clearInterval(GAME.failTimer); + if(GAME.successTimer) clearInterval(GAME.successTimer); + if(MODE === GROWN) { + gpio.output(erwLED, true); + } else { + gpio.output(childLED, true); + } + GlobalLog.info('Toggle, old', MODE); + if(MODE === CHILD) { + MODE = GROWN; + gpio.output(childLED, false); + gpio.output(erwLED, true); + // set all values for GROWN mode + } else { + MODE = CHILD; + gpio.output(childLED, true); + gpio.output(erwLED, false); + // set all values for CHILD mode + } + } else { + GlobalLog.debug('Toggle not in state', STATE); + } +} + +function playSound(file, runTime){ + if(!file) { GlobalLog.error('Sound not defined.'); return; } + GlobalLog.info("Play sound : ", file); + if(GAME.playing) { + GlobalLog.debug("Stopping old soundfile", GAME.playing); + player.stop(); + } + // Do not stop background sound + if(GAME.runTime > 1) { + GAME.playing = file; + } else { + GAME.startplayer = player; + } + player.play({ path: `wav/${file}`, sync: true }).then(() => { delete GAME.playing }) +} + +setup(); + +TelLog.start('Starting bot...'); +bot.on('message', function (msg) { + const chatId = msg.chat.id; + const resp = msg; + TelLog.info(`${chatId}: ${resp.text}`); + if(resp.text === '/stats') { + TelLog.info(`Dump stats to channel ${groupid}`); + if (fs.existsSync('/tmp/bestchild.png')) { + sendBotGroupImg(groupid, '/tmp/bestchild.png', `Bestes Kind mit ${best.child} Sekunden.`); + } + if (fs.existsSync('/tmp/bestgrown.png')) { + sendBotGroupImg(groupid, '/tmp/bestgrown.png', `Bester Erwachsener mit ${best.grown} Sekunden.`); + } + } +}); + + diff --git a/game.js b/game.js index 8c5e6be..42b280f 100644 --- a/game.js +++ b/game.js @@ -12,8 +12,8 @@ const groupid='-1001703800044'; const GlobalLog = QLog.scope('Global'); const TelLog = QLog.scope('Telegram'); -GlobalLog.enableLogs(['warn']); -TelLog.enableLogs(['warn']); +GlobalLog.enableLogs(['debug']); +TelLog.enableLogs(['debug']); GlobalLog.headPadding = 30; TelLog.headPadding = 30; @@ -67,22 +67,28 @@ const finishSound = "kidscheering.wav"; const GROWN=1; const CHILD=2; +// LOW EDGE +let preStart = true; + +// game running? +let isRunning = false; + const STATES = { STARTED: 'started', FINISH: 'finshed', FAIL: 'failed' }; -const best = { - grown: 25, - child: 37 -} +let best; const bounce = {}; var STATE = STATES.FINISH; var MODE = GROWN; -var GAME = {}; +var GAME = { + events: [], + runTime: 0 +}; var chatId; @@ -113,6 +119,18 @@ function setup() { // await ctx.reply(`Bestes Kind mit nur ${best.child} sek`); // }); // bot.launch(); + fs.readFile('stats.json', 'utf8', (err, res) => { + if(err) { + GlobalLog.info("No stats found. Using defaults"); + best = { + grown: 25, + child: 37 + } + } else { + GlobalLog.info("Stats loaded."); + best = JSON.parse(res); + } + }); GlobalLog.note('Setting up GPIOs'); @@ -144,8 +162,8 @@ function setup() { gpio.setup(toggleButton, gpio.DIR_IN, gpio.EDGE_FALLING, () => { exec(`raspi-gpio set ${toggleButton_GPIO} pu`); }); - gpio.setup(startButton, gpio.DIR_IN, gpio.EDGE_FALLING, () => { - exec(`raspi-gpio set ${startButton_GPIO} pu`); + gpio.setup(startButton, gpio.DIR_IN, gpio.EDGE_BOTH, () => { + exec(`raspi-gpio set ${startButton_GPIO} pu`); }); gpio.setup(failButton, gpio.DIR_IN, gpio.EDGE_FALLING, () => { exec(`raspi-gpio set ${failButton_GPIO} pu`); @@ -207,6 +225,39 @@ function getRandomInt(max) { } function start(value) { + if(preStart === false || isRunning) { + GlobalLog.note('Prestart detect.'); + preStart = true; + if(GAME.startplayer) GAME.startplayer.stop(); + if(GAME.failTimer) clearInterval(GAME.failTimer); + if(GAME.successTimer) clearInterval(GAME.successTimer); + if(GAME.countTimer) clearInterval(GAME.countTimer) + if(player) player.stop(); + if(MODE === GROWN) { + gpio.output(childLED, false); + gpio.output(erwLED, true); + } else { + gpio.output(childLED, true); + gpio.output(erwLED, false); + } + if(preStart === false) return; + } + if(isRunning === true) { + GlobalLog.note('Game reset.'); + if(GAME.startplayer) GAME.startplayer.stop(); + if(player) player.stop(); + STATE = STATES.FINISH; + clearInterval(GAME.countTimer) + preStart = false; + isRunning = false; + if(GAME.failTimer) clearInterval(GAME.failTimer); + if(GAME.successTimer) clearInterval(GAME.successTimer); + gpio.output(statusLED, true); + GAME = { + events: [], + runTime: 0 + } + } if(GAME.failTimer) clearInterval(GAME.failTimer); if(GAME.successTimer) clearInterval(GAME.successTimer); if(MODE === GROWN) { @@ -214,10 +265,9 @@ function start(value) { } else { gpio.output(childLED, true); } - GAME = { - events: [], - runTime: 0 - }; + + GAME.runTime = 0; + if(STATE === STATES.FINISH || STATE === STATES.FAIL) { STATE = STATES.STARTED; // set all params for next game, then start counter and event timer @@ -244,6 +294,7 @@ function start(value) { GAME.runtime = 0; } GlobalLog.note('Starting new game', JSON.stringify(GAME, 2, null)); + isRunning = true; GAME.countTimer = setInterval(gameTimer, 1000); sendBotMsg('new game startet'); @@ -252,7 +303,7 @@ function start(value) { } } -async function finish(value) { +function finish(value) { GlobalLog.debug('Finish'); if(STATE === STATES.STARTED) { GlobalLog.note('You made it in', GAME.runTime, 'seconds'); @@ -264,6 +315,7 @@ async function finish(value) { GAME.successTimer = setInterval(failTimer, 600); GlobalLog.note('Getting new image!') exec(`fswebcam -r 1024x768 /tmp/last.png`, (err) => { + let rt = GAME.runTime; if(err) { GlobalLog.error('Could not get image', err); } @@ -272,25 +324,31 @@ async function finish(value) { var img; if(MODE === GROWN) { if(GAME.runTime < best.grown) { - best.grown = GAME.runTime; - msg = `Gewinner in nur ${GAME.runTime} Sekunden!\nNeue Bestzeit bei den Erwachsenen!`; + best.grown = rt; + msg = `Gewinner in nur ${rt} Sekunden!\nNeue Bestzeit bei den Erwachsenen!`; fs.copyFile('/tmp/last.png', '/tmp/bestgrown.png', (err) => {}); } else { - msg = `Gewinner in nur ${GAME.runTime} Sekunden!`; + msg = `Gewinner in nur ${rt} Sekunden!`; } } else { if(GAME.runTime < best.child) { sendBotMsg(`Neue Bestzeit bei den Kindern!`); - best.child = GAME.runTime - msg = `Gewinner in nur ${GAME.runTime} Sekunden!\nNeue Bestzeit bei den Kindern!`; + best.child = rt; + msg = `Gewinner in nur ${rt} Sekunden!\nNeue Bestzeit bei den Kindern!`; fs.copyFile('/tmp/last.png', '/tmp/bestchild.png', (err) => {}); } else { - msg = `Gewinner in nur ${GAME.runTime} Sekunden!`; + msg = `Gewinner in nur ${rt} Sekunden!`; } } sendBotGroupImg(groupid, '/tmp/last.png', msg); GAME.led = true; }); + preStart = false; + isRunning = false; + GAME = { + events: [], + runTime: 0 + } } else { GlobalLog.warn('FINISH not in state', STATE); } @@ -307,6 +365,12 @@ function fail(value) { clearInterval(GAME.countTimer) GAME.failTimer = setInterval(failTimer, 100); GAME.events = []; + preStart = false; + isRunning = false; + GAME = { + events: [], + runTime: 0 + } } else { GlobalLog.debug('FAIL not in state', STATE); } @@ -348,6 +412,7 @@ function playSound(file, runTime){ // Do not stop background sound if(GAME.runTime > 1) { GAME.playing = file; + } else { GAME.startplayer = player; } player.play({ path: `wav/${file}`, sync: true }).then(() => { delete GAME.playing })