summaryrefslogtreecommitdiff
path: root/server.js
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2024-10-13 12:48:48 +0200
committerTor Andersson <tor@ccxvii.net>2024-10-13 18:49:00 +0200
commit4d7bdc955a2e6dd2c222f985c7fbc9b4febbccc4 (patch)
treea344e7c011b07e83ab0abf57e1aa043a9a81dff2 /server.js
parent88d909a874499f9d3d18e76ff30c1155caa2e48e (diff)
downloadserver-4d7bdc955a2e6dd2c222f985c7fbc9b4febbccc4.tar.gz
Tournaments!
Diffstat (limited to 'server.js')
-rw-r--r--server.js780
1 files changed, 775 insertions, 5 deletions
diff --git a/server.js b/server.js
index 1e4a2bd..5caf238 100644
--- a/server.js
+++ b/server.js
@@ -95,6 +95,29 @@ function set_has(set, item) {
return false
}
+// see Object.groupBy
+function object_group_by(items, callback) {
+ let groups = {}
+ if (typeof callback === "function") {
+ for (let item of items) {
+ let key = callback(item)
+ if (key in groups)
+ groups[key].push(item)
+ else
+ groups[key] = [ item ]
+ }
+ } else {
+ for (let item of items) {
+ let key = item[callback]
+ if (key in groups)
+ groups[key].push(item)
+ else
+ groups[key] = [ item ]
+ }
+ }
+ return groups
+}
+
/*
* Notification mail setup.
*/
@@ -179,6 +202,11 @@ app.locals.ENABLE_FORUM = process.env.FORUM | 0
app.locals.EMOJI_PRIVATE = "\u{1F512}" // or 512
app.locals.EMOJI_MATCH = "\u{1f3c6}"
+app.locals.TM_ICON_QUEUE = "\u{1f465}"
+app.locals.TM_ICON_TICKET = "\u{1f3ab}"
+app.locals.TM_ICON_ACTIVE = "\u{1f3c1}"
+app.locals.TM_ICON_FINISHED = "\u{1f3c6}"
+
app.locals.PACE_ICON = [
"", // none
"\u{26a1}", // blitz
@@ -193,6 +221,8 @@ app.locals.PACE_TEXT = [
"1+ moves per day",
]
+app.locals.human_date = human_date
+
app.set("x-powered-by", false)
app.set("etag", false)
app.set("view engine", "pug")
@@ -719,12 +749,14 @@ app.get("/user/:who_name", function (req, res) {
who.atime = human_date(who.atime)
let games = QUERY_LIST_PUBLIC_GAMES_OF_USER.all({ user_id: who.user_id })
annotate_games(games, 0, null)
+ let active_pools = TM_POOL_LIST_USER_ACTIVE.all(who.user_id)
+ let finished_pools = TM_POOL_LIST_USER_RECENT_FINISHED.all(who.user_id)
let relation = 0
if (req.user)
relation = SQL_SELECT_RELATION.get(req.user.user_id, who.user_id) | 0
- res.render("user.pug", { user: req.user, who, relation, games })
+ res.render("user.pug", { user: req.user, who, relation, games, active_pools, finished_pools })
} else {
- return res.status(404).send("Invalid user name.")
+ return res.status(404).send("User not found.")
}
})
@@ -1320,7 +1352,6 @@ const SQL_SELECT_SNAP = SQL("select * from game_snap where game_id = ? and snap_
const SQL_SELECT_SNAP_STATE = SQL("select state from game_snap where game_id = ? and snap_id = ?").pluck()
const SQL_SELECT_SNAP_COUNT = SQL("select max(snap_id) from game_snap where game_id=?").pluck()
-
const SQL_DELETE_GAME_SNAP = SQL("delete from game_snap where game_id=? and snap_id > ?")
const SQL_DELETE_GAME_REPLAY = SQL("delete from game_replay where game_id=? and replay_id > ?")
@@ -1613,9 +1644,22 @@ app.get("/games/active", must_be_logged_in, function (req, res) {
let games = QUERY_LIST_ACTIVE_GAMES_OF_USER.all({ user_id })
let unread = SQL_SELECT_UNREAD_CHAT_GAMES.all(user_id)
annotate_games(games, user_id, unread)
+
+ let seeds = TM_SEED_LIST_USER.all(user_id)
+ let active_pools = TM_POOL_LIST_USER_ACTIVE.all(user_id)
+ let finished_pools = TM_POOL_LIST_USER_RECENT_FINISHED.all(user_id)
+
res.render("games_active.pug", { user: req.user, who: req.user, games, seeds, active_pools, finished_pools })
})
+app.get("/tm/active", must_be_logged_in, function (req, res) {
+ let user_id = req.user.user_id
+ let seeds = TM_SEED_LIST_USER.all(user_id)
+ let active_pools = TM_POOL_LIST_USER_ACTIVE.all(user_id)
+ let finished_pools = TM_POOL_LIST_USER_RECENT_FINISHED.all(user_id)
+ res.render("tm_active.pug", { user: req.user, who: req.user, seeds, active_pools, finished_pools })
+})
+
app.get("/games/finished", must_be_logged_in, function (req, res) {
let games = QUERY_LIST_FINISHED_GAMES_OF_USER.all({ user_id: req.user.user_id })
let unread = SQL_SELECT_UNREAD_CHAT_GAMES.all(req.user.user_id)
@@ -1623,12 +1667,27 @@ app.get("/games/finished", must_be_logged_in, function (req, res) {
res.render("games_finished.pug", { user: req.user, who: req.user, games })
})
+app.get("/tm/finished", must_be_logged_in, function (req, res) {
+ let pools = TM_POOL_LIST_USER_ALL_FINISHED.all(req.user.user_id)
+ res.render("tm_finished.pug", { user: req.user, who: req.user, pools })
+})
+
app.get("/games/finished/:who_name", function (req, res) {
let who = SQL_SELECT_USER_BY_NAME.get(req.params.who_name)
if (who) {
let games = QUERY_LIST_FINISHED_GAMES_OF_USER.all({ user_id: who.user_id })
annotate_games(games, 0, null)
- res.render("games_finished.pug", { user: req.user, who: who, games: games })
+ res.render("games_finished.pug", { user: req.user, who, games })
+ } else {
+ return res.status(404).send("Invalid user name.")
+ }
+})
+
+app.get("/tm/finished/:who_name", function (req, res) {
+ let who = SQL_SELECT_USER_BY_NAME.get(req.params.who_name)
+ if (who) {
+ let pools = TM_POOL_LIST_USER_ALL_FINISHED.all(who.user_id)
+ res.render("tm_finished.pug", { user: req.user, who, pools })
} else {
return res.status(404).send("Invalid user name.")
}
@@ -1680,6 +1739,10 @@ function get_title_page(req, res, title_id) {
annotate_games(active_games, user_id, unread)
annotate_games(finished_games, user_id, unread)
+ let seeds = TM_SEED_LIST_TITLE.all(user_id, title_id)
+ let active_pools = TM_POOL_LIST_TITLE_ACTIVE.all(title_id)
+ let finished_pools = TM_POOL_LIST_TITLE_FINISHED.all(title_id)
+
res.render("info.pug", {
user: req.user,
title: title,
@@ -1687,6 +1750,9 @@ function get_title_page(req, res, title_id) {
replacement_games,
active_games,
finished_games,
+ seeds,
+ active_pools,
+ finished_pools,
})
}
@@ -1806,6 +1872,11 @@ function insert_rematch_players(old_game_id, new_game_id, req_user_id, order) {
app.get("/rematch/:old_game_id", must_be_logged_in, function (req, res) {
let old_game_id = req.params.old_game_id | 0
+
+ let pool_name = TM_FIND_POOL_NAME.get(old_game_id)
+ if (pool_name)
+ return res.redirect("/tm/pool/" + pool_name)
+
let magic = "\u{1F503} " + old_game_id
let new_game_id = SQL_SELECT_REMATCH.get(magic)
if (new_game_id)
@@ -1990,7 +2061,7 @@ app.post("/api/invite/:game_id/:role/:user", must_be_logged_in, function (req, r
res.send("User not found.")
else if (user_id === req.user.user_id)
res.send("You cannot invite yourself!")
- else
+ else
do_join(res, game_id, role, user_id, null, 1)
})
@@ -2538,6 +2609,7 @@ const QUERY_PURGE_FINISHED_GAMES = SQL(`
games
where
status > 1
+ and not is_match
and ( not is_opposed or moves < player_count * 3 )
and julianday(mtime) < julianday('now', '-10 days')
`)
@@ -2601,6 +2673,11 @@ function time_control_ticker() {
if (item.is_opposed) {
console.log("TIMED OUT GAME:", item.game_id, item.role)
do_resign(item.game_id, item.role, "timed out")
+ if (item.is_match) {
+ console.log("BANNED FROM TOURNAMENTS:", item.user_id)
+ TM_INSERT_BANNED.run(item.user_id)
+ TM_DELETE_QUEUE_ALL.run(item.user_id)
+ }
} else {
console.log("TIMED OUT GAME:", item.game_id, item.role, "(solo)")
SQL_DELETE_GAME.run(item.game_id)
@@ -2613,6 +2690,699 @@ setInterval(time_control_ticker, 13 * 60 * 1000)
setTimeout(time_control_ticker, 13 * 1000)
/*
+ * TOURNAMENTS
+ */
+
+const designs = require("./designs.js")
+
+const TM_INSERT_BANNED = SQL("insert into tm_banned (user_id, time) values (?, datetime())")
+const TM_DELETE_QUEUE_ALL = SQL("delete from tm_queue where user_id=?")
+
+const TM_MAY_JOIN_ANY_SEED = SQL(`
+ select ( select notify and is_verified from users where user_id=@user_id )
+ or ( select exists ( select 1 from webhooks where user_id=@user_id and error is null ) )
+ or ( select exists ( select 1 from ratings where user_id=@user_id ) )
+ as may_join
+`).pluck()
+
+const TM_MAY_JOIN_SEED = SQL(`
+ select ( select not exists ( select 1 from tm_banned where user_id=@user_id ) )
+ and ( select coalesce(is_open, 0) as may_join from tm_seeds where seed_id=@seed_id )
+`).pluck()
+
+function may_join_any_seed(user_id) {
+ return DEBUG || TM_MAY_JOIN_ANY_SEED.get({user_id})
+}
+
+function may_join_seed(user_id, seed_id) {
+ return TM_MAY_JOIN_SEED.get({user_id,seed_id})
+}
+
+const TM_SEED_LIST_ALL = SQL(`
+ select
+ tm_seeds.*,
+ sum(level is 1) as queue_size,
+ sum(user_id is ?) as is_queued
+ from tm_seeds left join tm_queue using(seed_id)
+ group by seed_id
+ order by seed_name
+`)
+
+const TM_SEED_LIST_TITLE = SQL(`
+ select
+ tm_seeds.*,
+ sum(level is 1) as queue_size,
+ sum(user_id is ?) as is_queued
+ from tm_seeds left join tm_queue using(seed_id)
+ where title_id = ?
+ group by seed_id
+ order by seed_name
+`)
+
+const TM_SEED_LIST_USER = SQL(`
+ select
+ tm_seeds.*,
+ sum(level is 1) as queue_size,
+ sum(user_id is ?) as is_queued
+ from tm_seeds left join tm_queue using(seed_id)
+ group by seed_id
+ having is_queued
+ order by seed_name
+`)
+
+const TM_POOL_LIST_USER_ACTIVE = SQL(`
+ select * from tm_pool_active_view
+ where not is_finished and pool_id in (
+ select pool_id
+ from tm_rounds
+ join players using(game_id)
+ where user_id = ?
+ )
+`)
+
+const TM_POOL_LIST_USER_RECENT_FINISHED = SQL(`
+ select * from tm_pool_finished_view
+ where
+ finish_date > date('now', '-14 days')
+ and pool_id in (
+ select pool_id
+ from tm_rounds
+ join players using(game_id)
+ where user_id = ?
+ )
+`)
+
+const TM_POOL_LIST_USER_ALL_FINISHED = SQL(`
+ select * from tm_pool_finished_view
+ where
+ pool_id in (
+ select pool_id
+ from tm_rounds
+ join players using(game_id)
+ where user_id = ?
+ )
+`)
+
+const TM_POOL_LIST_TITLE_ACTIVE = SQL(`
+ select tm_pool_active_view.* from tm_pool_active_view join tm_seeds using(seed_id)
+ where tm_seeds.title_id = ?
+`)
+
+const TM_POOL_LIST_TITLE_FINISHED = SQL(`
+ select tm_pool_finished_view.* from tm_pool_finished_view join tm_seeds using(seed_id)
+ where tm_seeds.title_id = ? and finish_date > date('now', '-14 days')
+`)
+
+const TM_POOL_LIST_SEED_ACTIVE = SQL("select * from tm_pool_active_view where seed_id = ?")
+const TM_POOL_LIST_SEED_FINISHED = SQL("select * from tm_pool_finished_view where seed_id = ?")
+
+const TM_SELECT_QUEUE_BLACKLIST = SQL("select me, you from contacts join tm_queue q on q.user_id=me or q.user_id=you where relation < 0 and seed_id=? and level=?")
+const TM_SELECT_QUEUE_NAMES = SQL("select user_id, name, level from tm_queue join users using(user_id) where seed_id=? and level=? order by time")
+const TM_SELECT_QUEUE = SQL("select user_id from tm_queue where seed_id=? and level=? order by time desc").pluck()
+const TM_DELETE_QUEUE = SQL("delete from tm_queue where user_id=? and seed_id=? and level=?")
+const TM_INSERT_QUEUE = SQL("insert into tm_queue (user_id, seed_id, level) values (?,?,?)")
+
+const TM_SELECT_SEED = SQL("select * from tm_seeds where seed_id = ?")
+const TM_SELECT_SEED_BY_NAME = SQL("select * from tm_seeds where seed_name = ?")
+const TM_SELECT_POOL_BY_NAME = SQL("select * from tm_pools where pool_name=?")
+
+const TM_INSERT_POOL = SQL("insert into tm_pools (seed_id, level, is_finished, start_date, pool_name) values (?,?,0,datetime(),?) returning pool_id").pluck()
+const TM_INSERT_ROUND = SQL("insert into tm_rounds (game_id, pool_id, round) values (?,?,?)")
+
+const TM_UPDATE_POOL_FINISHED = SQL("update tm_pools set is_finished=1, finish_date=datetime() where pool_id=?")
+
+const TM_FIND_POOL_NAME = SQL("select pool_name from tm_rounds join tm_pools using(pool_id) where game_id=?").pluck()
+const TM_FIND_NEXT_POOL_NUMBER = SQL("select 1 + count(1) from tm_pools where seed_id = ? and level = ?").pluck()
+
+const TM_SELECT_GAMES = SQL(`
+ select
+ tm_rounds.*,
+ games.status,
+ games.moves,
+ json_group_object(role, name) as role_names,
+ json_group_object(role, score) as role_scores
+ from
+ tm_rounds
+ left join games using(game_id)
+ left join players using(game_id)
+ left join users using(user_id)
+ where
+ pool_id=?
+ group by
+ game_id
+`)
+
+const TM_SELECT_WINNERS = SQL("select * from tm_winners where pool_id = ?")
+
+const TM_SELECT_PLAYERS_2P = SQL(`
+ with
+ score_cte as (
+ select
+ pool_id,
+ u1.user_id as user_id,
+ u1.name as name,
+ u2.name as opponent,
+ json_group_array(json_array(game_id, p1.score)) as result
+ from
+ tm_rounds
+ left join players as p1 using(game_id)
+ left join players as p2 using(game_id)
+ left join users as u1 on u1.user_id=p1.user_id
+ left join users as u2 on u2.user_id=p2.user_id
+ where
+ pool_id = ?
+ and p1.user_id != p2.user_id
+ group by u1.name, u2.name
+ )
+ select
+ name,
+ json_group_object(opponent, json(result)) as result,
+ coalesce(points, 0) as points,
+ coalesce(son, 0) as son
+ from
+ score_cte
+ left join tm_results using(pool_id, user_id)
+ group by
+ user_id
+ order by
+ points desc, son desc, name
+`)
+
+const TM_SELECT_PLAYERS_MP = SQL(`
+ select
+ name,
+ json_group_array(json_array(game_id, score)) as result,
+ coalesce(points, 0) as points,
+ coalesce(son, 0) as son
+ from
+ tm_rounds
+ left join games using(game_id)
+ left join players using(game_id)
+ left join users using(user_id)
+ left join tm_results using(pool_id, user_id)
+ where
+ pool_id = ?
+ group by
+ user_id
+ order by
+ points desc, son desc, name
+`)
+
+const TM_FIND_NEXT_GAME_TO_START = SQL(`
+ with
+ user_busy as (
+ select
+ pool_id, round, user_id, role
+ from
+ tm_rounds
+ join games using(game_id)
+ join players using(game_id)
+ where
+ status = 1
+ ),
+ next_round as (
+ select
+ pool_id,
+ round,
+ coalesce(
+ lag( sum(status < 2) = 0 ) over ( partition by pool_id order by round ),
+ 1
+ ) as is_round_ready
+ from
+ tm_rounds
+ join games using(game_id)
+ group by
+ pool_id, round
+ ),
+ next_game as (
+ select
+ pool_id,
+ games.game_id,
+ games.title_id,
+ games.scenario,
+ games.options,
+ sum(
+ exists (
+ select 1 from user_busy
+ where user_busy.pool_id = tm_rounds.pool_id
+ and user_busy.round = tm_rounds.round
+ and user_busy.user_id = players.user_id
+ and user_busy.role = players.role
+ )
+ ) = 0 as is_user_ready
+ from
+ next_round
+ join tm_rounds using(pool_id, round)
+ join games using(game_id)
+ join players using(game_id)
+ where
+ status = 0 and is_round_ready
+ group by
+ game_id
+ having
+ is_user_ready
+ )
+ select
+ pool_id, game_id, title_id, scenario, options
+ from
+ next_game
+ limit 1
+`)
+
+const TM_SELECT_ENDED_POOLS = SQL(`
+ select
+ pool_id, seed_id, level, pool_name, level_count
+ from
+ tm_pools
+ join tm_seeds using(seed_id)
+ join tm_rounds using(pool_id)
+ join games using(game_id)
+ where
+ not is_finished
+ group by
+ pool_id
+ having
+ sum(status < 2) = 0
+`)
+
+const TM_SELECT_SEED_READY_MINI_CUP = SQL(`
+ select
+ seed_id, level
+ from
+ tm_seeds
+ join tm_queue using(seed_id)
+ where
+ is_open and seed_name like 'mc.%'
+ and julianday(time) < julianday('now', '-30 seconds')
+ group by
+ seed_id, level
+ having
+ count(1) >= pool_size
+`)
+
+app.get("/tm/list", function (req, res) {
+ let seeds = TM_SEED_LIST_ALL.all(req.user ? req.user.user_id : 0)
+ let seeds_by_title = object_group_by(seeds, "title_id")
+ res.render("tm_list.pug", { user: req.user, seeds, seeds_by_title })
+})
+
+app.get("/tm/seed/:seed_name", function (req, res) {
+ let seed_name = req.params.seed_name
+ let seed = TM_SELECT_SEED_BY_NAME.get(seed_name)
+ if (!seed)
+ return res.status(404).send("Tournament seed not found.")
+ let seed_id = seed.seed_id
+ let queues = []
+ for (let level = 1; level <= seed.level_count; ++level)
+ queues[level-1] = TM_SELECT_QUEUE_NAMES.all(seed_id, level)
+
+ let active_pools = TM_POOL_LIST_SEED_ACTIVE.all(seed_id)
+ let finished_pools = TM_POOL_LIST_SEED_FINISHED.all(seed_id)
+
+ let error = null
+ let may_register = false
+ if (req.user && seed.is_open) {
+ if (!may_join_any_seed(req.user.user_id))
+ error = "Please verify your mail address and enable notifications to join tournaments."
+ else if (!may_join_seed(req.user.user_id, seed_id))
+ error = "You may not register for this tournament."
+ else
+ may_register = true
+ }
+
+ res.render("tm_seed.pug", { user: req.user, error, may_register, seed, queues, active_pools, finished_pools })
+})
+
+app.get("/tm/pool/:pool_name", function (req, res) {
+ let pool_name = req.params.pool_name
+ let pool = TM_SELECT_POOL_BY_NAME.get(pool_name)
+ if (!pool)
+ return res.status(404).send("Tournament pool not found.")
+ let pool_id = pool.pool_id
+ let seed = TM_SELECT_SEED.get(pool.seed_id)
+ let roles = get_game_roles(seed.title_id, seed.scenario, seed.options)
+ let players
+ if (seed.player_count === 2)
+ players = TM_SELECT_PLAYERS_2P.all(pool_id)
+ else
+ players = TM_SELECT_PLAYERS_MP.all(pool_id)
+ let games = TM_SELECT_GAMES.all(pool_id)
+ let games_by_round = object_group_by(games, "round")
+ res.render("tm_pool.pug", { user: req.user, seed, pool, roles, players, games_by_round })
+})
+
+app.post("/api/tm/register/:seed_id", must_be_logged_in, function (req, res) {
+ let seed_id = req.params.seed_id | 0
+ let user_id = req.user.user_id
+ if (!may_join_any_seed(user_id))
+ return res.status(401).send("You may not join any tournaments right now.")
+ if (!may_join_seed(user_id, seed_id))
+ return res.status(401).send("You may not join this tournament.")
+ TM_INSERT_QUEUE.run(user_id, seed_id, 1)
+ return res.redirect(req.headers.referer)
+})
+
+app.post("/api/tm/withdraw/:seed_id/:level", must_be_logged_in, function (req, res) {
+ let seed_id = req.params.seed_id | 0
+ let level = req.params.level | 0
+ let user_id = req.user.user_id
+ TM_DELETE_QUEUE.run(user_id, seed_id, level)
+ return res.redirect(req.headers.referer)
+})
+
+app.post("/api/tm/start/:seed_id/:level", must_be_administrator, function (req, res) {
+ let seed_id = req.params.seed_id | 0
+ let level = req.params.level | 0
+ start_tournament_seed(seed_id, level)
+ tm_start_ready_games()
+ return res.redirect(req.headers.referer)
+})
+
+function make_pools(seed, players) {
+ let v = players.length
+ let k = seed.player_count
+ let n = seed.round_count
+
+ if (k === 2) {
+ if (n === 4) {
+ if (v % 5 === 0)
+ return designs.pool_players(players, 5)
+ if (v % 3 === 0)
+ return designs.pool_players(players, 3)
+ if (v > 7)
+ return designs.pool_players_using_knapsack(players, "5/3")
+ }
+
+ if (n === 6) {
+ if (v % 7 === 0)
+ return designs.pool_players(players, 7)
+ if (v % 4 === 0)
+ return designs.pool_players(players, 4)
+ if (v > 17)
+ return designs.pool_players_using_knapsack(players, "7/4")
+ }
+
+ if (n === 8) {
+ if (v % 9 === 0)
+ return designs.pool_players(players, 9)
+ if (v % 5 === 0)
+ return designs.pool_players(players, 5)
+ if (v > 31)
+ return designs.pool_players_using_knapsack(players, "9/5")
+ }
+
+ if (v % (n+1) === 0)
+ return designs.pool_players(players, n+1)
+
+ throw new Error("cannot create pools for this player/rounds configuration")
+
+ if (v > n+1)
+ return designs.pool_players(players, n+1)
+
+ return [ players ]
+ }
+
+ if (k === 3) {
+ // youden squares
+ if (v % 7 === 0) return designs.pool_players(players, 7)
+ // kirkman triple systems
+ if (v % 9 === 0) return designs.pool_players(players, 9)
+ if (v % 15 === 0) return designs.pool_players(players, 15)
+ if (v % 21 === 0) return designs.pool_players(players, 21)
+ if (v % 27 === 0) return designs.pool_players(players, 27)
+ if (v % 33 === 0) return designs.pool_players(players, 33)
+ if (v % 39 === 0) return designs.pool_players(players, 39)
+ if (v % 45 === 0) return designs.pool_players(players, 45)
+ if (v % 51 === 0) return designs.pool_players(players, 51)
+ // misc bibd
+ if (v % 13 === 0 && n == 6)
+ return designs.pool_players(players, 13)
+ }
+
+ if (k === 4) {
+ // youden squares
+ if (v % 7 === 0) return designs.pool_players(players, 7)
+ if (v % 13 === 0) return designs.pool_players(players, 13)
+ // steiner quadrilateral systems
+ if (v % 16 === 0) return designs.pool_players(players, 16)
+ if (v % 28 === 0) return designs.pool_players(players, 28)
+ if (v % 40 === 0) return designs.pool_players(players, 40)
+ if (v % 52 === 0) return designs.pool_players(players, 52)
+ // misc bibd
+ if (v % 9 === 0 && n == 8)
+ return designs.pool_players(players, 9)
+ }
+
+ if (k === 5) {
+ // youden squares
+ if (v % 11 === 0) return designs.pool_players(players, 11)
+ if (v % 21 === 0) return designs.pool_players(players, 21)
+ // resolvable bibd
+ if (v % 25 === 0) return designs.pool_players(players, 25)
+ }
+
+ if (k === 6) {
+ // youden squares / bibd
+ if (v % 11 === 0) return designs.pool_players(players, 11)
+ if (v % 16 === 0) return designs.pool_players(players, 16)
+ if (v % 31 === 0) return designs.pool_players(players, 31)
+ }
+
+ throw new Error("cannot create pools for this player count")
+}
+
+function make_rounds(seed, players) {
+ let v = players.length
+ let k = seed.player_count
+ let n = seed.round_count
+ let rounds
+ if (seed.is_concurrent)
+ rounds = make_concurrent_rounds(v, k, n)
+ else
+ rounds = make_sequential_rounds(v, k, n)
+ return rounds.map(r => r.map(m => m.map(p => players[p])))
+}
+
+function make_concurrent_rounds(v, k, n) {
+ if (k === 2) {
+ if (v - 1 <= n / 2)
+ return [ designs.double_berger_table(v).flat() ]
+ else if (v & 1)
+ return [ designs.concurrent_round_robin(v).flat() ]
+ else
+ return [ designs.berger_table(v).flat() ]
+ }
+
+ let bibd = designs.youden_square(v, k)
+ if (bibd)
+ return [ bibd ]
+
+ let rbibd = designs.resolvable_bibd(v, k)
+ if (rbibd)
+ return rbibd.slice(0, n).flat()
+
+ throw new Error("cannot create rounds for this configuration")
+}
+
+function make_sequential_rounds(v, k, n) {
+ if (k === 2) {
+ if (v - 1 <= n / 2)
+ return designs.double_berger_table(v)
+ else
+ return designs.berger_table(v)
+ }
+
+ let rbibd = designs.resolvable_bibd(v, k)
+ if (rbibd)
+ return rbibd.slice(0, n)
+
+ throw new Error("cannot create rounds for this configuration")
+}
+
+function create_tournament(seed, level, players) {
+ let pools = make_pools(seed, players)
+ for (let i = 0; i < pools.length; ++i)
+ create_tournament_pool(seed, level, pools[i])
+}
+
+function create_tournament_pool(seed, level, players) {
+ let rounds = make_rounds(seed, players)
+
+ let pool_name = seed.seed_name + "." + level + "." + TM_FIND_NEXT_POOL_NUMBER.get(seed.seed_id, level)
+
+ let pool_id = TM_INSERT_POOL.get(seed.seed_id, level, pool_name)
+
+ console.log("TM POOL", pool_name, players.length, "players", rounds.length, "rounds")
+
+ for (let p of players) {
+ TM_DELETE_QUEUE.run(p, seed.seed_id, level)
+ }
+
+ for (let i = 0; i < rounds.length; ++i) {
+ for (let match of rounds[i]) {
+ create_tournament_game(seed, pool_id, i+1, pool_name, match)
+ }
+ }
+}
+
+function create_tournament_game(seed, pool_id, round, pool_name, players) {
+ if (players.length !== seed.player_count)
+ throw new Error("player count mismatch in tournament setup")
+
+ let roles = get_game_roles(seed.title_id, seed.scenario, parse_game_options(seed.options))
+ if (players.length !== roles.length)
+ throw new Error("player count mismatch in tournament setup")
+
+ let game_id = SQL_INSERT_GAME.get(
+ 0, // owner
+ seed.title_id,
+ seed.scenario,
+ seed.options,
+ seed.player_count,
+ 2, // pace
+ 0, // is_private
+ 0, // is_random
+ pool_name, // notice
+ 1 // is_match
+ )
+
+ for (let i = 0; i < players.length; ++i)
+ SQL_INSERT_PLAYER_ROLE.run(game_id, roles[i], players[i], 0)
+
+ TM_INSERT_ROUND.run(game_id, pool_id, round)
+
+ return game_id
+}
+
+function filter_queue_through_blacklist(queue, count, blacklist) {
+ function can_add_player(pool, b) {
+ for (let a of pool) {
+ for (let {me, you} of blacklist) {
+ if (me === a && you === b)
+ return false
+ if (me === b && you === a)
+ return false
+ }
+ }
+ return true
+ }
+
+ function rec(output, input) {
+ for (;;) {
+ if (output.length === count)
+ return output
+ if (input.length === 0)
+ return false
+ let a = input.pop()
+ if (can_add_player(output, a)) {
+ output.push(a)
+ if (rec(output, input.slice()))
+ return output
+ output.pop()
+ }
+ }
+ }
+
+ return rec([], queue)
+}
+
+function start_tournament_seed_mc(seed_id, level) {
+ let seed = TM_SELECT_SEED.get(seed_id)
+ let queue = TM_SELECT_QUEUE.all(seed_id, level)
+ let blacklist = TM_SELECT_QUEUE_BLACKLIST.all(seed_id, level)
+
+ console.log("TM SPAWN SEED (MC)", seed.seed_name, level, queue.length)
+
+ let players = filter_queue_through_blacklist(queue, seed.pool_size, blacklist)
+ if (!players)
+ throw new Error("Too many blacklisted players to form pool!")
+
+ SQL_BEGIN.run()
+ try {
+ shuffle(players)
+ create_tournament(seed, level, players)
+ SQL_COMMIT.run()
+ } catch (err) {
+ console.log(err)
+ } finally {
+ if (db.inTransaction)
+ SQL_ROLLBACK.run()
+ }
+}
+
+function start_tournament_seed(seed_id, level) {
+ let seed = TM_SELECT_SEED.get(seed_id)
+
+ if (seed.seed_name.startsWith("mc."))
+ return start_tournament_seed_mc(seed_id, level)
+
+ let queue = TM_SELECT_QUEUE.all(seed_id, level)
+ console.log("TM SPAWN SEED", seed.seed_name, level, queue.length)
+
+ shuffle(queue)
+
+ SQL_BEGIN.run()
+ try {
+ create_tournament(seed, level, queue)
+ SQL_COMMIT.run()
+ } finally {
+ if (db.inTransaction)
+ SQL_ROLLBACK.run()
+ }
+}
+
+function tm_reap_pools() {
+ // reap pools that are finished (and promote winners)
+ let ended = TM_SELECT_ENDED_POOLS.all()
+ for (let item of ended) {
+ console.log("TM POOL - END", item.pool_name)
+ SQL_BEGIN.run()
+ try {
+ TM_UPDATE_POOL_FINISHED.run(item.pool_id)
+ if (item.level < item.level_count) {
+ let winners = TM_SELECT_WINNERS.all(item.pool_id)
+ for (let user_id of winners)
+ TM_INSERT_QUEUE.run(user_id, item.seed_id, item.level + 1)
+ }
+ SQL_COMMIT.run()
+ } finally {
+ if (db.inTransaction)
+ SQL_ROLLBACK.run()
+ }
+ }
+}
+
+function tm_start_ready_seeds() {
+ // start seeds that are ready
+ for (let item of TM_SELECT_SEED_READY_MINI_CUP.all())
+ start_tournament_seed_mc(item.seed_id, item.level)
+}
+
+function tm_start_ready_games() {
+ // start games that are ready
+ for (;;) {
+ let game = TM_FIND_NEXT_GAME_TO_START.get()
+ if (game)
+ start_game(game)
+ else
+ break
+ }
+}
+
+function tournament_ticker() {
+ try {
+ tm_reap_pools()
+ tm_start_ready_seeds()
+ tm_start_ready_games()
+ } catch (err) {
+ console.log(err)
+ }
+}
+
+setTimeout(tournament_ticker, 19 * 1000)
+setInterval(tournament_ticker, 97 * 1000)
+
+/*
* GAME SERVER
*/