From 0560dcffa96bd3daac276efaa0e6c2fbf33a5f09 Mon Sep 17 00:00:00 2001 From: Alec Date: Fri, 19 Nov 2021 01:11:00 -0500 Subject: [PATCH] some timer logic --- client/config/globals.js | 4 +- client/images/roles/Double-BlindMinion.png | Bin 0 -> 1810 bytes client/images/roles/DreamWolf.png | Bin 0 -> 8886 bytes client/images/roles/Hunter.png | Bin 0 -> 1875 bytes client/images/roles/KnowingMinion.png | Bin 0 -> 1810 bytes client/images/roles/Mason.png | Bin 0 -> 1364 bytes client/images/roles/Seer.png | Bin 0 -> 1401 bytes client/images/roles/Shadow.png | Bin 0 -> 1178 bytes client/images/roles/Sorcerer.png | Bin 0 -> 1903 bytes client/images/roles/Villager.png | Bin 0 -> 1773 bytes client/images/roles/Werewolf.png | Bin 0 -> 2278 bytes client/modules/GameCreationStepManager.js | 3 + client/modules/GameStateRenderer.js | 15 +++ client/modules/Templates.js | 8 +- client/modules/Timer.js | 56 ++++++++--- client/scripts/game.js | 30 +++++- client/styles/game.css | 61 +++++++++++- server/config/globals.js | 9 +- server/model/Game.js | 3 +- server/modules/ActiveGameRunner.js | 109 +++++++-------------- server/modules/GameManager.js | 6 +- server/modules/GameProcess.js | 26 +++++ server/modules/Logger.js | 1 + server/modules/ServerTimer.js | 69 +++++++++++++ 24 files changed, 298 insertions(+), 102 deletions(-) create mode 100644 client/images/roles/Double-BlindMinion.png create mode 100644 client/images/roles/DreamWolf.png create mode 100644 client/images/roles/Hunter.png create mode 100644 client/images/roles/KnowingMinion.png create mode 100644 client/images/roles/Mason.png create mode 100644 client/images/roles/Seer.png create mode 100644 client/images/roles/Shadow.png create mode 100644 client/images/roles/Sorcerer.png create mode 100644 client/images/roles/Villager.png create mode 100644 client/images/roles/Werewolf.png create mode 100644 server/modules/GameProcess.js create mode 100644 server/modules/ServerTimer.js diff --git a/client/config/globals.js b/client/config/globals.js index 52f6f4b..6a16d74 100644 --- a/client/config/globals.js +++ b/client/config/globals.js @@ -1,5 +1,6 @@ export const globals = { USER_SIGNATURE_LENGTH: 25, + CLOCK_TICK_INTERVAL_MILLIS: 100, ACCESS_CODE_LENGTH: 6, PLAYER_ID_COOKIE_KEY: 'play-werewolf-anon-id', ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789', @@ -18,7 +19,8 @@ export const globals = { }, EVENTS: { PLAYER_JOINED: "playerJoined", - SYNC_GAME_STATE: "syncGameState" + SYNC_GAME_STATE: "syncGameState", + START_TIMER: "startTimer" }, USER_TYPES: { MODERATOR: "moderator", diff --git a/client/images/roles/Double-BlindMinion.png b/client/images/roles/Double-BlindMinion.png new file mode 100644 index 0000000000000000000000000000000000000000..e48ec91a434b4f4863507bff978a7b3862951db1 GIT binary patch literal 1810 zcmZ`)Yfw^Y82(U{5{b6m#3;qubgU%E%o0-~voz8&&2hxSG;OsAE74L`3{z_Dva`)* znZ=?xUN&_rH7_X;TTaa~Wf$>Q%9>(c^0;Ll6*+qVXZKe#=gc?n`+d*%KF@RBdC&Ph z!8-^xWE%hgAuwRuUH~vc<2%n1&CGZ~FuGt5vw~Ov+`Wv~AGAVar`UkKK>(z=0FZeW zfN3<9IS7D&4!}?}0E}w@kdg`-LN=ot!FzUv`J?L{45q`?6>9rC~NT@$L_2Csp`IRG0KGw6Zf!&sSTqVySENTa&%ammgE_k zI#-|5*xH$PCmd_64P{Z^+BQ}Xisj2qrR086yCEOGdZ26;>hCMA&*hcam{?0zE0}`( zrHVx{1cQ6XbW@A@V&(RBDXBBI)nO)XP~bXyi{fHte3YjDvLX`CEL8E$lw@rRlD@|B zT35i5EsIS;iTk!_daITw@_V`FBg$HZ#d*wCYHTZS3T(E z*I!R5e`O2IPTs{wTui0vyS-{{`!wwDGp0nH1Vd{U5fQ@d+=Qt(i4W9rQtjirG1J1( zwNMB{LWZk$OnPk6eE??RaDH_-0NkUf-^hO$#NB zXKYYB5y)=^Io zn;z42Z^g*O%S`~?Qc2_v&29~$miEUD%Z9XGtE9*<45O*T6`OnTZsK z=5lFuqoDThypu=wC*g%1hkiY%Kso#BCt%UOAZ;Gz^V&*YMRRG2+WPk=dM+Ti##OMk&Y(vNQ5g_2h69wLN(GyJ+6yD&{8XBj#M4O_>`Y4y0RK&g+P33V%Z> z+{L?Rc4ivmrlb*mYmP`6j^PwN!1HEU&ty20i>Z|l+iPfW{>8S=?3DRUhT?O@la?qi z3BjO*hL@H;X(pW00AxCtl(bqZ1eM*c$_yJk>JNDU%sUGhB7kw_;Er z^BS2}UD*Ih$iT>$2@K0#ws*a_EF%x3zv=Ot81K8=ce2f?=iZA)CJ%UUv17m){&>1- zbqhCFE|B@mDY_uXhSF-;Nr>$mcyg=ptPpNd^?aK3d2hD%0u%QsCE?E~{Asnzk=?Fn zv62ccb7$PQ_pRhnVd+FYXX8Hh*!ocd5+X)Bq}yeFplnD_J$kce5V_Fh?qC5Qa5wge{RU>7&f8@E^(grYuy!Dlq~q>*AH! zM5G;M9jv!_m|QecBcIoVJ%B?$+bgWGXWpe$`iMF$4A+gKmnnw5uX>)dGLMvs#~Sas z=`uI2psWrEJ2#3Hrh!R^zcyI^%i~5wXJ=z>Mm{C2LPxrDvVTPKp@YeBjMx)#=mOm7 z>%6FRFX}pvFn2n`!=16gol5s$(CI%Z@Wx-){{SBUP(u9auLEXo-j|~Q5a=Jgt#WI$ F_+M+3T{8dx literal 0 HcmV?d00001 diff --git a/client/images/roles/DreamWolf.png b/client/images/roles/DreamWolf.png new file mode 100644 index 0000000000000000000000000000000000000000..bf5b085ed82fe913ddd4c27f7ab4a049085c488e GIT binary patch literal 8886 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 literal 0 HcmV?d00001 diff --git a/client/images/roles/Hunter.png b/client/images/roles/Hunter.png new file mode 100644 index 0000000000000000000000000000000000000000..c4251e3204003c7642b172cda4a5a867f4af3f22 GIT binary patch literal 1875 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 F_+M+3T{8dx literal 0 HcmV?d00001 diff --git a/client/images/roles/Mason.png b/client/images/roles/Mason.png new file mode 100644 index 0000000000000000000000000000000000000000..58752a2512cf218cc2e57736e332032066c5b1f3 GIT binary patch literal 1364 zcmeAS@N?(olHy`uVBq!ia0y~yV5|XQ4rZXpZ_5uqfD}u*qpu?a!^VE@KZ&eBzCyA` zkS_y6l^O#>Lkk1LFQ8Dv3kHT#0|tgy2@DKYGZ+}e3+C(!v;j((1^9%x0%;Z|hQ*2E z7dkDI4H;`}nSdghj$F}EQMJ|8pU%z}(koJOo6}bse0XsM8>jlp2?hWE|7Y8O(I2Re zu_VYZn8D%MjWiG^$=lsU@X=P=T|f?diKnkC`(sW%5nZk<>AcB6TlhR(978H@y}g^8 zf7?u=?c%cAo7Tp~|NFmwE@umaVuzUM>0O~O=jiJSo!YSeIkRG9*}fV^ph*Nk!{5ES z9`^f#h3!55D13Czdaz!W?Ss=J_D*?~x&Y4K$C*CHy8tDckMIkDRR9^1ocRp;N_7){ zI6i7$17u4>^um=m+X>%eFoP&~zoh*0YvYf1eXK#oHy^owG3JTcH{*x$wrzQ#Z^_pih3#&-VgyZ^sC z^}jlQp2*i3o3_b(&V5ugd*cs~^ufpe%~wAx{`mUW&L5W@m)!2CPkYo|fA&WAJZipKpN^Ud|9?~sYCI{>>b}0-7NX*@#i$JLk%z%{vY_5FaPY3 zKCgGp$9~tEkL$(v=S|3dv|aq_vVHeE_5XbY#Z3RB`%?XnzW1&Ec$?AZ{uEGDUXDFt zzxmUl`a54AUAHKH=>DZIQ+~bqo6G!QQ|q69aJAH&)gAwD?dxl9@y;Nd;{RN4UvF;q zchBr&>mLFwjsYf#Kff5;?up-dEBOC956CegN9+UsSOTp}`|?O{?OO5g%Rq{P1^+92 ztd92zXW!ud=e;pR$X;gui(1aF_dY(K*9`Pwz1Ac71P1q;1we~{w%yNX2)=AuTXZB{Q8eV{r(~v8;?}@5WxFWI4W6!kF6*2UngI6iYxe*E literal 0 HcmV?d00001 diff --git a/client/images/roles/Seer.png b/client/images/roles/Seer.png new file mode 100644 index 0000000000000000000000000000000000000000..cc4ed3f09be68266d0da5ca6f2a42a0ba46470e1 GIT binary patch literal 1401 zcmaJ=X;4#F7`Pw zl%yCN%XO9j0Gqhjs8j$5eEeHQ#F*E?S{U2R^4W=O09wUXNDc|Z^xW9gL;#My2Y~2v z0CX57nggJm0l-2w0L*g$&<ykH&UvIl1L|$&kwi7SOh@W0C%jXjkc7dSCNXJzuie8@l2Nx4~sYz)d{OLFs2YcgV zqNvtgyXrAO=EWsOlhqch?HOwwg_NUMUhlXlcKRoii;sp&rarL!VtBk{IR`5toc?^amW`OH z$ki?z1+gyrnSlb^`7z!62@KZ31d03WKYT?lQ3xG6CZ`limizX5wz1)%mn+zgm1l35 zzg<9Zx*cK^xK=2IH(zzMO>@D2qF_s*9BiN)+r5OA?<~hlOZJ~_X0=bVjkf2#r!tqu zz7>GQsvE-=^-C4I>hWmRE&*Ack{KOyX!CRpE9=($^3Ps)3OiPT1M0~S5gl5kQkX0X z?@FO5N^-MD2hkQaGgi%c3Y-pi^-vxwcHGZEsJJi|Y3zaZKZQ|c_3f~SF)~{M%X*F^ zBUuks4W^XSRMZ;Mn1KEYJm^exw8g}9C~~1`G?3rVI@tQqOQ6pgpfw#fU0mOE9hNED zX$i&03e?-S=(KuLXCANxd9-j z!tDY-lr$ViyL_D@oO46=IKO`IELMu?@eVPQS_v$f>k)ur_b zHD@p}h;tM0;+0Z(u_CR)_5kimh8jnaBiwGc5C<^^*r!u~aEm zaRka5+|egcB%!`9=KuB)xrK(NRjt%<3P3q~q)54~%8vsjU)jgM8lIv+{esBAn`NA2ru^<~sDcS@{^4avFN; z5_6Eq(P%@VfI5NZW7SDvqN-}3DWe^Iae#MisbY7naax!BRaPul+ zo4dMfjSb651c#WWP+Y5^gd@(zXP4r#Fs#)Bxxy%}>YxQw=JA!uMlIp3ui12%?T!*T z>!#_XAG_CJlshkaCoeCDw~v`ywh!BYAHz4;n-T2o8<6hDUPAHNXa52W>v0zV literal 0 HcmV?d00001 diff --git a/client/images/roles/Shadow.png b/client/images/roles/Shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..11f5e645d9a8d52e70a812c625ee8edc1f1dce0d GIT binary patch literal 1178 zcmeAS@N?(olHy`uVBq!ia0y~yV5|XQ7G|J`-AUP>K#C>Z(btiIVPik{pF~z5Um@8e z$d`ekN{xY`p@o6r7f`6-1p`B=0RzLU1O^7H84L{K1#@-<+5jaC0(?STfiw`v$jHRS z#0UxsZeU?yV`KYnY_!@hljQC0!Y{Fl;UPsCUysVAnHfq^;7)5S5Q;?~Yp*C!E?+{?22PiYGSob;0iDgAeL1bNtD>mih1Q!RChrKbEX!{_s)z z61(@G0?UL8Ta|nI<0a)^RLWW%`td{bWwTICI@^QC;m4+5{w$xcc3wmOe!)5u-`n$_ z%IeAa->Pf)_|g1Wzqose_Ur-+-mO&(c6CqRx&GL)Rlc=Y&tulxu8{y32b54EFz{IGp4zn1re{Nu;pJLThM^wuYKvmg0? z?D%`@|GPeX{1`242Lb==KWtBL-~Q~?-Esjhr|H225lGTsBn*Z26> zr`-Maz|Xi&8|du(4*7L`c{RT;mVPg>HN78S9z9i8w)^XjDfW>)a~?h}pI84i z*KF}2-|LTg$|~~uF z*&jUqpZ~`I7#s0__CG!hjJ|yz4tIxcf4u$4Q_Vlyt`zRWp3K2%|4KnULy~6KyOsHO+t(etV|89jLmcnOsotHCPg}hplHa= bPsvQH#I4~m%P(Ld!oc9^>gTe~DWM4fZPXPt literal 0 HcmV?d00001 diff --git a/client/images/roles/Sorcerer.png b/client/images/roles/Sorcerer.png new file mode 100644 index 0000000000000000000000000000000000000000..a85b4da539bfca29fb5e7002acf530cba00aa08b GIT binary patch literal 1903 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 )O1A-`eCzmkd;7kg3AHSxaL}zNE zBg5lMh9{mM5^8v^JpI+;@hdwYx4eCAu{_De5usFuxM6MIp0LrH4Bng9UjoM+w>V`! z|5xj1uX#GmbFoLdRlA%Jm!Arwo{dUj?7v$zSs#|G9He^;8=s$Z4}edU*R6mZ-a2$9 zYDUsahnm|ICB&S%I2)xR+(1!6 zNZf3_SW@O0sAw2XF!tG^J5|bxkr%e>wpXj0R}Ftye^IsIM5C!#Ws27{q1(Lx zCc4nYe87-pS1{@NBC+mrJ8$EI71dU74+Sy#(L-(JPX;Y>qBhC6i-6%;#$hQCzm(U1 zrsL#-`J#0Y3c-d6D(xT5)ObkipYQe^xdcK_h1}N5o$Coyy)qrFQOquFExOtXU$ChDgU-(tvB&*+t|Bp zImSjbqT~Zs;A+D}g)&JzLDFmJytNAFt$MMMUi)9O4NO)D@}lBIfLTR02Xs2uSIa)h z5LYU9pt)ruR7uNF=@50;Nh&LUw6RZErK+3T^jWsFC^ z)_Q!_#1G|(^2+d|ar7JIm=_-e%ty%UzdE?Aq`1jSOna@~-L@cwVI(n6WeX1h^P#<{595-gI@~V)=<QzZ5kS})H-*es7d>7?^HT0iWQAkK3u35}g|_e!(VvQM4O zCz^MZyO@s}Otg>OyXf4p4u}Vvt6SWqpUYMjtTskOqk)@)%9ZvC+C6*e96#a%Oubut z=5oYS)7XZn{)w+pE|ANMWH@BZ<;*Y;xy}!bya(fJ-abfg!p%U_lI5~!>y=oIya1UH zYw#T3c)m+edef@IY_O_E=+Zc)PH!sX4z+UkS{O-fbhyzDy@TEDxJY=uglrjY+Fc?x zlO@W2svUM{0>?qw2F;MQWMq&J5BR*NV2P5=sRZ>bgGI5IElHqi)Y*5W0;*i6?qAN) zEJaqK4;O}bGC(!+h_nfAf?HAafnINvd)Yr!f^2ZtcE9D+ML}LWNw3zi%qhYfD#2>> z9rTk1+i>iwVS8lk%J;Lu)~q^`0#(w1TFCPDl_920d8=0&&zkUYG=Gg#?Yp+_q{W`zxAYorZ0c$=-w?Ob2sDYvh`$8`sAKtp7xM;9~ z4z&xFwoitVF_lA;mFkqs$06&;gGBupq z2t8AGBwB6sQ^auG^q;+)=Cb0Zt{gp4UCe-%;W- { + return a.role.localeCompare(b.role); + }); for (let i = 0; i < options.length; i ++) { let optionEl = document.createElement("option"); let alignmentClass = customCards[i].team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL diff --git a/client/modules/GameStateRenderer.js b/client/modules/GameStateRenderer.js index b7cc8e4..a947711 100644 --- a/client/modules/GameStateRenderer.js +++ b/client/modules/GameStateRenderer.js @@ -4,6 +4,7 @@ import { toast } from "./Toast.js"; export class GameStateRenderer { constructor(gameState) { this.gameState = gameState; + this.cardFlipped = false; } renderLobbyPlayers() { @@ -79,6 +80,20 @@ export class GameStateRenderer { } name.setAttribute("title", this.gameState.client.gameRole); document.querySelector('#role-description').innerText = this.gameState.client.gameRoleDescription; + document.getElementById("role-image").setAttribute( + 'src', + '../images/roles/' + this.gameState.client.gameRole.replaceAll(' ', '') + '.png' + ); + + document.getElementById("game-role-back").addEventListener('click', () => { + document.getElementById("game-role").style.display = 'flex'; + document.getElementById("game-role-back").style.display = 'none'; + }); + + document.getElementById("game-role").addEventListener('click', () => { + document.getElementById("game-role-back").style.display = 'flex'; + document.getElementById("game-role").style.display = 'none'; + }); } } diff --git a/client/modules/Templates.js b/client/modules/Templates.js index 51ad6fd..bc9d995 100644 --- a/client/modules/Templates.js +++ b/client/modules/Templates.js @@ -29,7 +29,7 @@ export const templates = { "
" + "
" + "
" + - "" + + "" + "
" + "
" + "
" + @@ -37,9 +37,13 @@ export const templates = { "
" + "
" + "
" + - "
" + + "" + + "
" + + "

Click to reveal your role

" + + "

(click again to hide)

" + "
" } diff --git a/client/modules/Timer.js b/client/modules/Timer.js index 608873a..d2fe55d 100644 --- a/client/modules/Timer.js +++ b/client/modules/Timer.js @@ -9,16 +9,18 @@ See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API const messageParameters = { STOP: 'stop', - TOTAL_TIME: 'totalTime', - TICK_INTERVAL: 'tickInterval' + TICK_INTERVAL: 'tickInterval', + HOURS: 'hours', + MINUTES: 'minutes' }; onmessage = function (e) { if (typeof e.data === 'object' - && e.data.hasOwnProperty(messageParameters.TOTAL_TIME) + && e.data.hasOwnProperty(messageParameters.HOURS) + && e.data.hasOwnProperty(messageParameters.MINUTES) && e.data.hasOwnProperty(messageParameters.TICK_INTERVAL) ) { - const timer = new Singleton(e.data.totalTime, e.data.tickInterval); + const timer = new Singleton(e.data.hours, e.data.minutes, e.data.tickInterval); timer.startTimer(); } }; @@ -30,7 +32,10 @@ function stepFn (expected, interval, start, totalTime) { } const delta = now - expected; expected += interval; - postMessage({ timeRemaining: (totalTime - (expected - start)) / 1000 }); + postMessage({ + timeRemainingInMilliseconds: totalTime - (expected - start), + displayTime: returnHumanReadableTime(totalTime - (expected - start)) + }); Singleton.setNewTimeoutReference(setTimeout(() => { stepFn(expected, interval, start, totalTime); }, Math.max(0, interval - delta) @@ -38,16 +43,17 @@ function stepFn (expected, interval, start, totalTime) { } class Timer { - constructor (totalTime, tickInterval) { + constructor (hours, minutes, tickInterval) { this.timeoutId = undefined; - this.totalTime = totalTime; + this.hours = hours; + this.minutes = minutes; this.tickInterval = tickInterval; } startTimer () { - if (!isNaN(this.totalTime) && !isNaN(this.tickInterval)) { + if (!isNaN(this.hours) && !isNaN(this.minutes) && !isNaN(this.tickInterval)) { const interval = this.tickInterval; - const totalTime = this.totalTime; + const totalTime = convertFromHoursToMilliseconds(this.hours) + convertFromMinutesToMilliseconds(this.minutes); const start = Date.now(); const expected = Date.now() + this.tickInterval; if (this.timeoutId) { @@ -61,19 +67,20 @@ class Timer { } class Singleton { - constructor (totalTime, tickInterval) { + constructor (hours, minutes, tickInterval) { if (!Singleton.instance) { - Singleton.instance = new Timer(totalTime, tickInterval); + Singleton.instance = new Timer(hours, minutes, tickInterval); } else { // This allows the same timer to be configured to run for different intervals / at a different granularity. - Singleton.setNewTimerParameters(totalTime, tickInterval); + Singleton.setNewTimerParameters(hours, minutes, tickInterval); } return Singleton.instance; } - static setNewTimerParameters (totalTime, tickInterval) { + static setNewTimerParameters (hours, minutes, tickInterval) { if (Singleton.instance) { - Singleton.instance.totalTime = totalTime; + Singleton.instance.hours = hours; + Singleton.instance.minutes = minutes; Singleton.instance.tickInterval = tickInterval; } } @@ -84,3 +91,24 @@ class Singleton { } } } + +function convertFromMinutesToMilliseconds(minutes) { + return minutes * 60 * 1000; +} + +function convertFromHoursToMilliseconds(hours) { + return hours * 60 * 60 * 1000; +} + +function returnHumanReadableTime(milliseconds) { + + let seconds = Math.floor((milliseconds / 1000) % 60); + let minutes = Math.floor((milliseconds / (1000 * 60)) % 60); + let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24); + + hours = hours < 10 ? "0" + hours : hours; + minutes = minutes < 10 ? "0" + minutes : minutes; + seconds = seconds < 10 ? "0" + seconds : seconds; + + return hours + ":" + minutes + ":" + seconds; +} diff --git a/client/scripts/game.js b/client/scripts/game.js index f9a5c52..1e76185 100644 --- a/client/scripts/game.js +++ b/client/scripts/game.js @@ -19,8 +19,9 @@ export const game = () => { userId = gameState.client.id; UserUtility.setAnonymousUserId(userId, environment); let gameStateRenderer = new GameStateRenderer(gameState); - processGameState(gameState, userId, socket, gameStateRenderer); // this socket is initialized via a script tag in the game page HTML. - setClientSocketHandlers(gameStateRenderer, socket); + const timerWorker = new Worker('../modules/Timer.js'); + processGameState(gameState, userId, socket, gameStateRenderer, timerWorker); // this socket is initialized via a script tag in the game page HTML. + setClientSocketHandlers(gameStateRenderer, socket, timerWorker); } }); } else { @@ -29,7 +30,7 @@ export const game = () => { }); }; -function processGameState (gameState, userId, socket, gameStateRenderer) { +function processGameState (gameState, userId, socket, gameStateRenderer, timerWorker) { cancelCurrentToast(); switch (gameState.status) { case globals.STATUS.LOBBY: @@ -58,7 +59,7 @@ function processGameState (gameState, userId, socket, gameStateRenderer) { } } -function setClientSocketHandlers(gameStateRenderer, socket) { +function setClientSocketHandlers(gameStateRenderer, socket, timerWorker) { socket.on(globals.EVENTS.PLAYER_JOINED, (player, gameIsFull) => { toast(player.name + " joined!", "success", false); gameStateRenderer.gameState.people.push(player); @@ -84,6 +85,16 @@ function setClientSocketHandlers(gameStateRenderer, socket) { } ); }) + + socket.on(globals.EVENTS.START_TIMER, () => { + runGameTimer( + gameStateRenderer.gameState.timerParams.hours, + gameStateRenderer.gameState.timerParams.minutes, + globals.CLOCK_TICK_INTERVAL_MILLIS, + null, + timerWorker + ) + }) } function displayStartGamePromptForModerators(gameStateRenderer) { @@ -99,3 +110,14 @@ function displayStartGamePromptForModerators(gameStateRenderer) { }); } + +function runGameTimer (hours, minutes, tickRate, soundManager, timerWorker) { + if (window.Worker) { + timerWorker.onmessage = function (e) { + if (e.data.hasOwnProperty('timeRemainingInMilliseconds') && e.data.timeRemainingInMilliseconds > 0) { + document.getElementById('game-timer').innerText = e.data.displayTime; + } + }; + timerWorker.postMessage({ hours: hours, minutes: minutes, tickInterval: tickRate }); + } +} diff --git a/client/styles/game.css b/client/styles/game.css index 62c8ef3..a101038 100644 --- a/client/styles/game.css +++ b/client/styles/game.css @@ -64,7 +64,7 @@ h1 { #game-role { position: relative; - border-bottom: 2px solid gray; + border: 5px solid transparent; background-color: #e7e7e7; display: flex; flex-direction: column; @@ -85,6 +85,56 @@ h1 { /*transform-style: preserve-3d;*/ } +#game-role-back { + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + display: flex; + align-items: center; + justify-content: center; + background-color: #333243; + border: 5px solid #61606a; + position: relative; + flex-direction: column; + cursor: pointer; + max-width: 17em; + border-radius: 3px; + height: 23em; + margin: 0 auto 2em auto; + width: 100%; + box-shadow: 0 1px 1px rgba(0,0,0,0.11), + 0 2px 2px rgba(0,0,0,0.11), + 0 4px 4px rgba(0,0,0,0.11), + 0 8px 8px rgba(0,0,0,0.11), + 0 16px 16px rgba(0,0,0,0.11), + 0 32px 32px rgba(0,0,0,0.11); + /*perspective: 1000px;*/ + /*transform-style: preserve-3d;*/ +} + +#game-role-back h4 { + font-size: 24px; + padding: 0.5em; + text-align: center; + color: #e7e7e7; +} + +#game-role-back p { + color: #c3c3c3; + font-size: 20px; +} + +#game-timer { + padding: 1px; + background-color: #3c3c3c; + color: whitesmoke; + border-radius: 3px; + font-size: 35px; + text-shadow: 0 3px 4px rgb(0 0 0 / 85%); + border: 1px solid #747474; +} + #role-name { position: absolute; top: 6%; @@ -100,10 +150,15 @@ h1 { } #role-image { + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + -moz-user-select: none; position: absolute; - top: 34%; + top: 37%; left: 50%; transform: translate(-50%, -50%); + width: 78%; } #role-description { @@ -114,7 +169,7 @@ h1 { transform: translate(-50%, 0); font-size: 16px; width: 78%; - max-height: 7em; + max-height: 6em; } #game-link img { diff --git a/server/config/globals.js b/server/config/globals.js index 15fa0cc..947f9bb 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -1,6 +1,7 @@ const globals = { ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789', ACCESS_CODE_LENGTH: 6, + CLOCK_TICK_INTERVAL_MILLIS: 100, CLIENT_COMMANDS: { FETCH_GAME_STATE: 'fetchGameState', GET_ENVIRONMENT: 'getEnvironment', @@ -8,7 +9,8 @@ const globals = { }, STATUS: { LOBBY: "lobby", - IN_PROGRESS: "in progress" + IN_PROGRESS: "in progress", + ENDED: "ended" }, USER_SIGNATURE_LENGTH: 25, USER_TYPES: { @@ -33,6 +35,11 @@ const globals = { ERROR: "error", WARN: "warn", TRACE: "trace" + }, + GAME_PROCESS_COMMANDS: { + END_GAME: "endGame", + START_GAME: "startGame", + START_TIMER: "startTimer" } }; diff --git a/server/model/Game.js b/server/model/Game.js index efd635c..7c71fa7 100644 --- a/server/model/Game.js +++ b/server/model/Game.js @@ -1,5 +1,6 @@ class Game { - constructor(status, people, deck, hasTimer, moderator, timerParams=null) { + constructor(accessCode, status, people, deck, hasTimer, moderator, timerParams=null) { + this.accessCode = accessCode this.status = status; this.moderator = moderator; this.people = people; diff --git a/server/modules/ActiveGameRunner.js b/server/modules/ActiveGameRunner.js index e3c415f..dc5adb1 100644 --- a/server/modules/ActiveGameRunner.js +++ b/server/modules/ActiveGameRunner.js @@ -1,90 +1,49 @@ const { fork } = require('child_process'); const path = require('path'); - -const logger = require('./logger')(false); +const globals = require('../config/globals'); class ActiveGameRunner { - constructor () { + constructor (logger) { this.activeGames = {}; + this.logger = logger; } - // runGame = (game, namespace, gameStateFn) => { - // logger.debug('running game ' + game.accessCode); - // const gameProcess = fork(path.join(__dirname, '/GameProcess.js')); - // gameProcess.on('message', (msg) => { - // switch (msg.command) { - // case serverGlobals.COMMAND.END_COUNTDOWN: - // logger.debug('GAME PARENT PROCESS ' + game.accessCode + ': COMMAND: END COUNTDOWN'); - // namespace.in(game.accessCode).emit(serverGlobals.COMMAND.END_COUNTDOWN); - // gameProcess.send({ - // command: serverGlobals.COMMAND.START_GAME, - // cycleNumber: game.words.length - 1, - // cycleLength: game.timePerWord * 1000, - // accessCode: game.accessCode - // }); - // break; - // case serverGlobals.COMMAND.START_GAME: - // game.status = serverGlobals.GAME_STATE.STARTED; - // game.lastCycleTime = new Date().toJSON(); - // logger.debug('GAME PARENT PROCESS ' + game.accessCode + ': COMMAND: START GAME'); - // namespace.in(game.accessCode).emit(serverGlobals.COMMAND.START_GAME, { - // firstWord: game.words[0].baseword, - // gameLength: game.words.length, - // timePerWord: game.timePerWord * 1000 - // }); - // break; - // case serverGlobals.COMMAND.CYCLE_WORD: - // game.currentWordIndex += 1; - // game.lastCycleTime = new Date().toJSON(); - // logger.debug('GAME PARENT PROCESS ' + game.accessCode + ': COMMAND: CYCLE WORD'); - // if (game.currentWordIndex < game.words.length) { - // namespace.in(game.accessCode).emit(serverGlobals.COMMAND.CYCLE_WORD, { - // word: game.words[game.currentWordIndex].baseword, - // index: game.currentWordIndex + 1, - // totalTime: game.timePerWord * 1000, - // gameLength: game.words.length - // }); - // } - // gameProcess.send({ - // command: serverGlobals.COMMAND.CYCLE_WORD, - // cycleIndex: game.currentWordIndex, - // cycleLength: game.timePerWord * 1000, - // accessCode: game.accessCode, - // gameLength: game.words.length - // }); - // break; - // case serverGlobals.COMMAND.END_GAME: - // game.status = serverGlobals.GAME_STATE.ENDED; - // if (!game.posted) { - // logger.debug('GAME PARENT PROCESS: GAME ' + game.accessCode + ' HAS ENDED...BEGINNING POST TO DATABASE'); - // this.postGameFn(game).then(() => { - // game.posted = true; - // logger.debug('GAME ' + game.accessCode + ' SUCCESSFULLY POSTED'); - // namespace.in(game.accessCode).emit(serverGlobals.COMMAND.END_GAME, game.accessCode); - // }); - // } - // break; - // } - // }); - // - // gameProcess.on('exit', () => { - // if (this.activeGames[game.accessCode]) { - // delete this.activeGames[game.accessCode]; - // logger.debug('GAME ' + game.accessCode + ' REMOVED FROM ACTIVE GAMES.'); - // } - // }); - // gameProcess.send({ command: serverGlobals.COMMAND.START_COUNTDOWN, accessCode: game.accessCode }); - // game.status = serverGlobals.GAME_STATE.STARTING; - // game.startCountdownTime = new Date().toJSON(); - // namespace.in(game.accessCode).emit(serverGlobals.COMMAND.START_COUNTDOWN); - // } + /* We're only going to fork a child process for games with a timer. They will report back to the parent process whenever + the timer is up. + */ + runGame = (game, namespace) => { + this.logger.debug('running game ' + game.accessCode); + const gameProcess = fork(path.join(__dirname, '/GameProcess.js')); + gameProcess.on('message', (msg) => { + switch (msg.command) { + case globals.GAME_PROCESS_COMMANDS.END_GAME: + game.status = globals.STATUS.ENDED; + this.logger.debug('PARENT: END GAME'); + namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_GAME, game.accessCode); + break; + } + }); + + gameProcess.on('exit', () => { + this.logger.debug('Game ' + game.accessCode + ' has ended. Elapsed: ' + (new Date() - game.startTime) + 'ms'); + }); + gameProcess.send({ + command: globals.GAME_PROCESS_COMMANDS.START_TIMER, + accessCode: game.accessCode, + logLevel: this.logger.logLevel, + hours: game.timerParams.hours, + minutes: game.timerParams.minutes + }); + game.startTime = new Date().toJSON(); + namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.START_TIMER); + } } class Singleton { - constructor () { + constructor (logger) { if (!Singleton.instance) { logger.log('CREATING SINGLETON ACTIVE GAME RUNNER'); - Singleton.instance = new ActiveGameRunner(); + Singleton.instance = new ActiveGameRunner(logger); } } diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js index 42e2d1c..97244bf 100644 --- a/server/modules/GameManager.js +++ b/server/modules/GameManager.js @@ -9,7 +9,7 @@ class GameManager { constructor (logger, environment) { this.logger = logger; this.environment = environment; - this.activeGameRunner = new ActiveGameRunner().getInstance(); + this.activeGameRunner = new ActiveGameRunner(logger).getInstance(); this.namespace = null; //this.gameSocketUtility = GameSocketUtility; } @@ -37,6 +37,9 @@ class GameManager { if (game) { game.status = globals.STATUS.IN_PROGRESS; namespace.in(accessCode).emit(globals.EVENTS.SYNC_GAME_STATE); + if (game.hasTimer) { + this.activeGameRunner.runGame(game, namespace); + } } }); } @@ -51,6 +54,7 @@ class GameManager { const newAccessCode = this.generateAccessCode(); let moderator = initializeModerator(gameParams.moderatorName, gameParams.hasDedicatedModerator); this.activeGameRunner.activeGames[newAccessCode] = new Game( + newAccessCode, globals.STATUS.LOBBY, initializePeopleForGame(gameParams.deck, moderator), gameParams.deck, diff --git a/server/modules/GameProcess.js b/server/modules/GameProcess.js new file mode 100644 index 0000000..c170a72 --- /dev/null +++ b/server/modules/GameProcess.js @@ -0,0 +1,26 @@ +const globals = require('../config/globals.js'); +const ServerTimer = require('./ServerTimer.js'); + +process.on('message', (msg) => { + const logger = require('./Logger')(msg.logLevel); + switch (msg.command) { + case globals.GAME_PROCESS_COMMANDS.START_TIMER: + logger.debug('CHILD PROCESS ' + msg.accessCode + ': START TIMER'); + runGameTimer(msg.hours, msg.minutes, logger).then(() => { + logger.debug('Timer finished for ' + msg.accessCode); + process.send({ command: globals.GAME_PROCESS_COMMANDS.END_GAME }); + process.exit(0); + }); + break; + } +}); + +function runGameTimer (hours, minutes, logger) { + const cycleTimer = new ServerTimer( + hours, + minutes, + globals.CLOCK_TICK_INTERVAL_MILLIS, + logger + ); + return cycleTimer.runTimer(); +} diff --git a/server/modules/Logger.js b/server/modules/Logger.js index d0d1a5c..01a1db4 100644 --- a/server/modules/Logger.js +++ b/server/modules/Logger.js @@ -2,6 +2,7 @@ const globals = require('../config/globals'); module.exports = function (logLevel = globals.LOG_LEVEL.INFO) { return { + logLevel: logLevel, log (message = '') { const now = new Date(); console.log('LOG ', now.toGMTString(), ': ', message); diff --git a/server/modules/ServerTimer.js b/server/modules/ServerTimer.js new file mode 100644 index 0000000..8cb439b --- /dev/null +++ b/server/modules/ServerTimer.js @@ -0,0 +1,69 @@ +/* ALL TIMES ARE IN MILLIS */ + +function stepFn (expected, interval, start, totalTime, ticking, timesUpResolver, logger) { + const now = Date.now(); + if (now - start >= totalTime) { + clearTimeout(ticking); + logger.debug('ELAPSED: ' + (now - start) + 'ms (~' + (Math.abs(totalTime - (now - start)) / totalTime).toFixed(3) + '% error).'); + timesUpResolver(); // this is a reference to the callback defined in the construction of the promise in runTimer() + return; + } + const delta = now - expected; + expected += interval; + ticking = setTimeout(function () { + stepFn( + expected, + interval, + start, + totalTime, + ticking, + timesUpResolver, + logger + ); + }, Math.max(0, interval - delta)); // take into account drift +} + +class ServerTimer { + constructor (hours, minutes, tickInterval, logger) { + this.hours = hours; + this.minutes = minutes; + this.tickInterval = tickInterval; + this.logger = logger; + } + + runTimer () { + const interval = this.tickInterval; + const totalTime = convertFromHoursToMilliseconds(this.hours) + convertFromMinutesToMilliseconds(this.minutes); + const logger = this.logger; + logger.debug('STARTING TIMER FOR ' + totalTime + 'ms'); + const start = Date.now(); + const expected = Date.now() + this.tickInterval; + let timesUpResolver; + const timesUpPromise = new Promise((resolve) => { + timesUpResolver = resolve; + }); + const ticking = setTimeout(function () { + stepFn( + expected, + interval, + start, + totalTime, + ticking, + timesUpResolver, + logger + ); + }, this.tickInterval); + + return timesUpPromise; + } +} + +function convertFromMinutesToMilliseconds(minutes) { + return minutes * 60 * 1000; +} + +function convertFromHoursToMilliseconds(hours) { + return hours * 60 * 60 * 1000; +} + +module.exports = ServerTimer;