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.

367 lines
12 KiB

  1. import { Socket } from "socket.io";
  2. import randomstring from "randomstring";
  3. import { 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");
  209. return;
  210. }
  211. room.tabellone = [];
  212. room.gameStarted = true;
  213. room.players.forEach(player => player.cartelle = []);
  214. const sockets = socketObjects(roomID);
  215. if (!sockets.length)
  216. return;
  217. const tabelloneGiver = sockets[Math.floor(Math.random() * sockets.length)];
  218. const tgIndex = room.players.findIndex(v => v.socketData.id === tabelloneGiver.id);
  219. room.players.forEach((_, i) => {
  220. room.players[i].hasTabellone = i === tgIndex;
  221. });
  222. const others = allButOne(tabelloneGiver.id);
  223. tabelloneGiver.socket.emit("startingGame", true);
  224. others.forEach(socket => {
  225. socket.emit("startingGame", false);
  226. })
  227. }
  228. export function pleaseGiveMeCartelle(socket: Socket, id: SocketID) {
  229. // console.log("pgmc")
  230. const roomID = roomIDForID(id) ?? "";
  231. const room = rooms.get(roomID);
  232. if (!room) {
  233. socket.emit("gaveMeCartelle", [0]);
  234. return;
  235. }
  236. socket.emit("gaveMeCartelle", room.players.find(player => player.socketData.id == id)?.cartelle ?? [0]);
  237. }
  238. export function ready(socket: Socket, id: SocketID) {
  239. const roomID = roomIDForID(id) ?? "";
  240. const room = rooms.get(roomID);
  241. if (!room)
  242. return;
  243. const i = room.players.findIndex(p => p.socketData.id === id);
  244. room.players[i].ready = !room.players[i].ready;
  245. everySocket(id).forEach(socket => {
  246. socket.emit(ServerEventNames.PlayersUpdate, room.players.map((p): StrippedPlayer => {
  247. return {
  248. id: p.socketData.id,
  249. username: p.username,
  250. ready: p.ready
  251. }
  252. }))
  253. });
  254. }
  255. function roomIDForID(id: SocketID): string | undefined {
  256. return [...rooms.entries()].find(([k, v]) => {
  257. return v.players.map(s => s.socketData.id).includes(id);
  258. })?.[0];
  259. }
  260. function allButOne(id: SocketID): Socket[] {
  261. return rooms.get(roomIDForID(id) ?? "")?.players.filter(player => {
  262. return player.socketData.id !== id;
  263. }).map(player => player.socketData.socket) ?? [];
  264. }
  265. function everySocketData(id: SocketID): SocketWrapper[] {
  266. return rooms.get(roomIDForID(id) ?? "")?.players.map(player => player.socketData) ?? [];
  267. }
  268. function everySocket(id: SocketID): Socket[] {
  269. return everySocketData(id).map(s => s.socket);
  270. }
  271. function socketObjects(room?: string): SocketWrapper[] {
  272. return rooms.get(room ?? "")?.players.map(player => player.socketData) ?? [];
  273. }
  274. // function subdivideTabellone(tabellone: number[]): [
  275. // number[], number[], number[], number[], number[], number[]
  276. // ] {
  277. // return [[],[],[],[],[],[]];
  278. // }
  279. const matrix = [1, 2, 3, 4, 5, 11, 12, 13, 14, 15, 21, 22, 23, 24, 25];
  280. const subdividedTabellone = [
  281. matrix,
  282. matrix.map(n => n + 5),
  283. matrix.map(n => n + 30),
  284. matrix.map(n => n + 35),
  285. matrix.map(n => n + 60),
  286. matrix.map(n => n + 65),
  287. ] as const;
  288. function checkTabellone(room: Room): [boolean, TombolaAction] {
  289. for (let cartella of subdividedTabellone) {
  290. const lines = [
  291. cartella.filter((n, i) => i < 5 && room.tabellone.includes(n)),
  292. cartella.filter((n, i) => i >= 5 && i < 10 && room.tabellone.includes(n)),
  293. cartella.filter((n, i) => i >= 10 && i < 15 && room.tabellone.includes(n)),
  294. ]
  295. // console.log(lines, "lines")
  296. if (lines.flat().length === 15)
  297. return [true, TombolaAction.TOMBOLA];
  298. else for (let i of [5, 4, 3, 2].sort((b, a) => a - b).filter(a => a >= room.nextProgress)) {
  299. // // console.log("Checking", i);
  300. if (lines.some(line => line.length === i)) {
  301. return [true, i];
  302. }
  303. }
  304. }
  305. return [false, TombolaAction.NONE];
  306. }
  307. function checkCartelle(room: Room, cartelle: number[][]): [boolean, TombolaAction] {
  308. for (let cartella of cartelle) {
  309. const lines = [
  310. cartella.filter((n, i) => i < 9 && n && room.tabellone.includes(n)),
  311. cartella.filter((n, i) => i >= 9 && i < 18 && n && room.tabellone.includes(n)),
  312. cartella.filter((n, i) => i >= 18 && i < 27 && n && room.tabellone.includes(n)),
  313. ]
  314. // console.log(lines);
  315. if (lines.flat().length === 15) {
  316. return [true, TombolaAction.TOMBOLA];
  317. } else for (let i of [5, 4, 3, 2].sort((b, a) => a - b).filter(a => a >= room.nextProgress)) {
  318. // console.log(lines.some(line => line.length === i));
  319. if (lines.some(line => line.length === i)) {
  320. return [true, i];
  321. }
  322. }
  323. }
  324. return [false, TombolaAction.NONE];
  325. }