From 4f9950c6a9eb8e2d2dc5544fe1510af1e0d27981 Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Sat, 8 Feb 2025 16:23:55 +0100 Subject: [PATCH 1/9] Create landing page base layout --- web/src/App.tsx | 4 ++- web/src/pages/lobby/LobbyPage.scss | 42 ++++++++++++++++++++++++++++++ web/src/pages/lobby/LobbyPage.tsx | 23 ++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 web/src/pages/lobby/LobbyPage.scss create mode 100644 web/src/pages/lobby/LobbyPage.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index b1fc803..ac08d68 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -2,17 +2,19 @@ import React from "react"; import { Socket } from "socket.io-client"; import LandingPage from "./pages/landing/LandingPage"; import GamePage from "./pages/game/GamePage"; +import LobbyPage from "./pages/lobby/LobbyPage"; export interface AppProps { socket: Socket; } const App = (props: AppProps) => { - const renderedPage: string = "LANDING"; + const renderedPage: string = "LOBBY"; return (
{renderedPage === "LANDING" && } + {renderedPage === "LOBBY" && } {renderedPage === "GAME" && }
); diff --git a/web/src/pages/lobby/LobbyPage.scss b/web/src/pages/lobby/LobbyPage.scss new file mode 100644 index 0000000..226320d --- /dev/null +++ b/web/src/pages/lobby/LobbyPage.scss @@ -0,0 +1,42 @@ +.lobby-page { + height: 100%; + width: 100%; + padding: 2%; + margin: 0 auto; + text-align: center; + + background: linear-gradient(-45deg, #03444a, #00a8a8, #f1bc52, #ff8f4b); + background-size: 400% 400%; + animation: gradient 15s ease infinite; + height: 100vh; + + @keyframes gradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } + } + + .lobby-page-body { + margin: auto; + width: 20%; + min-width: 300px; + + .lobby-user-name { + margin: 0 auto 10px; + padding: 10px; + width: 60%; + border: 2px solid cyan; + border-radius: 10px; + } + + .start-game-button { + margin-top: 30px; + } + } +} diff --git a/web/src/pages/lobby/LobbyPage.tsx b/web/src/pages/lobby/LobbyPage.tsx new file mode 100644 index 0000000..231027a --- /dev/null +++ b/web/src/pages/lobby/LobbyPage.tsx @@ -0,0 +1,23 @@ +import "./LobbyPage.scss"; + +const LobbyPage = () => { + const userNames = ["Laura", "Miguel", "David"]; + + return ( +
+
+

Trains And Roads

+
+
+ {userNames.map((name) => ( +
{name}
+ ))} +
+ +
+
+
+ ); +}; + +export default LobbyPage; -- 2.40.1 From 00c365de84def4576c8e2aa8fce11c6c8ff716f2 Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Sun, 9 Feb 2025 14:38:08 +0100 Subject: [PATCH 2/9] Add transition to lobby page --- interface/server-events/UpdateLobbyEvent.ts | 6 ++--- web/src/App.tsx | 29 ++++++++++++++++++--- web/src/pages/lobby/LobbyPage.scss | 10 ++++++- web/src/pages/lobby/LobbyPage.tsx | 23 +++++++++++++--- 4 files changed, 57 insertions(+), 11 deletions(-) diff --git a/interface/server-events/UpdateLobbyEvent.ts b/interface/server-events/UpdateLobbyEvent.ts index 90f696a..2f0881d 100644 --- a/interface/server-events/UpdateLobbyEvent.ts +++ b/interface/server-events/UpdateLobbyEvent.ts @@ -3,12 +3,12 @@ import { ServerEvent } from "./ServerEvent"; export type UpdateLobbyEvent = { playerNames: Array; + gameCode: string; }; export function attachHandlerToUpdateLobbyEvent( socket: Socket, handler: (event: UpdateLobbyEvent) => void, -): () => void { - socket.on(ServerEvent.LOBBY_UPDATE, handler); - return () => socket.off(ServerEvent.LOBBY_UPDATE, handler); +): void { + socket.once(ServerEvent.LOBBY_UPDATE, handler); } diff --git a/web/src/App.tsx b/web/src/App.tsx index ac08d68..7b878b6 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,20 +1,43 @@ -import React from "react"; +import React, { useCallback, useState } from "react"; import { Socket } from "socket.io-client"; import LandingPage from "./pages/landing/LandingPage"; import GamePage from "./pages/game/GamePage"; import LobbyPage from "./pages/lobby/LobbyPage"; +import { attachHandlerToUpdateLobbyEvent } from "interface"; export interface AppProps { socket: Socket; } const App = (props: AppProps) => { - const renderedPage: string = "LOBBY"; + const { socket } = props; + const [renderedPage, setRenderedPage] = useState("LANDING"); + + const [initialPlayerNames, setInitialPlayerNames] = useState([""]); + const [gameCode, setGameCode] = useState(""); + + const setupNextPageTransition = useCallback(() => { + if (renderedPage === "LANDING") { + attachHandlerToUpdateLobbyEvent(socket, (event) => { + setInitialPlayerNames(event.playerNames); + setGameCode(event.gameCode); + setRenderedPage("LOBBY"); + }); + } + }, [renderedPage]); + + setupNextPageTransition(); return (
{renderedPage === "LANDING" && } - {renderedPage === "LOBBY" && } + {renderedPage === "LOBBY" && ( + + )} {renderedPage === "GAME" && }
); diff --git a/web/src/pages/lobby/LobbyPage.scss b/web/src/pages/lobby/LobbyPage.scss index 226320d..5a935f7 100644 --- a/web/src/pages/lobby/LobbyPage.scss +++ b/web/src/pages/lobby/LobbyPage.scss @@ -35,8 +35,16 @@ border-radius: 10px; } + .game-code { + margin: 0 auto 10px; + padding: 10px; + width: 60%; + border: 2px solid blue; + border-radius: 10px; + } + .start-game-button { - margin-top: 30px; + margin: 30px 0; } } } diff --git a/web/src/pages/lobby/LobbyPage.tsx b/web/src/pages/lobby/LobbyPage.tsx index 231027a..e92f168 100644 --- a/web/src/pages/lobby/LobbyPage.tsx +++ b/web/src/pages/lobby/LobbyPage.tsx @@ -1,7 +1,21 @@ +import { useState } from "react"; import "./LobbyPage.scss"; +import { Socket } from "socket.io-client"; +import { attachHandlerToUpdateLobbyEvent } from "interface"; -const LobbyPage = () => { - const userNames = ["Laura", "Miguel", "David"]; +export interface LobbyPageProps { + initialPlayerNames: string[]; + gameCode: string; + socket: Socket; +} + +const LobbyPage = (props: LobbyPageProps) => { + const { initialPlayerNames, gameCode, socket } = props; + const [playerNames, setPlayerNames] = useState(initialPlayerNames); + + attachHandlerToUpdateLobbyEvent(socket, (event) => { + setPlayerNames(event.playerNames); + }); return (
@@ -9,12 +23,13 @@ const LobbyPage = () => {

Trains And Roads

- {userNames.map((name) => ( -
{name}
+ {playerNames.map((name) => ( +
{name}
))}
+
Game code: {gameCode}
); -- 2.40.1 From b72a74789c6cdc8295c7dba1e84fc40017b9e94a Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Wed, 4 Dec 2024 12:25:24 +0100 Subject: [PATCH 3/9] Add icons for special cells and fix piece placement issues --- interface/types/Cell.ts | 6 +- web/public/factory-icon.png | Bin 0 -> 19135 bytes web/public/house-icon.png | Bin 0 -> 17778 bytes web/public/university-icon.png | Bin 0 -> 17895 bytes web/src/pages/game/components/BoardCell.scss | 85 ++++++------------- web/src/pages/game/components/BoardCell.tsx | 13 +-- 6 files changed, 37 insertions(+), 67 deletions(-) create mode 100644 web/public/factory-icon.png create mode 100644 web/public/house-icon.png create mode 100644 web/public/university-icon.png diff --git a/interface/types/Cell.ts b/interface/types/Cell.ts index 6412cc5..c71bb2b 100644 --- a/interface/types/Cell.ts +++ b/interface/types/Cell.ts @@ -63,7 +63,7 @@ export class Cell { trackType: track.type, }; }) - .some(({ trackExternalNodes, trackType }) => { + .reduce((isConnected, { trackExternalNodes, trackType }) => { let isTrackConnected: boolean = false; trackExternalNodes .filter((node) => node.traverseBorder() instanceof Exit) @@ -98,8 +98,8 @@ export class Cell { } }); - return isTrackConnected; - }); + return isConnected || isTrackConnected; + }, false); if (!hasAnyConnection) { throw Error("No adjacent exit or piece available to connect to"); diff --git a/web/public/factory-icon.png b/web/public/factory-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e7c3ad86ab6038748a4477ad8fd1f72e3a2d46fb GIT binary patch literal 19135 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

`oTS6=D}!v^Ls2N_ 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

TFeg{gjCvHo|fq}@;d3Ozwk!F`>t@VI=8 zg01f+H6wW6o8C1g7kG&6mV{R`futDycK*z?pT(m{WyEmDWX4+B6(-O$4L)C+_L?Sv zFE*aa`?YIGk^qCq+Wbep&NL>F9efI)bJG*>q-pe!^V)^}A`jY$VyM?wa=lRonxYw$ zaUkdCo3SZ958`^()s=<@WF?V|MuSJJ*TU%at?3CObxeSp2@op9tjJ4u_ME=?puKH4 zBy0Emg(3efv~mt3zAVuSNFBc$;@qd6ZPS+tiZ+`#A4~fi%S@a z@Q4t3aFvj6%3JRj68l^V;fZQrvipFb33fC1_kc`*%Gv4sWb6i!Tv`j~?>UIrX)BsK z3MUVZ9d^7E>*OHI&(Wo{?#tksVFK(-fS%0+FvTS<6~S|$9HG({*K}xNp9s&L%&g;r zg5S^s(N_E>vTACIu*FmRt7kbco}1G35fzsYo_r=f#e41({{Ew9EPZEA07C!c^Gtag zp_x^C2sNbVNBhotDq(v6rEf>hZX{W7nDP0RuSH+iZX{rqe+&%Oi$W|FgKKOgmM+cn zs~&t%{tb+nyzA#tFGc>?;Vo~jYimnkuY$iQeV{LQkY!zCjP={iO6hPaaw^z=t@sv> zl#3^ORnAkX>?Y4#O^a|%t(4K~HwH-Z$P>)&K7JL?m}}wua~a7#det=LKjvz-D}yi0 zR|X#p?g9Nty2(u9tXF)c=ihazRUOy%%wAq$GYJkv{tL#B9F}#Y zzNg*0DsDP|QX6^i5b9=)b@b#nNmWTYu{vlbC1vh}5X<@iO_0doKS<9(*kl4K6M;X{n{~}7YGO7i|aJO0S3ZUBdMjVM=84~^I@E`D1oz4DTeGq zXZ{=K*^6tNAyaEP@N*c0uZKxkg;f36;A8$#>u6gCy(}isk!^Pae8k`ufL63d&&H9m zd&XR=WABKzVUI*yD2%Dqm$ev{%V~uC6j^@gYEUSh!@XB*);cpHR1@scY!MmXV7)toD%#vd?he_-pJ9EROCC92n10J4hDso5OBJMjOsW0BgE=sj zK1wI~R%uZ$zW>o12T*&rR}SA9ilx^)sqI93N1mFsxVf5QJgku9sd0-LA&Ei@0l{&AE09P@RiASJMN5m(`o&4cfAwGs! zf0)5jLq$Ov>9|Pm=^i~(Ccs&o=zON8X>7NWGj^ftINRe4p6{R1l8ujMK4TbP@Vt`- zUYL5$axdwc5gTrQ>aKSlp?DRO__iiPqu~mpPXnHZQw))#7vFsgw{rJR={2A;fm6Rr z#xbmMB!~~lK6t{?V9C6no!|`~id#c}u2Juyjlmm-Kk*+P8uJ-(9Czf016yk&(gc?n zJlSLzgL{|o>qp~w!NM4thejkrlmg4o%J~?4TjQoJ;+VKXN^+qL5SDb}c%^RX&n$j} z>?^CX72_jeE-Wrj;sC|JJs&zX3sic~vn&|$7nI%is0j$u-&f=#MDItLk8aGPBtQa# zTM%N`QILfRu&tISbFqu)*HaL7YS{$GrdVRCv za8?y*d5#f6wQkKOTi~~LB7_9Cwes4M{!mlo-C9@Hif+C@?D2P1yc~x|PNxZ1{dhbt z=Da9DQ~if|(2kVGdvX^0NpXO*ePpT6zw2Cy*34i1=;)U<|DuK;{UPp|u>lhp!*rWE z26@`tyzUm`u^^Y))j8TBSQAk1ykBnf)6sc1aO9VIaKC)q+gg1k zLV*7+eztWdD-%h2ALlmvFE6^O;jR;oSe>DWvuOgYR+}0(o0DeJJQae!_Ul6PF97;zzAJynMtw+Bge7 z86xxS$Y>0xoisTxf2H3>^=`4~#7sodJ?nXig$|YHZej{|-D*W96?kO4IDiHKFwp(h(Q(=V3 zl+APLl|F;ECWmTEFSYY<2|V$Y9+2Agt>HanNxT{HX;3+p>HmX{JC$M2GUO`DWU`lh>{UufLpEH%qt?VMVxrO%5>!OeM5qux z1l!u&?1TfK=!dippS}b-7%7E3`we729*w>S$V5?R&PH-V7U4jVZ+ZC7;j~1K5a;ix zol28zj zlK3ND8t>j$-&3m)NaPF^@UGT$%I)fSKAOt}vexfkgyYhY2OMCk37s3A-rjpgThdM~ z>}hE!OH1E-{krz`G4uIUQ+ep~5TGNr7$4X80VmA_JnSl5u;P8Y#!(RpBYEaM1&)0x z7Awoeho9Eh9vAR#vsC@A1=5U!_Y_T4h^lvD6F8d(Wwj!)VG; zQrZ}CNJ@xh(+px3D#R>V^}3ArMb5e4?0CIKpbG1oN0+U9s#~9ieYDOFDoKlw3FQS0 zAuVn9MwBM`tpSt`iO#|1h z8MV-cLlBS&1`5l9M+kfI8B73FsH91;P&(F5ib#mIz#(&|om6$RW%n27;i8KsnQ2mE z2BD6mQ}QxbJpo?qEVcnjzStd(C!aA>5z`Z#_*&Xg$eVO!6i3Rm`5XBR5 zjhC_fxJGrFsuZDx&+_M7i}ib_-de4vZIu#PPv9((>D(zZKRb8{QP;lmOmD~Wb628tecN1M3&q=Z=s0C~?LGB+Ea_1UXheF~e7t|?P_vZHH#EGn zb||rS=Gz?$2bgjKVL5X4=jplj#l5MfDk^te3pCUo1}Gd@+8a=!AV0O7<$K04t1e{n z2u;7P=8Pfd-4erpO6< zw&t!YAg1V$@?QRMzsKN_RseT{dpLIDgt8@htQ0eWLJZCxPMWrv-LdDRYCOio;sb7n z(aSQHHbxJT|I#F~0Ve?(YC){aWfg_XTbfHMyHtCHNL-at0#hl_%FFFyq;>jhwTf-b z3g)tqwGW#%yS%I}IK>zz5X{eMBy%;&z~Uts)6-F1#+F`US0{_d?J|hui^ZT8q8ZYY zNg|`6CZO-Di-CxVdp*QwVNc}f858?hqxlriR%=(hh>+nu^3Ak82Qh6JkCl;7`pN{{ zlR$;yHpC#J4L6uAq$l_y(C<{M$04bhFdZtgRq8`RLgvpv?e{V}3@QHpx~brh zEF_J<5s*D8e5$`Mh!|f+Dle?wS$Vvo z$ov^J+1LL?gYDU{ctR1yv zCJ6aJ#m_V==yQx&7hSEx;g#=DjRZ{Q1I2v3)_ zd>oVhBA+^AQtN^D*~kAw`KJkNr=oI1xgl)LCQr@je)hzQyuc*(i&q9GqR8PTC_cqpoO9zc#39eBVg-1t;Op z-ZvI69-BZncJt62$YwJ!w2L`E4CKj-9L{&W<+?`}i>`O;wUG*prX(GuP4ot)&@>@5 zgHMi(ZpjNP#&!=a2a4MKUfvw&c0OhC;G+t{NZd7vlNv$I7;%S$m*D^^E5uJUC%_~5IqkbTck7$z{7X9Mm3P;8$Kp($Iy zO~SLX$i=GP_wwikxuOc{U6+}`M-A7x<$0~d`gTx>%6Qv26F!-d&xWvw-+c@X$9ja0Orm!kwXhqUQ>6g-quE@A*OjT+zN zZ5{3@NO7|5(7Si>V#T8E`bNgL)%Wtq&-kk1yGXWL1Zqj)ia2r3rRBvi*ylI1hE@y-cBp?h8kUzZjhc>Ma{gLa0PLSrg_ zuo6aGf!!Jaa0aC5xK+$h&Dw6Th`J+ik|98hGvMm4RlrUam1U@lmg5`54lk=%o^R7W zOE}8HhS<-O!PB-hvFmr?%}C!(3)VA6Vkr8J|4!`uyzS~F_=fYk(nz>Ajbg}UrCL5o7v6EPuH#nUbMU8sivN)SKehT2DHE^ za%)GnSU2=4yk1pe49 zPDMDYj`c@#fAu5t9bHjon#1OyG@h5+m*ZzJo6QMd+VRV~n%lYhs9R zJY933a_012w6=^xz!ltn$`Yt((yH%E(waA-__mEnyo(vOBUj#9l{#yl>Js{WvMO%$ zE6+Z?*GDs1b^NFCnox`aNxr!rVW9BH4#DV=kf5Qyd~I=P3BGoyEkQ|eJwNGzNBdHe znqb;JH~?FHIWg0K=fw6%!)nQdL_Mu1U}>4RDRe zaHLPFykOatz`JWVDcqp*nf;wmu%+;}yz5H4DML&}%`rABuCbw;{TzUDU`U3ZCoi=w z!M}o0O%MAsCX^8tXpJ!@fQIb;F$?r{%aAFDXL4+;oJIZ8_IhtCeB&p=QE`Z&yfq~- zpSlyAV+i2yjPdl-_B>Np@*F5hudHu?tNr%Q^jzdWb`TV6NPl!Q)#Ut#rS+y%u2!5E zQM8^t5VaOfW}k_s`H$C$wq{Ayv!M%T(_OvUAZTKFwVQdR-6-8qNCwi#w_OYr-MV)$ zy0YXUHV?7T#(*b6k13fI)&~uH_(#c0u2}JFlb$sI$nY}j0!E)*Ng*WQ247C_1lmpD zq7G$q-57b~?JwY>YJuxGSs-pINY9t&k3S!AX8QE9Fl^cxxj#}%eT^*=Tc@v~c9P`; zFtZ`8Bfau}Oa1tDQ)^{nZs__Fy z!AqntOslogz3WwxNR_s3{pok1C&UOSP1X>YT?Le=Pz4WS8tVa(E4%5rwT60_=T44x zJl}unNgT%v2zIt)u=(kRma_>g1LEN*C{X06I;c4@voJh)qK6>@%2`o?H@vjlcaG7_}DJpGJ6CVTA#W%Lpz90|$L8Tm#cd ziJD2uzA4Lq7^5ci0H{BXi>&7>hfJkgeo_j}!BTOzNpE|VE&Ij7^#Mc8E>ndqKRWF) z%SVs!n(9dJW#{70;c8fMnmoC?5zlG!U0Seb1<&`rf?KZQZEulWWsI;?f#ve4$4>#yA=@B{=Wjh!qt6Q}1fp}ec9UaGgRye;qf*q*vt!uvdSPfrZUvgGy*xp6KKZ z{dO>(k=kz8mW(=VJ|F)~X5ngGgN$=_=)l?~2qCUa;P-2^P$p1B|WvT1K7}QMNOBipgr03ACdf!eg5du>WM_RB0 zXjYNk&3NfDD=?(!@F4nj+30|cULu-rKdz{lFfV69czituFD*={Vo^yKJO2O#HEaX5`Wb9j^~3X*yAjjlyr4fNoOl zgKn7shTZ+*iJ5J^6hm{QN6;<|RW@}?-pJZ@@xr0D%$n=HkkFW`dl@D4YoX(~MqLzX z0TqiAHE15kC)rqlZpEi^;kT6oY+bD3@}9q6?R3jBxF&yFa{4$mj%BlmQoI&SMlb9p zGnSg_^~v?I6HEL=UP#JR-1UYrfij__dvqzU=8|Fp0#wYPNhmy>dopK7&kX&|?^4^y zOcPcvv(tI^$s{XlNFl#&}}NO0ONqe}lf417p0fE0W89byTgMTCrRA#%)R3G~1DO(~3J$CwOk zJ>`@}>h4u5gIlbe(k}Rexk4{>{Vq3XBCnoA<>N5fk~m1avod-OSZm4nB$|32h;Oc( z6m;Eu{MObmu))dFz*_35t_DO1_>TSo$fbV#8Y*^l(Kj2&UTz&qJ#3wOlOax zarQ|)ueUL0vmB+uQS2<;c!&9rPgm|xO&RUDop=W#vND>X5U6#T>{;2J#-HbY9&;wR z9rpU$W7N={Ojxp)5v*%M+Wm=vUegh?j(17!`?P<6R7grnF=FBISJ(eFR{ON;7&7DA zT=xd@K@R&<-|`Ezn)5yvGG%eKqoS1JGxT)aHOL*atH39oZN~G^L|=iG7DtvH%WoCs z-&c6G&UZW4*7w~YH~kG{kKBZKG%rh>99}{ZT@aXnO?dn!d@ocr*PJ|P?3;=9X1ula zlij^@O4>Gbo&|hKRlv3&g>jxJN!3_}FsNih@t?gX*y+YUth>|8SHELIEQTNJ%$68F zu#mKsx8)Nj7l)4#T2N)E+t#f^>6U2lMA^Qlv1TS+-riogkbN-VcI~|`lkS8C$ju`s zk*sfFSp%g9nz3Bu#guU~{`Q;A4h5H#Yn9rybqx<}M;cR2wO{8(saX3Jja=o&N#ZmO zC5cUTk@bS0Fsa545vba{X%fAnD=*OOR4|lZyDw^hn?*izJ4)HUxB4ie#t3hyJ;GeXp^FWywSH0zz+AFl7VRz`ovIDj3&LI&)u zsEL4ta=hS`-O3(9pT&w^`M`G^tvWd%^*iq>)cdEGdUj*dhr$3vW~yfQl`<^uPN(85 zZWKw?2XJn}y9WaGS8Bhqy3-P51KR*{^^1CgPf&zER4ub#3!UpVSrHazs`C+u#}s@s-4_Ph|{(muJJ z!*;Hf`}{FFJIufN$*VB|zqAFLOI8#u4O-E9ghHlwQeg@01Wp`TVnP$jm^7gY&Oo-? z5Q>N9@yuoIt?9Jw_W}5Vgh6f>F3g3{;n1b^%zCLB?}OS-^4-Khn<8f zPnUT7qEmuw=s2lf)y*Gm>m4Hu4nXRaAEM~7B24Ra9rXzKXda#JJ&xRY>I%n^uHj1~ zq6u|uIZ}`VMMHYs~^opvnfWp?PSh$k-YZ{l>-vlpGvly|w@aBiAm2IO_RO{p=>ZfS$sWz-FT=wq0hYzB*l~ zlJhfbJ~~<2cD@aM2!xD(cF0+ZI=(y84zdCmK4OHmO^R~}F`7;ShVygNoxA(kN# zh-{Ho>Eq(r{kF`Py+oh4cZY1WcbB98kttR+^a7Mw>R|Bg__X477bSrGgq3HER>$*# z=;n20sj|=+kJhn(H>)R)vidG-S+an2R23`%C5ZDtAx*}!qZxetw97BFSvrf}iS%bq zYMKuw9+DcYAO_Q4L3$(@(j!lT5zz&2D5?v>6RP0J;>q)V|$ra}-`X(?f>`dcDp zJVuCP?)=_FfECwm1AMFJqp^~4^XsJ3I9?Fh{1A0(W!IV8^JO*1E90}~hm9}ZX+K~- zVQ%{Rsq9eO`HQ#NQi1lVnK#|IA_s7JA}N+32x_di7%29aCGorHTzjBWE@e(p_E7E# z2y)zQO)(e^EPfC9oUt^i=Ok2g>XpC{l5`|dpuF)uzOwvy$YqhiNxI|d=u{Ffc{s&X z+;aBV4n#i`z7f&WBnLEB*2_?=YMusSjCw8Vx^kb5?kRdDffQfS*mHV`0k79Wmv#r!+H52%RI7t zePNXnvPPyn8!*@xIB7{a*Hd!1|6`ldE|mr&)`bKpYQrTz1?fhRT6`a`sfn~|q(k~4 zo{ked0kGdi-k;jss%WrGETn&80;Qk-dC>!3tE58my*-`=iQE!FNb>iF3eO2oLH@tp zmO8*u1rDO%lw>;+{*58Huz6&e?GjmWMQHDK{%i__=S^E>w@-M}BH%*Wli z7Z+{Sl$JIaMZ?pq!IB9~lHHC&s-l4%m-}i>C(OKW4OB<3Jeq!-sGilp-V`my{Q@9a zz)_xBrV_6W9O6rcYIzgGOl=*;@KxyWG!amWL`*56-*My!Gw4Pt%z*>swGWaSS?3?= zhB#=`=yPkn48CEA+gAf$Hf4*{>XMO@93=+$@oYicf=AIn(2WtfS z>(>JY$m(k+$GVda4+=&+fEn}qD*mwR90eAB{M0ggpM!i(iVS;yMA8Dju0L+K%hf=K z{4J{}_l6C&JbzYzt4Q_0Ypalu``socN6-Jx1cH3_&-a8qJ3RmF>bZEVXg?IpXm z!g+m2nxeMU0~>{c7-EQlT~zblf@ITx+%ds`JmTRX4V#jm&Dk5@BOKv*5#d{*BS=?|a;$TCAnp-JSO^%!I)a6Z00 z7#4n;Wy2arc3yi0x>Aq*P`?r=O2`tR_}4;V)vr2bqMkx42X41!d~XmK;aJE-Zf=`q8g8)bv0;dFLU(2W_a+U-tC4ICg#4 z?Y2c-vi6}FCLn=#SB=J%CMK>k^uRl$Zmwb3bmWjtt(xgx-TfjXgs(=2HUg+25S>Se zd|1(;QOKBL_1lHN{c!oZ{Fhb5D+H5Dcf{EKHy5urzK0@34swyrY~k3-z?liFC@3<& zJ|;ssT>2Em8TFpYukQ0#8eH)3fW)BWi{c5KZ1Z!X;ETSB98uQAm+sjCV&{z)`PWAWrfjw4W$RD*Kv)<>(#NudiBc=6R`>9h#kr` z8D+gGA9XI@`9Xc@rz^VESzGt5TT5+9No?n`WZFO$-FI__Qs|gf81N3Z2`J_Onb~&F zkbmA>r*yK{O&Hy#|M)gU4iV=ep|w|&Y{zIZLofj|^55F51MWzI(5z#o4pb?N`(cRu z0H;a5qnKEL84$1D0v}+)G*7#YyZF)hTOkqnBI3aK{fHmct_2Qr?@Y*3G%>ti*e8O0 zgq;&q+xbkC!6Qy-w+^o^e*;NQo*Uqxmm=`|mCE(rCHr@k4^O-1q-#4oR8W%leEL=+ z<0g)tJ8~*JQsANi@3GSL) z48}FFzsjT^_=f5grn#g<{dGYhcPqci2j9%q?n>>3HCrqsfdntkic0v^JJq$emW3}O zcwgN@x=SJ&5G~7><+G(iy_E~U4!2#|_tU$wL*mu=$(6R*Z!S4p#pOx~mOl!rAG+O< z|2B5H&;>TZvk6HdnWg~abwWHXl9Fwa#hy~v@$X`ek z2UQ_!e{U=kD1dOF^Z|8sN}bW1&2etk4(1XCITSL*);X*X6vpg|R}OAEW(Q&MJw5QB z>QEIP9QVNytAiW70!jUo%5IZ3R3!#KWW4_Vp5_N(x2}?rY%|bS23X*649KPEi560Y zqCWMwmE91VLHJ_=jNeeCLMYgTWIQ4j+ZM|~+5-nsKb9u66@$ER!f!i@erNo|*7p$Mg1J|g+0HbOoOrgqQZa-1#fXJw|(t2%P1$VvBfFt!I};}-*9l6vA;C5gOVvm$@`?uNEpI9Jr95zrvEduvikXX)Fw}9Mf^Blq zvkas=$#+SJ2%3VoysvGbongu6Jt+orb#=Ag>e4N_uiGARIz=Co`UtQkPCqte*Fe|e z?Y_INd^Pw<$K~cArtJQLy5;MIG|5u3?`6;$+Vp{7#m2B@IUzVX?DKGC|Nl`wsT4uaM>!L@7+;Vav=fjG+O~P) z#+Wz-+L0KK!38Ha+S|6lJ&wlQA4VS-t-|XI%7PRg*VH7IvCy`3%|#UHUX}*5B3o=v zr8M$OZdG5X=&OzcV-ghBmg73{$EF3;+gY8u`YnBbrZ-qBeY5>`E-?VWsc+Il2G$y> zmly;F3%!EDeS?T#$h%loug9!9ZcL`!Z1+%O%l~Muop{dk{O?J}-uWnPK-N6ix`G-TOo0zqrw;<)!#M9Gw&x$- z7ygc^OFA=1XDNuvBGLczv|>)w8#PJI+XM8%S?hsC(x>5`NjW`M_?Y&0K)R~94n<1J zCLcv3x6G?hYqtqy8>wTBc=)U7;gAU~2epye9Ia0F)Drf){Jc-yuNa$>!hN#ICaR#k z>RlA+IZ-mV^1>pNcHd-wkJ~v%<-BF=Wzfzw8P*pC8a3_aHKmr*uh7K6J>x{ELS@`M zid4+?qkj_>L(_7WN=SI69xc%F*f;SNA>E;4C;IF~e5n@XdE>uok?Lbfb}TL;SV{6& z%p;G5tMN*j$U6B1nhzAH)HgjcDNHnuRQzHceI^`!1CXefyK}T|!t(kICl> zFv%=4oxgsuid8E%#B;Q~MtEiA(7nl|*iEe#JogP9(c1@yrQA%ukPcT?Sig}cUp`6< z)-q;4l|{qm2$Y@xu#oO=v__HoOfDrHYa^EG-IaIBnx1K>TtCw65U|O%NMQ2`eHY}= zo}6ZP2Psf50p1-)#uwM3CKixZ$P`eQ@=dA+k-eYhD7ufxikRuEB#*6V6RacRMeUaJP%F^4}jg-!<;b(nnp0V4b<9 zs}@5_^iEQZ2WqXqpNP;vq`02`WD+-cw<)+!{^iBl>d=Z_O@6Nz#upsmUZkkx zlZ`q2R&mBxbA#+2CDrFRSGCJ{_lm#Qvz(O9j{dV_@#|HL)A1f7H=i%!@_{`lY@&fALHihxv*VX2K0QIDnsn`XwDu;A_#w`?FA1_bSEKq9E1wPK-K2pfjGU>FVrrXX_}V>Exy)2 z)y6@}dw!F}a4Na#(B>!=!eJv6BnKnBonD6qQH~=KWzV@g_C`H1l^QGd(OlniUy#=@ zf@Wafi1hnrvXDIwX+E|^(1^+g3X~zEvD}|&s(c@!>uSEv%t}PG>lRe`TxK{VKmGm6 z2SwP=YzEf}C=y&S6nJgjoKm^4Z%_Smf6pKOZlnOl7+52U4>5|mO6*yT+!TSximvEE{5jx z^rKpTb=za%5jV!->K=5JOx&Vk4&sB|5T3!OK+mpM=cy@<+3b!p5SsPfId-C!VD-7< z>eWNFgpWLo`5cx808m;kTuXv>_HQUx%&lvelGw)rbte+yobI_pc2w!o&4{sGk6&gr z$S3rK!B>VTr5w%D!Ol6flA~?s!se;c-iFB5?6$2E)g%J?>pLhUaBFfL*@EIHh!ciX z4r)LAQrGfcoO2y6Jeg`F?@?C#k&E}~f;}khMtzmue*IS4;LqU~3v2@4jo7R+Xjb$X z1HMq-17m8SHF>!uan!L-PuJCZ_arhb?*O7o2C9WQDpWZ-G?tl#lXO_kJtmNvx_|Sn z6h2FRb71u)1;OC^Ku^TVX|`)Mj-pOw9W_lm-M8d%INR}Jg=&v-5d03F^cnFLW$P-@ zSug*+X!m>S-Ws6@gDgLOoCd`0ICb4Vtuec2Q&>O2=*j6#>y;2F-qrXBUrL|E<QGVsoUPjRHfDBAUK)MK^{LSC@M9(wm!+t`epFkR+5 zk?gDhMSk35+?H-AY`o4fLE7!;9Z=dV$f@i3rp|ar4##&KwM*~(knK$U+l~|0MQrP7k>NGO|Cxe6G zNtw{g3$FXF*|CN0ra+SX+WqiKt4+PjHRTtb1JC6d@4Dv< zkr}ABIGThjRhuRtQkkVwOHpl2FqhOc!TD@rA9f+mvLWiI-tw=H=yL#c3DArTYecbx zD)2{ { }; return ( -

+
{directions.map((direction) => { const traversedNode = cell.getNodeAt(direction).traverseBorder(); const isExit = traversedNode instanceof Exit; @@ -77,6 +74,12 @@ const BoardCell = (props: BoardCellProps) => { src={`pieces/${cell.placedPiece.id}.jpeg`} > )} + {cell.cellType !== CellType.NORMAL && ( + + )}
); }; -- 2.40.1 From 86d5137857ea41e0fac1a10e831e327927870b4c Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Wed, 4 Dec 2024 17:09:21 +0100 Subject: [PATCH 4/9] Implement special dice usage and layout --- web/src/pages/game/GamePage.scss | 1 + web/src/pages/game/GamePage.tsx | 67 ++++++++++++++++-- web/src/pages/game/components/BoardCell.tsx | 57 ++++++++-------- web/src/pages/game/components/DiceSet.scss | 14 +++- web/src/pages/game/components/DiceSet.tsx | 76 +++++++++------------ web/src/pages/game/components/Die.scss | 5 ++ web/src/pages/game/components/Die.tsx | 16 +---- web/src/pages/game/components/GameBoard.tsx | 27 +++----- web/src/pages/game/types/DieViewProps.ts | 9 +++ 9 files changed, 164 insertions(+), 108 deletions(-) create mode 100644 web/src/pages/game/types/DieViewProps.ts diff --git a/web/src/pages/game/GamePage.scss b/web/src/pages/game/GamePage.scss index 9847b6c..87b1eba 100644 --- a/web/src/pages/game/GamePage.scss +++ b/web/src/pages/game/GamePage.scss @@ -12,5 +12,6 @@ h1 { .right-panel { display: flex; flex-direction: column; + padding: 50px; } } diff --git a/web/src/pages/game/GamePage.tsx b/web/src/pages/game/GamePage.tsx index 4b06b3d..4e15ed6 100644 --- a/web/src/pages/game/GamePage.tsx +++ b/web/src/pages/game/GamePage.tsx @@ -3,6 +3,7 @@ import GameBoard from "./components/GameBoard"; import DiceSet from "./components/DiceSet"; import "./GamePage.scss"; import { PieceId } from "interface"; +import { DieViewProps } from "./types/DieViewProps"; const GamePage = () => { const getRandomPieceId = () => { @@ -12,22 +13,80 @@ const GamePage = () => { const [dice, setDice] = useState( [1, 2, 3, 4].map(() => { - return { + const dieViewProps: DieViewProps = { pieceId: getRandomPieceId(), isSelected: false, isDisabled: false, + isSpecial: false, + rotation: 0, + }; + return dieViewProps; + }), + ); + const [specialDice, setSpecialDice] = useState( + [ + PieceId.P03, + PieceId.P08, + PieceId.P13, + PieceId.P14, + PieceId.P15, + PieceId.P19, + ].map((pieceId) => { + return { + pieceId: pieceId, + isSelected: false, + isDisabled: false, + isSpecial: true, rotation: 0, }; }), ); + const [specialDieUsedInRound, setSpecialDieUsedInRound] = useState(false); + + const modifyDieState = ( + matcher: (die: DieViewProps) => boolean, + newStateComputer: (die: DieViewProps) => Partial, + ) => { + setDice( + dice.map((die) => { + if (!matcher(die)) return die; + return { + ...die, + ...newStateComputer(die), + }; + }), + ); + setSpecialDice( + specialDice.map((die) => { + if (!matcher(die)) return die; + return { + ...die, + ...newStateComputer(die), + }; + }), + ); + }; + + const fetchDie = (matcher: (die: DieViewProps) => boolean) => { + return dice.concat(specialDice).find((die) => matcher(die)); + }; return (

Game Page Title

- -
- + +
+
diff --git a/web/src/pages/game/components/BoardCell.tsx b/web/src/pages/game/components/BoardCell.tsx index ecfa25f..1c426a6 100644 --- a/web/src/pages/game/components/BoardCell.tsx +++ b/web/src/pages/game/components/BoardCell.tsx @@ -1,33 +1,32 @@ -import { Cell, CellType, directions, Exit, PieceId } from "interface"; +import { Cell, CellType, directions, Exit } from "interface"; import "./BoardCell.scss"; import { useState } from "react"; +import { DieViewProps } from "../types/DieViewProps"; export interface BoardCellProps { cell: Cell; - dice: { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }[]; - setDice: React.Dispatch< - React.SetStateAction< - { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }[] - > - >; refreshBoardRender: () => void; + modifyDieState: ( + matcher: (die: DieViewProps) => boolean, + newStateComputer: (die: DieViewProps) => Partial, + ) => void; + fetchDie: ( + matcher: (die: DieViewProps) => boolean, + ) => DieViewProps | undefined; + setSpecialDieUsedInRound: React.Dispatch>; } const BoardCell = (props: BoardCellProps) => { - const { cell, dice, setDice, refreshBoardRender } = props; + const { + cell, + refreshBoardRender, + modifyDieState, + fetchDie, + setSpecialDieUsedInRound, + } = props; const [pieceRotationAngle, setPieceRotationAngle] = useState(0); const handleBoardCellClick = () => { - const selectedDie = dice.find((die) => die.isSelected); + const selectedDie = fetchDie((die) => die.isSelected); if (!selectedDie) return; try { cell.placePiece( @@ -38,17 +37,17 @@ const BoardCell = (props: BoardCellProps) => { console.log(error); return; } - setDice( - dice.map((die) => { - return die !== selectedDie - ? die - : { - ...selectedDie, - isSelected: false, - isDisabled: true, - }; - }), + modifyDieState( + (die) => die === selectedDie, + () => { + return { + isSelected: false, + isDisabled: true, + }; + }, ); + if (selectedDie.isSpecial) setSpecialDieUsedInRound(true); + // Set rotation to the piece in the board, not the die setPieceRotationAngle(selectedDie.rotation); refreshBoardRender(); }; diff --git a/web/src/pages/game/components/DiceSet.scss b/web/src/pages/game/components/DiceSet.scss index 87776c6..da09ab7 100644 --- a/web/src/pages/game/components/DiceSet.scss +++ b/web/src/pages/game/components/DiceSet.scss @@ -1,9 +1,8 @@ .dice-set-actions { display: flex; flex-direction: row; - padding: 50px 50px 0; width: max-content; - margin: auto; + margin: auto auto 50px auto; img { border: 3px solid green; @@ -24,5 +23,14 @@ display: grid; grid-template-columns: repeat(2, auto); gap: 20px; - padding: 50px; + margin: 0 0 50px 0; +} + +.special-dice-set { + display: grid; + grid-template-columns: repeat(3, auto); + margin: 0 0 50px 0; + column-gap: 12%; + row-gap: 20px; + width: 100%; } \ No newline at end of file diff --git a/web/src/pages/game/components/DiceSet.tsx b/web/src/pages/game/components/DiceSet.tsx index 1779190..518e2a1 100644 --- a/web/src/pages/game/components/DiceSet.tsx +++ b/web/src/pages/game/components/DiceSet.tsx @@ -1,55 +1,42 @@ -import { PieceId } from "interface"; import "./DiceSet.scss"; import Die from "./Die"; import React from "react"; +import { DieViewProps } from "../types/DieViewProps"; export interface DiceSetProps { - dice: { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }[]; - setDice: React.Dispatch< - React.SetStateAction< - { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }[] - > - >; + dice: DieViewProps[]; + specialDice: DieViewProps[]; + modifyDieState: ( + matcher: (die: DieViewProps) => boolean, + newStateComputer: (die: DieViewProps) => Partial, + ) => void; + specialDieUsedInRound: boolean; } const DiceSet = (props: DiceSetProps) => { - const { dice, setDice } = props; - const handleDieClick = (die: { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }) => { - if (die.isDisabled) return; - const newDiceState = dice.map((oldDie) => { - const isSelected = die === oldDie; - return { - ...oldDie, - isSelected: isSelected, - }; - }); - setDice(newDiceState); + const { dice, specialDice, modifyDieState, specialDieUsedInRound } = props; + const handleDieClick = (clickedDie: DieViewProps) => { + if (clickedDie.isDisabled) return; + const isSpecialDie = clickedDie.isSpecial; + if (isSpecialDie && specialDieUsedInRound) return; + modifyDieState( + () => true, + (die) => { + return { + isSelected: die === clickedDie, + }; + }, + ); }; const handleRotateButton = (rotation: number) => { - const newDiceState = dice.map((die) => { - if (!die.isSelected) return die; - const rotationAngle = (die.rotation + rotation + 360) % 360; - return { - ...die, - rotation: rotationAngle, - }; - }); - setDice(newDiceState); + modifyDieState( + (die) => die.isSelected, + (die) => { + return { + rotation: (die.rotation + rotation + 360) % 360, + }; + }, + ); }; return ( @@ -70,6 +57,11 @@ const DiceSet = (props: DiceSetProps) => { ))}
+
+ {specialDice.map((die) => ( + + ))} +
); }; diff --git a/web/src/pages/game/components/Die.scss b/web/src/pages/game/components/Die.scss index b3f4195..675582d 100644 --- a/web/src/pages/game/components/Die.scss +++ b/web/src/pages/game/components/Die.scss @@ -29,4 +29,9 @@ } transition: transform 0.5s cubic-bezier(.47,1.64,.41,.8); +} + +.special-dice-set .dice { + height: 80px; + width: 80px; } \ No newline at end of file diff --git a/web/src/pages/game/components/Die.tsx b/web/src/pages/game/components/Die.tsx index ba8474f..c039c39 100644 --- a/web/src/pages/game/components/Die.tsx +++ b/web/src/pages/game/components/Die.tsx @@ -1,19 +1,9 @@ -import { PieceId } from "interface"; import "./Die.scss"; +import { DieViewProps } from "../types/DieViewProps"; interface DieProps { - die: { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }; - handleDieClick: (die: { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }) => void; + die: DieViewProps; + handleDieClick: (die: DieViewProps) => void; } const Die = (props: DieProps) => { diff --git a/web/src/pages/game/components/GameBoard.tsx b/web/src/pages/game/components/GameBoard.tsx index 0997041..8ea33b9 100644 --- a/web/src/pages/game/components/GameBoard.tsx +++ b/web/src/pages/game/components/GameBoard.tsx @@ -1,25 +1,18 @@ -import { buildBoard, PieceId } from "interface"; +import { buildBoard } from "interface"; import "./GameBoard.scss"; import { useState } from "react"; import BoardCell from "./BoardCell"; +import { DieViewProps } from "../types/DieViewProps"; export interface GameBoardProps { - dice: { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }[]; - setDice: React.Dispatch< - React.SetStateAction< - { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }[] - > - >; + modifyDieState: ( + matcher: (die: DieViewProps) => boolean, + newStateComputer: (die: DieViewProps) => Partial, + ) => void; + fetchDie: ( + matcher: (die: DieViewProps) => boolean, + ) => DieViewProps | undefined; + setSpecialDieUsedInRound: React.Dispatch>; } const GameBoard = (props: GameBoardProps) => { diff --git a/web/src/pages/game/types/DieViewProps.ts b/web/src/pages/game/types/DieViewProps.ts new file mode 100644 index 0000000..05f049a --- /dev/null +++ b/web/src/pages/game/types/DieViewProps.ts @@ -0,0 +1,9 @@ +import { PieceId } from "interface"; + +export interface DieViewProps { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + isSpecial: boolean; + rotation: number; +} -- 2.40.1 From 0a0ca65e3b05a866e3add6da5a40aec020db4468 Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Wed, 4 Dec 2024 22:38:28 +0100 Subject: [PATCH 5/9] Introduce clean and commit action buttons --- web/public/clean-icon.png | Bin 0 -> 9113 bytes web/public/commit-icon.png | Bin 0 -> 16323 bytes web/src/pages/game/GamePage.tsx | 42 ++++++++++++++++---- web/src/pages/game/components/DiceSet.scss | 8 +++- web/src/pages/game/components/DiceSet.tsx | 13 +++++- web/src/pages/game/components/GameBoard.tsx | 22 +++------- web/src/pages/game/types/DieViewProps.ts | 2 +- 7 files changed, 60 insertions(+), 27 deletions(-) create mode 100644 web/public/clean-icon.png create mode 100644 web/public/commit-icon.png diff --git a/web/public/clean-icon.png b/web/public/clean-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c06ba906d80c3f150600d10e774ef0842bb30a62 GIT binary patch literal 9113 zcmd5?hg%cR(+>oYu2Q7;1f+MQC?HJ^1T?^xULpcgM0$}LiGV0ANa#(16zNEj8bClg zB2A?Pk=~nhc#q#-@jl5txt+|;d}e26cW?IIJkozaL&-`Bfk0>gZ4CqjLbQK%k&}Ux zzVX>;@Q3V)?gI_T<<%#rxgZgwP`GQGc|jml^j8-VBrSs(0^x=L8uyL-e{D<$?i()z zFYO#2TM-WDLlVNK{(dNA*8EOhSU9T7CZ1yYqG;qDz`v}M`K(mEYJJj(U%JMpQ!;Ph z9ZM;Etp96O+QP`fvgrp8%_2!29S*1-vxWw>riO;b*V(h3bsPD!Gpj-S?CR{`!PA;? zYdyY*;Fclfp`~?4mD2!K3=H`H|6SE=qtnLlj*z`0(}IMDS>e}Ilk*c`f*0w1KTr=6 z37Y}yq!$d#c118C`UktM7uWrn-yTfEPCvo+QrX$K)T^eWNuvH*GzMnRwRmX4yx7^D zibY^>>r<&}DSljDA_Xwu;P5p=M-$Z~g}3Q{0A|(EycanW95g+mgE8XTcz-9oIm6Co zw^ba#IIp8vUk4m|py60pQSD%K z;zVj;KusmPe5HQq#D9IF0OCX+_it$6ZQKgh5`**hxiM~0?wHe9{|*KS3PB9U75Z&z z*DK_yM; zwKWI_bb6k(yEz`v?lt4x`@{jnfq58{RD+Vr(C$^|7f%@$g+-fTz=}M7p2RGTw|NMF zDV_k%lYKXoZ<&FOksWs}WJ~-1RP{+y?s4F4|0jla;C6pw7%A2UgIld3Vw0lW3lwby z<$QB?n%KZ||Bpu>-kAIE%l}wtuKfd5`TbA43FJRVhD8hVp$#~INOFULOm-q*-!UcL z4hb8#V>nPe!%Y}?*B%5NcBa?H7&Tv$YEn{}0udWEcd|qrOZ`uJ7vvPw8Y4d{R-_Ha z1fvs?bB5B}DkKq4_YZBLt~D&MgT+ti0T_&C{VmH(YIzq>f;sUJm-r#2 z4NZHGOi>yYb5dnPQHh3+mV+*cK(>74j~~sB#Z~*`5!&YsDxfL$$8egz-*6q1Z-*9f zEg6}}lr5;%ZxRfdr_bYV$r(>=uEd3rGAle>4uwOM2(QYwI4j`7KSRWyGB3t~d^(a1 z50)ySk<LpTiYW2IaBmS8kFLfvUA79I4vASJ>RJ|9(rSj3Vv4S4a@sd5mf1}43b3;? z!$P^gPrbb%y}anWMC$~9yiqan_T3~JC~aksvn^lPJmsP$j_GVkCIiqf85*rd?gVXojIhA9uUH5NeItN%&MiBi38AvLbA+Z zuOsJ}-4{2u9lB5OKxtB$BDqPkZ{1*gN_4&4qF%V zIBw=0Sxc(NHRGUlQa3W%N+*U~3#p3`*RQ$0fF>oLn$QyUs;T^NoQa4-E|qt?h`fc~ z5VR%}zk|0)*q}=qyX$!Qa3uG5rgZf=7-sWZ_yalhCO>V&uL&bvI$u5%1zyIM(_Pm6 zS$Ob_4Y2`KoLHP_E$lFnOPCAJk@Ft=AZ$~sS9KwzSlcb+3+gm!OJwV=EF^) zqAJ0W@TB1t=}0qJV=fvx82ZKUn>o#V_K<*ZG9{XPjwi6$>ms zm-NWcStY|?L7B@aXySqjbc=H6Umcy_YR=UZo$2VB`TWT#oqy^Z$&=I=XXLGwGLb`S ziHwYfL@8zs0m@uvvX~Zd~!}@2U+u{bL+G;233Bc+Q-zhFEvt=NPZSswJ>oUytMNPyWq- z+0Bt%`5art5UowMtpgfNbVmU5o=>HFX(_so4AX-ds)l=PzN% zix0Ec0F2NNE&qEW5vtiwytEM^?dl$8H-QxK8bO$#N`%>~!VESbrM%1J4qVX0GEs@s zE-V-v$`IRWN@yx;RdNxJT9e^*BW`gmUM zHEdyGLzCkqsPD0#i5A^rAZku761HGSb#3d0x-CYlgtpsV+Cx%M}2{;+<%%^!c2rKYO?i1po9M9 zNsqr)es*C(LYVP z9Z4Nko=-KvfVsjUn;Jf=Aig9&yK8JOQ6UPN8>_SiAlx`KO({y!xJ|YE$x3!hqn0

!iBoxHS#BHtQ)Me=SZvZC8V2&_S+r400#mz_7j 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 literal 0 HcmV?d00001 diff --git a/web/public/commit-icon.png b/web/public/commit-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d254d8d86fe123985ce1e9f5a02a34a84fbc27df GIT binary patch literal 16323 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 V{Loa7JqUj`bye-F1y?K|{6GGnGu;3H literal 0 HcmV?d00001 diff --git a/web/src/pages/game/GamePage.tsx b/web/src/pages/game/GamePage.tsx index 4e15ed6..4ad8707 100644 --- a/web/src/pages/game/GamePage.tsx +++ b/web/src/pages/game/GamePage.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import GameBoard from "./components/GameBoard"; import DiceSet from "./components/DiceSet"; import "./GamePage.scss"; -import { PieceId } from "interface"; +import { buildBoard, PieceId } from "interface"; import { DieViewProps } from "./types/DieViewProps"; const GamePage = () => { @@ -11,18 +11,18 @@ const GamePage = () => { return Object.values(PieceId)[randomId]; }; - const [dice, setDice] = useState( + const getRandomDiceSet: () => DieViewProps[] = () => [1, 2, 3, 4].map(() => { - const dieViewProps: DieViewProps = { + return { pieceId: getRandomPieceId(), isSelected: false, isDisabled: false, isSpecial: false, rotation: 0, }; - return dieViewProps; - }), - ); + }); + + const [dice, setDice] = useState(getRandomDiceSet()); const [specialDice, setSpecialDice] = useState( [ PieceId.P03, @@ -71,14 +71,40 @@ const GamePage = () => { return dice.concat(specialDice).find((die) => matcher(die)); }; + const [storedBoard, setStoredBoard] = useState(buildBoard()); + const [board, setBoard] = useState(buildBoard()); + const [id, setId] = useState(1); + const refreshBoardRender = () => { + setBoard(board); + setId(id + 1); + }; + const resetBoard = () => { + setBoard(storedBoard); + modifyDieState( + () => true, + () => { + return { isSelected: false, isDisabled: false }; + }, + ); + setId(id + 1); + }; + const commitBoard = () => { + if (dice.some((die) => !die.isDisabled)) return; + setStoredBoard(board); + setDice(getRandomDiceSet()); + setSpecialDieUsedInRound(false); + }; + return (

Game Page Title

-
+
{ specialDice={specialDice} modifyDieState={modifyDieState} specialDieUsedInRound={specialDieUsedInRound} + resetBoard={resetBoard} + commitBoard={commitBoard} />
diff --git a/web/src/pages/game/components/DiceSet.scss b/web/src/pages/game/components/DiceSet.scss index da09ab7..114a733 100644 --- a/web/src/pages/game/components/DiceSet.scss +++ b/web/src/pages/game/components/DiceSet.scss @@ -9,13 +9,17 @@ box-shadow: 0 0 10px green; border-radius: 20%; padding: 5px; - width: 80px; - height: 80px; + width: 70px; + height: 70px; margin-right: 10px; &.icon-inverted { transform: scaleX(-1); } + + &:last-child { + margin-right: 0; + } } } diff --git a/web/src/pages/game/components/DiceSet.tsx b/web/src/pages/game/components/DiceSet.tsx index 518e2a1..bf0b5a2 100644 --- a/web/src/pages/game/components/DiceSet.tsx +++ b/web/src/pages/game/components/DiceSet.tsx @@ -11,9 +11,18 @@ export interface DiceSetProps { newStateComputer: (die: DieViewProps) => Partial, ) => void; specialDieUsedInRound: boolean; + resetBoard: () => void; + commitBoard: () => void; } const DiceSet = (props: DiceSetProps) => { - const { dice, specialDice, modifyDieState, specialDieUsedInRound } = props; + const { + dice, + specialDice, + modifyDieState, + specialDieUsedInRound, + resetBoard, + commitBoard, + } = props; const handleDieClick = (clickedDie: DieViewProps) => { if (clickedDie.isDisabled) return; const isSpecialDie = clickedDie.isSpecial; @@ -51,6 +60,8 @@ const DiceSet = (props: DiceSetProps) => { className="icon-inverted" onClick={() => handleRotateButton(90)} > + +
{dice.map((die) => ( diff --git a/web/src/pages/game/components/GameBoard.tsx b/web/src/pages/game/components/GameBoard.tsx index 8ea33b9..2ddfb81 100644 --- a/web/src/pages/game/components/GameBoard.tsx +++ b/web/src/pages/game/components/GameBoard.tsx @@ -1,6 +1,5 @@ -import { buildBoard } from "interface"; +import { Cell } from "interface"; import "./GameBoard.scss"; -import { useState } from "react"; import BoardCell from "./BoardCell"; import { DieViewProps } from "../types/DieViewProps"; @@ -13,26 +12,17 @@ export interface GameBoardProps { matcher: (die: DieViewProps) => boolean, ) => DieViewProps | undefined; setSpecialDieUsedInRound: React.Dispatch>; + refreshBoardRender: () => void; + board: Cell[][]; } const GameBoard = (props: GameBoardProps) => { - const [board, setBoard] = useState(buildBoard()); - const [id, setId] = useState(1); - const refreshBoardRender = () => { - setBoard(board); - setId(id + 1); - }; + const { board } = props; return ( -
+
{board.flatMap((row) => - row.map((cell) => ( - - )), + row.map((cell) => ), )}
); diff --git a/web/src/pages/game/types/DieViewProps.ts b/web/src/pages/game/types/DieViewProps.ts index 05f049a..71914ed 100644 --- a/web/src/pages/game/types/DieViewProps.ts +++ b/web/src/pages/game/types/DieViewProps.ts @@ -4,6 +4,6 @@ export interface DieViewProps { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; - isSpecial: boolean; + readonly isSpecial: boolean; rotation: number; } -- 2.40.1 From 7c4a6f560320f3b08d7ea2ecf0bff5c68bcb4dd9 Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Thu, 5 Dec 2024 10:46:38 +0100 Subject: [PATCH 6/9] Fix commit and reset actions crossing pointers by creating an action stack (will be used to send data to backend) --- interface/types/Cell.ts | 4 ++ web/package-lock.json | 8 ++-- web/package.json | 2 +- web/src/pages/game/GamePage.tsx | 13 ++++-- web/src/pages/game/components/BoardCell.tsx | 9 ++--- web/src/pages/game/components/GameBoard.tsx | 24 +++++++++-- web/src/pages/game/types/PlacePieceAction.ts | 32 +++++++++++++++ .../pages/game/types/PlacePieceActionStack.ts | 40 +++++++++++++++++++ 8 files changed, 116 insertions(+), 16 deletions(-) create mode 100644 web/src/pages/game/types/PlacePieceAction.ts create mode 100644 web/src/pages/game/types/PlacePieceActionStack.ts diff --git a/interface/types/Cell.ts b/interface/types/Cell.ts index c71bb2b..cd12587 100644 --- a/interface/types/Cell.ts +++ b/interface/types/Cell.ts @@ -105,4 +105,8 @@ export class Cell { throw Error("No adjacent exit or piece available to connect to"); } } + + public removePiece() { + this.placedPiece = undefined; + } } diff --git a/web/package-lock.json b/web/package-lock.json index 936fa4a..6028a80 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,7 +13,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", - "@types/node": "^16.18.119", + "@types/node": "^17.0.29", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "interface": "file:../interface", @@ -4202,9 +4202,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "16.18.119", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.119.tgz", - "integrity": "sha512-ia7V9a2FnhUFfetng4/sRPBMTwHZUkPFY736rb1cg9AgG7MZdR97q7/nLR9om+sq5f1la9C857E0l/nrI0RiFQ==", + "version": "17.0.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.29.tgz", + "integrity": "sha512-tx5jMmMFwx7wBwq/V7OohKDVb/JwJU5qCVkeLMh1//xycAJ/ESuw9aJ9SEtlCZDYi2pBfe4JkisSoAtbOsBNAA==", "license": "MIT" }, "node_modules/@types/node-forge": { diff --git a/web/package.json b/web/package.json index f240e25..b5e64b2 100644 --- a/web/package.json +++ b/web/package.json @@ -8,7 +8,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", - "@types/node": "^16.18.119", + "@types/node": "^17.0.29", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "interface": "file:../interface", diff --git a/web/src/pages/game/GamePage.tsx b/web/src/pages/game/GamePage.tsx index 4ad8707..529308b 100644 --- a/web/src/pages/game/GamePage.tsx +++ b/web/src/pages/game/GamePage.tsx @@ -4,6 +4,7 @@ import DiceSet from "./components/DiceSet"; import "./GamePage.scss"; import { buildBoard, PieceId } from "interface"; import { DieViewProps } from "./types/DieViewProps"; +import { PlacePieceActionStack } from "./types/PlacePieceActionStack"; const GamePage = () => { const getRandomPieceId = () => { @@ -71,15 +72,19 @@ const GamePage = () => { return dice.concat(specialDice).find((die) => matcher(die)); }; - const [storedBoard, setStoredBoard] = useState(buildBoard()); const [board, setBoard] = useState(buildBoard()); const [id, setId] = useState(1); const refreshBoardRender = () => { setBoard(board); setId(id + 1); }; + + const [placePieceActionStack, setPlacePieceActionStack] = useState( + new PlacePieceActionStack(), + ); const resetBoard = () => { - setBoard(storedBoard); + placePieceActionStack.resetActions(board); + setBoard(board); modifyDieState( () => true, () => { @@ -90,7 +95,7 @@ const GamePage = () => { }; const commitBoard = () => { if (dice.some((die) => !die.isDisabled)) return; - setStoredBoard(board); + placePieceActionStack.commitActions(); setDice(getRandomDiceSet()); setSpecialDieUsedInRound(false); }; @@ -105,6 +110,8 @@ const GamePage = () => { setSpecialDieUsedInRound={setSpecialDieUsedInRound} refreshBoardRender={refreshBoardRender} board={board} + placePieceActionStack={placePieceActionStack} + setPlacePieceActionStack={setPlacePieceActionStack} />
boolean, ) => DieViewProps | undefined; setSpecialDieUsedInRound: React.Dispatch>; + placePieceActionHandler: (pieceId: PieceId, rotation: number) => void; } const BoardCell = (props: BoardCellProps) => { @@ -23,16 +24,14 @@ const BoardCell = (props: BoardCellProps) => { modifyDieState, fetchDie, setSpecialDieUsedInRound, + placePieceActionHandler, } = props; const [pieceRotationAngle, setPieceRotationAngle] = useState(0); const handleBoardCellClick = () => { const selectedDie = fetchDie((die) => die.isSelected); if (!selectedDie) return; try { - cell.placePiece( - selectedDie.pieceId, - selectedDie.rotation as 0 | 90 | 180 | 270, - ); + placePieceActionHandler(selectedDie.pieceId, selectedDie.rotation); } catch (error) { console.log(error); return; diff --git a/web/src/pages/game/components/GameBoard.tsx b/web/src/pages/game/components/GameBoard.tsx index 2ddfb81..94a94f2 100644 --- a/web/src/pages/game/components/GameBoard.tsx +++ b/web/src/pages/game/components/GameBoard.tsx @@ -2,6 +2,8 @@ import { Cell } from "interface"; import "./GameBoard.scss"; import BoardCell from "./BoardCell"; import { DieViewProps } from "../types/DieViewProps"; +import { PlacePieceActionStack } from "../types/PlacePieceActionStack"; +import { PlacePieceAction } from "../types/PlacePieceAction"; export interface GameBoardProps { modifyDieState: ( @@ -14,15 +16,31 @@ export interface GameBoardProps { setSpecialDieUsedInRound: React.Dispatch>; refreshBoardRender: () => void; board: Cell[][]; + placePieceActionStack: PlacePieceActionStack; + setPlacePieceActionStack: React.Dispatch< + React.SetStateAction + >; } const GameBoard = (props: GameBoardProps) => { - const { board } = props; + const { board, placePieceActionStack, setPlacePieceActionStack } = props; return (
- {board.flatMap((row) => - row.map((cell) => ), + {board.flatMap((row, rowIndex) => + row.map((cell, colIndex) => ( + { + placePieceActionStack.executeAction( + new PlacePieceAction(pieceId, rotation, rowIndex, colIndex), + board, + ); + setPlacePieceActionStack(placePieceActionStack); + }} + /> + )), )}
); diff --git a/web/src/pages/game/types/PlacePieceAction.ts b/web/src/pages/game/types/PlacePieceAction.ts new file mode 100644 index 0000000..6fe46d3 --- /dev/null +++ b/web/src/pages/game/types/PlacePieceAction.ts @@ -0,0 +1,32 @@ +import { Cell, PieceId } from "interface"; + +export class PlacePieceAction { + pieceId: PieceId; + rotation: number; + cell: { + row: number; + col: number; + }; + + constructor(pieceId: PieceId, rotation: number, row: number, col: number) { + this.pieceId = pieceId; + this.rotation = rotation; + this.cell = { + row: row, + col: col, + }; + } + + do(board: Cell[][]) { + const cell = board[this.cell.row][this.cell.col]; + cell.placePiece(this.pieceId, this.rotation as 0 | 90 | 180 | 270); + } + + undo(board: Cell[][]) { + const cell = board[this.cell.row][this.cell.col]; + if (!cell.placedPiece || cell.placedPiece.id !== this.pieceId) { + throw Error("Un-doing action error"); + } + cell.removePiece(); + } +} diff --git a/web/src/pages/game/types/PlacePieceActionStack.ts b/web/src/pages/game/types/PlacePieceActionStack.ts new file mode 100644 index 0000000..4a6841f --- /dev/null +++ b/web/src/pages/game/types/PlacePieceActionStack.ts @@ -0,0 +1,40 @@ +import { Cell } from "interface"; +import { PlacePieceAction } from "./PlacePieceAction"; + +export class PlacePieceActionStack { + readonly committedActions: PlacePieceAction[] = []; + inProgressActions: PlacePieceAction[] = []; + + executeAction(action: PlacePieceAction, board: Cell[][]) { + action.do(board); + this.inProgressActions.push(action); + } + + undoLastAction(board: Cell[][]) { + const lastAction = this.inProgressActions.pop(); + if (!lastAction) { + throw Error("No in progress action to undo"); + } + lastAction.undo(board); + } + + resetActions(board: Cell[][]) { + try { + while (true) { + this.undoLastAction(board); + } + } catch (error) { + if ( + !(error instanceof Error) || + error.message !== "No in progress action to undo" + ) { + throw error; + } + } + } + + commitActions() { + this.committedActions.push(...this.inProgressActions); + this.inProgressActions = []; + } +} -- 2.40.1 From 0b5684f2b9f9fea2185f8c44c954762590e9840d Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Thu, 5 Dec 2024 10:46:38 +0100 Subject: [PATCH 7/9] Fix commit and reset actions crossing pointers by creating an action stack (will be used to send data to backend) --- web/src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 7b878b6..30a0cc5 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -27,7 +27,6 @@ const App = (props: AppProps) => { }, [renderedPage]); setupNextPageTransition(); - return (
{renderedPage === "LANDING" && } -- 2.40.1 From 09bd13f772c99d69b568128126c73d785a9e683b Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Sun, 9 Feb 2025 15:24:43 +0100 Subject: [PATCH 8/9] Create page transition from lobby to in-game --- interface/index.ts | 1 + interface/server-events/StartRoundEvent.ts | 14 ++++++++++++++ web/src/App.tsx | 9 ++++++++- web/src/pages/lobby/LobbyPage.tsx | 5 +++-- 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 interface/server-events/StartRoundEvent.ts diff --git a/interface/index.ts b/interface/index.ts index 17a354e..cacd699 100644 --- a/interface/index.ts +++ b/interface/index.ts @@ -14,6 +14,7 @@ export * from "./types/PlacedPiece"; export * from "./BoardBuilder"; export * from "./server-events/ServerError"; export * from "./server-events/ServerEvent"; +export * from "./server-events/StartRoundEvent"; export * from "./server-events/UpdateLobbyEvent"; export * from "./client-events/ClientEvent"; export * from "./client-events/CreateLobbyEvent"; diff --git a/interface/server-events/StartRoundEvent.ts b/interface/server-events/StartRoundEvent.ts new file mode 100644 index 0000000..75179fc --- /dev/null +++ b/interface/server-events/StartRoundEvent.ts @@ -0,0 +1,14 @@ +import { Socket } from "socket.io-client"; +import { ServerEvent } from "./ServerEvent"; +import { PieceId } from "../constants/Pieces"; + +export type StartRoundEvent = { + pieceIds: PieceId[]; +}; + +export function attachHandlerToStartRoundEvent( + socket: Socket, + handler: (event: StartRoundEvent) => void, +): void { + socket.once(ServerEvent.START_ROUND, handler); +} diff --git a/web/src/App.tsx b/web/src/App.tsx index 30a0cc5..12ad8e7 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -3,7 +3,10 @@ import { Socket } from "socket.io-client"; import LandingPage from "./pages/landing/LandingPage"; import GamePage from "./pages/game/GamePage"; import LobbyPage from "./pages/lobby/LobbyPage"; -import { attachHandlerToUpdateLobbyEvent } from "interface"; +import { + attachHandlerToStartRoundEvent, + attachHandlerToUpdateLobbyEvent, +} from "interface"; export interface AppProps { socket: Socket; @@ -23,6 +26,10 @@ const App = (props: AppProps) => { setGameCode(event.gameCode); setRenderedPage("LOBBY"); }); + } else if (renderedPage === "LOBBY") { + attachHandlerToStartRoundEvent(socket, (event) => { + setRenderedPage("GAME"); + }); } }, [renderedPage]); diff --git a/web/src/pages/lobby/LobbyPage.tsx b/web/src/pages/lobby/LobbyPage.tsx index e92f168..a336d29 100644 --- a/web/src/pages/lobby/LobbyPage.tsx +++ b/web/src/pages/lobby/LobbyPage.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import "./LobbyPage.scss"; import { Socket } from "socket.io-client"; -import { attachHandlerToUpdateLobbyEvent } from "interface"; +import { attachHandlerToUpdateLobbyEvent, ClientEvent } from "interface"; export interface LobbyPageProps { initialPlayerNames: string[]; @@ -16,6 +16,7 @@ const LobbyPage = (props: LobbyPageProps) => { attachHandlerToUpdateLobbyEvent(socket, (event) => { setPlayerNames(event.playerNames); }); + const startGame = () => socket.emit(ClientEvent.START_GAME); return (
@@ -27,7 +28,7 @@ const LobbyPage = (props: LobbyPageProps) => {
{name}
))}
- +
Game code: {gameCode}
-- 2.40.1 From 10949f30df104e8afcec373be71f26a0794f5adc Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Sun, 9 Feb 2025 15:45:07 +0100 Subject: [PATCH 9/9] Use server response to consume dice as defined by the backend --- interface/server-events/StartRoundEvent.ts | 12 +++++++++-- interface/server-events/UpdateLobbyEvent.ts | 12 +++++++++-- web/src/App.tsx | 9 ++++++-- web/src/pages/game/GamePage.tsx | 23 ++++++++++++--------- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/interface/server-events/StartRoundEvent.ts b/interface/server-events/StartRoundEvent.ts index 75179fc..7b870ec 100644 --- a/interface/server-events/StartRoundEvent.ts +++ b/interface/server-events/StartRoundEvent.ts @@ -1,4 +1,5 @@ -import { Socket } from "socket.io-client"; +import { Socket as ServerSocket } from "socket.io"; +import { Socket as ClientSocket } from "socket.io-client"; import { ServerEvent } from "./ServerEvent"; import { PieceId } from "../constants/Pieces"; @@ -6,8 +7,15 @@ export type StartRoundEvent = { pieceIds: PieceId[]; }; +export const emitStartRoundEvent = ( + socket: ServerSocket, + payload: StartRoundEvent, +) => { + socket.emit(ServerEvent.START_ROUND, payload); +}; + export function attachHandlerToStartRoundEvent( - socket: Socket, + socket: ClientSocket, handler: (event: StartRoundEvent) => void, ): void { socket.once(ServerEvent.START_ROUND, handler); diff --git a/interface/server-events/UpdateLobbyEvent.ts b/interface/server-events/UpdateLobbyEvent.ts index 2f0881d..9cbbb79 100644 --- a/interface/server-events/UpdateLobbyEvent.ts +++ b/interface/server-events/UpdateLobbyEvent.ts @@ -1,4 +1,5 @@ -import { Socket } from "socket.io-client"; +import { Socket as ServerSocket } from "socket.io"; +import { Socket as ClientSocket } from "socket.io-client"; import { ServerEvent } from "./ServerEvent"; export type UpdateLobbyEvent = { @@ -6,8 +7,15 @@ export type UpdateLobbyEvent = { gameCode: string; }; +export const emitUpdateLobbyEvent = ( + socket: ServerSocket, + payload: UpdateLobbyEvent, +) => { + socket.emit(ServerEvent.LOBBY_UPDATE, payload); +}; + export function attachHandlerToUpdateLobbyEvent( - socket: Socket, + socket: ClientSocket, handler: (event: UpdateLobbyEvent) => void, ): void { socket.once(ServerEvent.LOBBY_UPDATE, handler); diff --git a/web/src/App.tsx b/web/src/App.tsx index 12ad8e7..93d4015 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -6,6 +6,7 @@ import LobbyPage from "./pages/lobby/LobbyPage"; import { attachHandlerToStartRoundEvent, attachHandlerToUpdateLobbyEvent, + PieceId, } from "interface"; export interface AppProps { @@ -16,8 +17,9 @@ const App = (props: AppProps) => { const { socket } = props; const [renderedPage, setRenderedPage] = useState("LANDING"); - const [initialPlayerNames, setInitialPlayerNames] = useState([""]); + const [initialPlayerNames, setInitialPlayerNames] = useState([] as string[]); const [gameCode, setGameCode] = useState(""); + const [initialPieceIds, setInitialPieceIds] = useState([] as PieceId[]); const setupNextPageTransition = useCallback(() => { if (renderedPage === "LANDING") { @@ -28,6 +30,7 @@ const App = (props: AppProps) => { }); } else if (renderedPage === "LOBBY") { attachHandlerToStartRoundEvent(socket, (event) => { + setInitialPieceIds(event.pieceIds); setRenderedPage("GAME"); }); } @@ -44,7 +47,9 @@ const App = (props: AppProps) => { gameCode={gameCode} /> )} - {renderedPage === "GAME" && } + {renderedPage === "GAME" && ( + + )}
); }; diff --git a/web/src/pages/game/GamePage.tsx b/web/src/pages/game/GamePage.tsx index 529308b..c66ee9e 100644 --- a/web/src/pages/game/GamePage.tsx +++ b/web/src/pages/game/GamePage.tsx @@ -6,16 +6,19 @@ import { buildBoard, PieceId } from "interface"; import { DieViewProps } from "./types/DieViewProps"; import { PlacePieceActionStack } from "./types/PlacePieceActionStack"; -const GamePage = () => { - const getRandomPieceId = () => { - const randomId = Math.floor(Math.random() * Object.keys(PieceId).length); - return Object.values(PieceId)[randomId]; - }; +export interface GamePageProps { + initialPieceIds: PieceId[]; +} - const getRandomDiceSet: () => DieViewProps[] = () => - [1, 2, 3, 4].map(() => { +const GamePage = (props: GamePageProps) => { + const { initialPieceIds } = props; + + const [pieceIds] = useState(initialPieceIds); + + const getDiceSet: () => DieViewProps[] = () => + pieceIds.map((pieceId) => { return { - pieceId: getRandomPieceId(), + pieceId: pieceId, isSelected: false, isDisabled: false, isSpecial: false, @@ -23,7 +26,7 @@ const GamePage = () => { }; }); - const [dice, setDice] = useState(getRandomDiceSet()); + const [dice, setDice] = useState(getDiceSet()); const [specialDice, setSpecialDice] = useState( [ PieceId.P03, @@ -96,7 +99,7 @@ const GamePage = () => { const commitBoard = () => { if (dice.some((die) => !die.isDisabled)) return; placePieceActionStack.commitActions(); - setDice(getRandomDiceSet()); + setDice(getDiceSet()); setSpecialDieUsedInRound(false); }; -- 2.40.1