Two teams try and fill in any horizontal, vertical, or diagonal line on a bingo board by playing maps on osu!
osu.bingo
osu
1/**
2 * Starts a bingo game!
3 */
4
5import q from '$lib/drizzle/queries';
6import { addGame } from './watch';
7import boards from '$lib/bingo-helpers/default_boards';
8import { logger } from '$lib/logger';
9import { sendToGame } from '$lib/emitter/server';
10import { clearTimer } from './auto_deletion';
11import type { Options } from '$lib/gamerules/options';
12
13export const startGame = async (game_id: string) => {
14 logger.info(`Starting game ${game_id}!`, { type: 'game_start' });
15 const now = Date.now();
16 clearTimer(game_id);
17
18 const game = await q.getGame(game_id);
19 if (!game) return;
20
21 // Merge options from template with game-specific options
22 const template: Options = JSON.parse(game.template.data);
23 const gameOptions: Options = JSON.parse(game.options);
24
25 const options = { ...template, ...gameOptions, setup: { ...template.setup, ...gameOptions.setup } }
26
27 // Grab board from default boards if necessary
28 const board =
29 typeof options.board == 'string' ? boards[options.board] : options.board;
30 options.board = board;
31
32 await q.updateGameOptions(game_id, options);
33
34 // ===========
35 // MAP PICKING
36 // ===========
37
38
39 // Group squares via block
40 const blocks: { block: Options.Block, squares: [number, number][] }[] = []
41 for (let i = 0; i < board.squares.length; i++) {
42 let block = board.blocks?.find(x => x.indices.includes(i))?.block;
43 if (!block) block = options.setup;
44
45 const blocksIdx = blocks.findIndex(x => x.block == block);
46 if (blocksIdx != -1) {
47 blocks[blocksIdx].squares.push(board.squares[i]);
48 } else {
49 blocks.push({ block, squares: [board.squares[i]] })
50 }
51 }
52
53 // Pick maps in each block
54 for (const { block, squares } of blocks) {
55 const totalChance = block.maps.reduce((a, b) => a + (b.chance ?? 1), 0);
56
57 // Calculate how many maps in each pool to fetch
58 const counts = [];
59 for (const mappool of block.maps) {
60 const chance = (mappool.chance ?? 1) / totalChance;
61 counts.push({
62 mappool,
63 maps: Math.floor(squares.length * chance),
64 });
65 }
66
67 // If the count doesn't add up to the number of squares, add the difference to the smallest pool
68 if (counts.map((x) => x.maps).reduce((a, b) => a + b) < squares.length) {
69 counts.sort((a, b) => a.maps - b.maps);
70 counts[0].maps += squares.length - counts.map((x) => x.maps).reduce((a, b) => a + b);
71 }
72
73 // Fetch maps in all the pools
74 const maps = [];
75 for (const pool of counts) {
76 if (pool.maps == 0) continue;
77
78 const picks = await q.getRandomMaps(
79 pool.mappool.mappool_id,
80 pool.maps,
81 // Pick block's settings, or fallback to default block's settings
82 block.stars?.min ?? options.setup.stars?.min,
83 block.stars?.max ?? options.setup.stars?.max,
84 block.length?.min ?? options.setup.length?.min,
85 block.length?.max ?? options.setup.length?.max,
86 pool.mappool.mode
87 );
88 maps.push(...picks);
89 }
90
91 // Shuffle maps
92 maps.sort(() => (Math.random() > 0.5 ? -1 : 1));
93 for (let i = 0; i < squares.length; i++) {
94 const square = squares[i];
95 await q.newSquare({
96 game_id,
97 map_id: maps[i].id,
98 x_pos: square[0],
99 y_pos: square[1]
100 });
101 }
102 }
103
104 // Create Events
105 for (const event of template.event) {
106 const date = new Date(now + event.seconds_after_start * 1000);
107 await q.setEvent(game_id, event.event, date);
108 }
109
110 // Update last settings and send game to clients
111 await q.setGameState(game.id, 1);
112 await q.setStartTime(game.id, new Date(now));
113 addGame(game_id);
114 sendToGame(game_id, {
115 type: 'state',
116 data: {
117 state: 1,
118 card: await q.getGame(game_id)
119 }
120 });
121};