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.

341 lines
11 KiB

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