A React/Express online multiplayer Tombola client/server combo.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

373 lines
12 KiB

  1. import { Socket } from "socket.io";
  2. import randomstring from "randomstring";
  3. import { GameStartError, MAX_PLAYERS, PracticalTombolaAction, Room, RoomJoinError, SocketID, SocketWrapper, StrippedPlayer, TombolaAction } from "../../common/types";
  4. import cartelle from "../cartelle";
  5. import { ServerEventNames } from "../../common/events";
  6. const rooms = new Map<string, Room>();
  7. const flatbellone = [...Array(90).keys()].map(n => n + 1);
  8. export function createRoom(socket: Socket, id: SocketID, username: string) {
  9. // console.log(username);
  10. const key = randomstring.generate({
  11. length: 6,
  12. capitalization: "uppercase",
  13. charset: "hex",
  14. readable: true
  15. });
  16. socket.emit("createRoom", {
  17. key
  18. });
  19. rooms.set(key, {
  20. tabellone: [],
  21. players: [
  22. {
  23. socketData: {
  24. id,
  25. socket
  26. },
  27. username,
  28. hasTabellone: false,
  29. cartelle: [],
  30. choseAllCartelle: false,
  31. ready: true,
  32. }
  33. ],
  34. gameStarted: false,
  35. id: key,
  36. nextProgress: TombolaAction.AMBO,
  37. winners: {
  38. 2: null,
  39. 3: null,
  40. 4: null,
  41. 5: null,
  42. 15: null,
  43. }
  44. })
  45. socket.on("disconnect", () => {
  46. const room = rooms.get(key);
  47. rooms.delete(key);
  48. room?.players.forEach(player => {
  49. player.socketData.socket.emit("hostClosed");
  50. player.socketData.socket.disconnect();
  51. });
  52. });
  53. }
  54. export function joinRoom(socket: Socket, id: SocketID, username: string, roomID: string) {
  55. const room = rooms.get(roomID);
  56. if (!room) {
  57. socket.emit("joinRoomError", RoomJoinError.NoSuchRoom);
  58. return;
  59. }
  60. if (room.gameStarted) {
  61. socket.emit("joinRoomError", RoomJoinError.GameHasStarted);
  62. return;
  63. }
  64. room.players.push({
  65. socketData: {
  66. id,
  67. socket
  68. },
  69. username,
  70. hasTabellone: false,
  71. cartelle: [],
  72. choseAllCartelle: false,
  73. ready: false,
  74. });
  75. room.players.forEach(player => {
  76. player.socketData.socket.emit("playersUpdate", room.players.map(player => {
  77. return {
  78. id: player.socketData.id,
  79. name: player.username
  80. }
  81. }));
  82. });
  83. socket.emit("joinRoom", "OK");
  84. socket.on("disconnect", () => {
  85. room.players = room.players.filter(member => {
  86. member.socketData.id !== id;
  87. });
  88. socket.emit("playersUpdate", room.players.map(player => {
  89. return {
  90. id: player.socketData.id,
  91. name: player.username
  92. }
  93. }));
  94. });
  95. }
  96. export function updatePlayers(socket: Socket, id: SocketID, roomID: string) {
  97. if (!rooms.has(roomID))
  98. return socket.emit("playersUpdate", []);
  99. const room = rooms.get(roomID)!;
  100. socket.emit("playersUpdate", room.players.map((player): StrippedPlayer => {
  101. return {
  102. id: player.socketData.id,
  103. username: player.username,
  104. ready: player.ready
  105. }
  106. }));
  107. }
  108. export function extractNumber(socket: Socket, id: SocketID) {
  109. const roomID = roomIDForID(id);
  110. const room = rooms.get(roomID ?? "");
  111. if (!roomID || !room || room.players.find(p => p.hasTabellone)?.socketData.id != id)
  112. return;
  113. const available = flatbellone.filter(n => !room.tabellone.includes(n));
  114. const extracted = available[Math.floor(Math.random() * available.length)];
  115. if (extracted) {
  116. room.tabellone.push(extracted);
  117. const progressing = room.players.map(player => {
  118. const checked = player.hasTabellone ? checkTabellone(room) : checkCartelle(room, player.cartelle.map(c => cartelle[c]));
  119. return {
  120. checked,
  121. player
  122. }
  123. }).filter(({ checked }) => {
  124. // console.log(checked, TombolaAction[checked[1]], TombolaAction[room.nextProgress])
  125. return checked[0] && checked[1] === room.nextProgress;
  126. }).sort((dataA, dataB) => {
  127. return dataA.checked[1] - dataB.checked[1];
  128. });
  129. if (progressing.length) {
  130. room.nextProgress = nextOne(progressing[0].checked[1]);
  131. room.winners[previousOne(room.nextProgress) as PracticalTombolaAction] = progressing.map(({ player }) => {
  132. return {
  133. username: player.username,
  134. id: player.socketData.id,
  135. ready: player.ready
  136. }
  137. });
  138. }
  139. return everySocketData(id).forEach(data => {
  140. data.socket.emit("extractedNumber", extracted, room.tabellone);
  141. // console.log(progressing);
  142. if (!progressing.length) return;
  143. data.socket.emit("progress",
  144. previousOne(room.nextProgress),
  145. progressing.map(p => p.player.socketData.id).includes(data.id),
  146. ...progressing.map(({ player }) => {
  147. return player.socketData.id == data.id ? "" : player.username;
  148. })
  149. );
  150. if (room.nextProgress == TombolaAction.DONE) {
  151. data.socket.emit("endGame", room.winners);
  152. }
  153. });
  154. }
  155. }
  156. // Skip over the jumps
  157. function nextOne(action: TombolaAction): Exclude<TombolaAction, TombolaAction.NONE> {
  158. switch(action) {
  159. case TombolaAction.NONE: return TombolaAction.AMBO;
  160. case TombolaAction.CINQUINA: return TombolaAction.TOMBOLA;
  161. default: return action + 1;
  162. }
  163. }
  164. // opposite of nextOne above
  165. function previousOne(action: TombolaAction): Exclude<TombolaAction, TombolaAction.DONE> {
  166. switch(action) {
  167. case TombolaAction.AMBO: return TombolaAction.NONE;
  168. case TombolaAction.TOMBOLA: return TombolaAction.CINQUINA;
  169. default: return action - 1;
  170. }
  171. }
  172. export function chooseCartella(socket: Socket, id: SocketID, cartella: number) {
  173. const roomID = roomIDForID(id) ?? "";
  174. const room = rooms.get(roomID);
  175. if (!room)
  176. return;
  177. const i = room.players.findIndex(({ socketData: socket }) => socket.id === id);
  178. room.players[i].cartelle.push(cartella);
  179. allButOne(id).forEach(socket => socket.emit("chosenCartella", cartella));
  180. }
  181. export function unchooseCartella(socket: Socket, id: SocketID, cartella: number) {
  182. const roomID = roomIDForID(id) ?? "";
  183. const room = rooms.get(roomID);
  184. if (!room)
  185. return;
  186. const i = room.players.findIndex(({ socketData: socket }) => socket.id === id);
  187. room.players[i].cartelle = room.players[i].cartelle
  188. .filter(c => c !== cartella);
  189. allButOne(id).forEach(socket => socket.emit("unchosenCartella", cartella));
  190. }
  191. export function choseAllCartelle(socket: Socket, id: SocketID) {
  192. const roomID = roomIDForID(id) ?? "";
  193. const room = rooms.get(roomID);
  194. if (!room)
  195. return;
  196. room.players[room.players.findIndex(v => v.socketData.id === id)].choseAllCartelle = true;
  197. if (room.players.map(player => player.choseAllCartelle || player.hasTabellone).every(p => p))
  198. everySocket(id).forEach(socket => {
  199. socket.emit("everyoneChose");
  200. })
  201. }
  202. export function startGame(socket: Socket, id: SocketID) {
  203. const roomID = roomIDForID(id) ?? "";
  204. const room = rooms.get(roomID);
  205. if (!room)
  206. return;
  207. if (room.players.length <= 1) {
  208. socket.emit("startGameError", GameStartError.TooFewPlayers);
  209. return;
  210. } else if (room.players.length >= MAX_PLAYERS) {
  211. socket.emit("startGameError", GameStartError.TooManyPlayers);
  212. return;
  213. } else if (!room.players.every(player => player.ready)) {
  214. socket.emit("startGameError", GameStartError.NotReady);
  215. return;
  216. }
  217. room.tabellone = [];
  218. room.gameStarted = true;
  219. room.players.forEach(player => player.cartelle = []);
  220. const sockets = socketObjects(roomID);
  221. if (!sockets.length)
  222. return;
  223. const tabelloneGiver = sockets[Math.floor(Math.random() * sockets.length)];
  224. const tgIndex = room.players.findIndex(v => v.socketData.id === tabelloneGiver.id);
  225. room.players.forEach((_, i) => {
  226. room.players[i].hasTabellone = i === tgIndex;
  227. });
  228. const others = allButOne(tabelloneGiver.id);
  229. tabelloneGiver.socket.emit("startingGame", true);
  230. others.forEach(socket => {
  231. socket.emit("startingGame", false);
  232. })
  233. }
  234. export function pleaseGiveMeCartelle(socket: Socket, id: SocketID) {
  235. // console.log("pgmc")
  236. const roomID = roomIDForID(id) ?? "";
  237. const room = rooms.get(roomID);
  238. if (!room) {
  239. socket.emit("gaveMeCartelle", [0]);
  240. return;
  241. }
  242. socket.emit("gaveMeCartelle", room.players.find(player => player.socketData.id == id)?.cartelle ?? [0]);
  243. }
  244. export function ready(socket: Socket, id: SocketID) {
  245. const roomID = roomIDForID(id) ?? "";
  246. const room = rooms.get(roomID);
  247. if (!room)
  248. return;
  249. const i = room.players.findIndex(p => p.socketData.id === id);
  250. room.players[i].ready = !room.players[i].ready;
  251. everySocket(id).forEach(socket => {
  252. socket.emit(ServerEventNames.PlayersUpdate, room.players.map((p): StrippedPlayer => {
  253. return {
  254. id: p.socketData.id,
  255. username: p.username,
  256. ready: p.ready
  257. }
  258. }))
  259. });
  260. }
  261. function roomIDForID(id: SocketID): string | undefined {
  262. return [...rooms.entries()].find(([k, v]) => {
  263. return v.players.map(s => s.socketData.id).includes(id);
  264. })?.[0];
  265. }
  266. function allButOne(id: SocketID): Socket[] {
  267. return rooms.get(roomIDForID(id) ?? "")?.players.filter(player => {
  268. return player.socketData.id !== id;
  269. }).map(player => player.socketData.socket) ?? [];
  270. }
  271. function everySocketData(id: SocketID): SocketWrapper[] {
  272. return rooms.get(roomIDForID(id) ?? "")?.players.map(player => player.socketData) ?? [];
  273. }
  274. function everySocket(id: SocketID): Socket[] {
  275. return everySocketData(id).map(s => s.socket);
  276. }
  277. function socketObjects(room?: string): SocketWrapper[] {
  278. return rooms.get(room ?? "")?.players.map(player => player.socketData) ?? [];
  279. }
  280. // function subdivideTabellone(tabellone: number[]): [
  281. // number[], number[], number[], number[], number[], number[]
  282. // ] {
  283. // return [[],[],[],[],[],[]];
  284. // }
  285. const matrix = [1, 2, 3, 4, 5, 11, 12, 13, 14, 15, 21, 22, 23, 24, 25];
  286. const subdividedTabellone = [
  287. matrix,
  288. matrix.map(n => n + 5),
  289. matrix.map(n => n + 30),
  290. matrix.map(n => n + 35),
  291. matrix.map(n => n + 60),
  292. matrix.map(n => n + 65),
  293. ] as const;
  294. function checkTabellone(room: Room): [boolean, TombolaAction] {
  295. for (let cartella of subdividedTabellone) {
  296. const lines = [
  297. cartella.filter((n, i) => i < 5 && room.tabellone.includes(n)),
  298. cartella.filter((n, i) => i >= 5 && i < 10 && room.tabellone.includes(n)),
  299. cartella.filter((n, i) => i >= 10 && i < 15 && room.tabellone.includes(n)),
  300. ]
  301. // console.log(lines, "lines")
  302. if (lines.flat().length === 15)
  303. return [true, TombolaAction.TOMBOLA];
  304. else for (let i of [5, 4, 3, 2].sort((b, a) => a - b).filter(a => a >= room.nextProgress)) {
  305. // // console.log("Checking", i);
  306. if (lines.some(line => line.length === i)) {
  307. return [true, i];
  308. }
  309. }
  310. }
  311. return [false, TombolaAction.NONE];
  312. }
  313. function checkCartelle(room: Room, cartelle: number[][]): [boolean, TombolaAction] {
  314. for (let cartella of cartelle) {
  315. const lines = [
  316. cartella.filter((n, i) => i < 9 && n && room.tabellone.includes(n)),
  317. cartella.filter((n, i) => i >= 9 && i < 18 && n && room.tabellone.includes(n)),
  318. cartella.filter((n, i) => i >= 18 && i < 27 && n && room.tabellone.includes(n)),
  319. ]
  320. // console.log(lines);
  321. if (lines.flat().length === 15) {
  322. return [true, TombolaAction.TOMBOLA];
  323. } else for (let i of [5, 4, 3, 2].sort((b, a) => a - b).filter(a => a >= room.nextProgress)) {
  324. // console.log(lines.some(line => line.length === i));
  325. if (lines.some(line => line.length === i)) {
  326. return [true, i];
  327. }
  328. }
  329. }
  330. return [false, TombolaAction.NONE];
  331. }