summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--designs.js3
-rw-r--r--public/common/client.css19
-rw-r--r--public/common/client.js28
-rw-r--r--public/common/replay.js33
-rw-r--r--public/common/util.js2
-rw-r--r--public/docs/tournaments.html2
-rw-r--r--public/style.css2
-rw-r--r--schema.sql275
-rw-r--r--server.js464
-rw-r--r--tools/elo.js2
-rwxr-xr-xtools/import-game.js4
-rw-r--r--tools/lift-bans.sh30
-rwxr-xr-xtools/patchgame.js66
-rw-r--r--tools/purge.sql6
-rw-r--r--views/create-index.pug2
-rw-r--r--views/create.pug25
-rw-r--r--views/games_finished.pug5
-rw-r--r--views/games_public.pug2
-rw-r--r--views/head.pug11
-rw-r--r--views/profile.pug4
-rw-r--r--views/tm_active.pug4
-rw-r--r--views/tm_finished.pug5
-rw-r--r--views/tm_list.pug4
-rw-r--r--views/tm_pool.pug30
-rw-r--r--views/tm_seed.pug4
-rw-r--r--views/user.pug32
26 files changed, 773 insertions, 291 deletions
diff --git a/designs.js b/designs.js
index ae5396d..4df0b9b 100644
--- a/designs.js
+++ b/designs.js
@@ -197,6 +197,7 @@ designs.resolvable_bibd = function (v, k) {
switch (k) {
case 3:
switch (v) {
+ case 6: return designs.social_golfer_6_3_1
case 9: return designs.resolvable_bibd_9_3_1
case 12: return designs.social_golfer_12_3_1
case 15: return designs.resolvable_bibd_15_3_1
@@ -233,6 +234,7 @@ designs.youden_square = function (v, k) {
case 3: return designs.youden_square_3_3_3
case 4: return designs.youden_square_4_3_2
case 7: return designs.youden_square_7_3_1
+ case 13: return designs.bibd_13_3_1 // sit twice
}
break
case 4:
@@ -240,6 +242,7 @@ designs.youden_square = function (v, k) {
case 4: return designs.youden_square_4_4_4
case 5: return designs.youden_square_5_4_3
case 7: return designs.youden_square_7_4_2
+ case 9: return designs.bibd_9_4_3 // sit twice
case 13: return designs.youden_square_13_4_1
}
break
diff --git a/public/common/client.css b/public/common/client.css
index 53b6a38..f328d12 100644
--- a/public/common/client.css
+++ b/public/common/client.css
@@ -12,7 +12,7 @@ html {
-webkit-tap-highlight-color: transparent; /* disable blue flashes when tapping on chrome mobile */
}
-summary::-webkit-details-marker {
+header summary::-webkit-details-marker {
display: none;
}
@@ -24,7 +24,7 @@ html {
user-select: text;
}
-summary img, button img, menu img {
+header summary img, button img, menu img {
pointer-events: none;
}
@@ -270,12 +270,12 @@ header.replay {
/* MENUS AND ICONS */
-details menu {
+header details menu {
display: block;
min-width: 140px;
}
-summary img, #toolbar button img {
+header summary img, #toolbar button img {
display: block;
height: 36px;
padding: 4px;
@@ -292,13 +292,13 @@ summary img, #toolbar button img {
margin: 0;
}
-details[open] > summary { background-color: #0004; }
+header details[open] > summary { background-color: #0004; }
@media (hover: hover) {
- summary:hover, #toolbar button:hover { background-color: #0004; }
+ header summary:hover, #toolbar button:hover { background-color: #0004; }
}
-summary:active, #toolbar button:active { background-color: #0008; }
+header summary:active, #toolbar button:active { background-color: #0008; }
-summary {
+header summary {
cursor: pointer;
list-style: none;
}
@@ -323,6 +323,9 @@ menu li {
cursor: pointer;
}
+menu li.checked::before { content: "\2714 " }
+menu li.unchecked::before { content: "\2714 "; color:transparent; }
+
menu li a {
display: block;
margin: -4px -8px;
diff --git a/public/common/client.js b/public/common/client.js
index 393eb0a..f8a5e23 100644
--- a/public/common/client.js
+++ b/public/common/client.js
@@ -361,9 +361,9 @@ function toggle_notepad() {
show_notepad()
}
-/* REMATCH & REPLAY BUTTONS WHEN GAME OVER */
+/* REMATCH & REPLAY BUTTONS WHEN GAME IS FINISHED */
-function on_game_over() {
+function on_finished() {
remove_resign_menu()
add_icon_button(1, "replay_button", "sherlock-holmes-mirror",
@@ -547,8 +547,10 @@ function connect_play() {
if (typeof on_update === "function")
on_update()
on_update_log(view.log_start, game_log.length)
- if (view.game_over)
- on_game_over()
+ break
+
+ case "finished":
+ on_finished()
break
case "snapsize":
@@ -569,10 +571,6 @@ function connect_play() {
if (typeof on_reply === "function")
on_reply(arg[0], arg[1])
break
-
- case "save":
- window.localStorage[params.title_id + "/save"] = arg
- break
}
}
}
@@ -580,7 +578,6 @@ function connect_play() {
/* HEADER */
let is_your_turn = false
-let old_active = null
function on_update_header() {
if (typeof on_prompt === "function")
@@ -600,14 +597,15 @@ function on_update_header() {
document.querySelector("header").classList.remove("your_turn")
is_your_turn = false
}
- old_active = view.active
update_title()
}
function on_update_roles() {
if (view.active !== undefined)
for (let role in roles)
- roles[role].element.classList.toggle("active", view.active === role)
+ roles[role].element.classList.toggle("active",
+ view.active === role || view.active === "Both" || view.active.includes(role)
+ )
}
/* LOG */
@@ -743,14 +741,6 @@ function send_query(q, param) {
send_message("query", [ q, param ])
}
-function send_save() {
- send_message("save")
-}
-
-function send_restore() {
- send_message("restore", window.localStorage[params.title_id + "/save"])
-}
-
/* REPLAY */
function init_replay() {
diff --git a/public/common/replay.js b/public/common/replay.js
index b6c5e84..950b244 100644
--- a/public/common/replay.js
+++ b/public/common/replay.js
@@ -92,6 +92,20 @@ function snap_from_state(state) {
return snap
}
+function finish_game_state(state, result, message) {
+ if (typeof rules.finish === "function") {
+ state = rules.finish(state, result, message)
+ } else {
+ state.state = "game_over"
+ state.active = "None"
+ state.result = result
+ state.victory = message
+ state.log.push("")
+ state.log.push(message)
+ }
+ return state
+}
+
function eval_action(s, item, p) {
let [ item_role, item_action, item_arguments ] = item
switch (item_action) {
@@ -101,11 +115,15 @@ function eval_action(s, item, p) {
if (params.mode === "debug")
s.log.push([p, item_role.substring(0,2), item_action, null])
- s.state = "game_over"
- s.active = "None"
- s.victory = item_role + " resigned."
- s.log.push("")
- s.log.push(s.victory)
+ let result = "None"
+ if (roles.length === 2) {
+ for (let r of roles)
+ if (r !== item_role)
+ result = r
+ }
+
+ s = finish_game_state(s, result, role + " resigned.")
+
return s
default:
if (params.mode === "debug")
@@ -151,11 +169,6 @@ function update_replay_view() {
if (params.mode !== "debug")
view.actions = null
- if (viewpoint === "Observer")
- view.game_over = 1
- if (replay_state.state === "game_over")
- view.game_over = 1
-
if (replay.length > 0) {
if (document.body.classList.contains("shift")) {
view.prompt = `[${replay_this}/${replay.length}] ${replay_state.active} / ${replay_state.state}`
diff --git a/public/common/util.js b/public/common/util.js
index 7f5459e..b8f1b6c 100644
--- a/public/common/util.js
+++ b/public/common/util.js
@@ -1,4 +1,4 @@
-// === COMMON LIBRARY ===
+/* COMMON LIBRARY */
function clear_undo() {
if (game.undo) {
diff --git a/public/docs/tournaments.html b/public/docs/tournaments.html
index 6122194..e8f08f8 100644
--- a/public/docs/tournaments.html
+++ b/public/docs/tournaments.html
@@ -52,7 +52,7 @@ score is used to break ties.
<p>
Some tournaments may have multiple levels. If you win a tournament at one
-level, you will automatically be queued for the next level up.
+level, you may play in the next level (once for each victory).
<hr>
diff --git a/public/style.css b/public/style.css
index 2707345..486b309 100644
--- a/public/style.css
+++ b/public/style.css
@@ -87,7 +87,7 @@ input[type="checkbox"], input[type="radio"] {
accent-color: currentcolor;
}
-input[type="text"], input[type="password"], input[type="email"], textarea, select[size] {
+input[type="text"], input[type="password"], input[type="email"], input[type="number"], textarea, select[size] {
background-color: var(--color-text);
color: var(--color-black);
border: var(--thin-border);
diff --git a/schema.sql b/schema.sql
index d321295..2840934 100644
--- a/schema.sql
+++ b/schema.sql
@@ -38,24 +38,53 @@ create table if not exists users (
mail text unique collate nocase,
notify integer default 0,
is_verified boolean default 0,
- is_banned boolean default 0,
- ctime datetime default current_timestamp,
- password text,
- salt text,
- about text
+ is_banned boolean default 0
);
insert or ignore into
- users (user_id, name, mail, ctime)
- values (0, 'Deleted', 'deleted@nowhere', null)
+ users (user_id, name, mail)
+ values (0, 'Deleted', 'deleted@nowhere')
;
+create table if not exists user_password (
+ user_id integer primary key,
+ password text,
+ salt text
+);
+
+create table if not exists user_about (
+ user_id integer primary key,
+ about text
+);
+
+create table if not exists user_first_seen (
+ user_id integer primary key,
+ ctime datetime,
+ ip text
+);
+
create table if not exists user_last_seen (
user_id integer primary key,
atime datetime,
ip text
);
+create table if not exists user_timeout (
+ user_id integer,
+ game_id integer,
+ time datetime default current_timestamp,
+ primary key (user_id, game_id)
+);
+
+create index if not exists user_timeout_idx on user_timeout(user_id, time);
+
+create table if not exists user_move_hist (
+ user_id integer,
+ minutes integer,
+ frequency integer default 1,
+ primary key (user_id, minutes)
+) without rowid;
+
create table if not exists tokens (
user_id integer primary key,
token text,
@@ -84,15 +113,101 @@ create view user_login_view as
user_id, name, mail, notify, password, salt
from
users
+ left join user_password using(user_id)
+ ;
+
+drop view if exists user_move_iqr;
+create view user_move_iqr as
+ with
+ aa as (
+ select
+ user_id,
+ sum(frequency) as total
+ from user_move_hist
+ group by user_id
+ ),
+ bb as (
+ select
+ user_id,
+ minutes,
+ 4 * sum(frequency) over (partition by user_id order by minutes) / (total+1) as quartile
+ from aa join user_move_hist using(user_id)
+ ),
+ cc as (
+ select
+ user_id,
+ quartile,
+ last_value(minutes) over (partition by user_id order by quartile) as minutes
+ from bb
+ where quartile < 3
+ group by user_id, quartile
+ )
+ select
+ user_id,
+ sum(minutes) filter (where quartile = 0) as q1,
+ sum(minutes) filter (where quartile = 1) as q2,
+ sum(minutes) filter (where quartile = 2) as q3
+ from cc
+ group by user_id
;
drop view if exists user_profile_view;
create view user_profile_view as
- select
- user_id, name, mail, notify, ctime, atime, about, is_banned
- from
- users
- left join user_last_seen using(user_id)
+ with
+ timeout as (
+ select
+ user_id,
+ count(1) as timeout_total,
+ max(time) as timeout_last
+ from
+ user_timeout
+ group by
+ user_id
+ ),
+ user_move_mean as (
+ select
+ user_id,
+ sum(minutes * frequency) / sum(frequency) as move_time_mean
+ from
+ user_move_hist
+ group by
+ user_id
+ ),
+ profile as (
+ select
+ user_id, name, mail, notify, ctime, atime, about, is_banned,
+ move_time_mean,
+ coalesce(q1, q2, q3) as move_time_q1,
+ coalesce(q2, q3) as move_time_q2,
+ q3 as move_time_q3,
+ coalesce(timeout_total, 0) as timeout_total,
+ coalesce(timeout_last, 0) as timeout_last
+ from
+ users
+ left join user_first_seen using(user_id)
+ left join user_last_seen using(user_id)
+ left join user_about using(user_id)
+ left join timeout using(user_id)
+ left join user_move_mean using(user_id)
+ left join user_move_iqr using(user_id)
+ )
+ select
+ profile.*,
+ (
+ select
+ count(1)
+ from
+ players
+ join games using(game_id)
+ where
+ players.user_id = profile.user_id
+ and games.is_opposed
+ and games.status > 1
+ and games.result != 'None'
+ and games.mtime > timeout_last
+ ) as games_since_timeout
+ from
+ profile
;
drop view if exists user_dynamic_view;
@@ -119,16 +234,17 @@ create view user_dynamic_view as
join games using(game_id)
where
status = 1
- and user_count = player_count
+ and is_opposed
+ and is_active
and players.user_id = users.user_id
- and active in ( players.role, 'Both' )
) + (
select
count(*)
from
players
where
- players.user_id = users.user_id and players.is_invite
+ is_invite
+ and players.user_id = users.user_id
) + (
select
count(*)
@@ -157,6 +273,7 @@ create view rated_games_view as
and moves >= player_count * 3
and user_count = player_count
and player_count > 1
+ and result != 'None'
and not exists (
select 1 from players where players.game_id = games.game_id and user_id = 0
)
@@ -427,6 +544,8 @@ create table if not exists players (
role text,
user_id integer,
is_invite integer,
+ is_active boolean,
+ active_time real, -- julianday
clock real,
score integer,
primary key (game_id, role)
@@ -471,19 +590,11 @@ create view player_view as
name,
role,
is_invite,
+ is_active,
(
- case status
- when 0 then
- owner_id = user_id
- when 1 then
- active in ( 'Both', role )
- else
- 0
- end
- ) as is_active,
- (
- case when active in ( 'Both', role ) then
- clock - (julianday() - julianday(mtime))
+ case when is_active
+ then
+ clock - (julianday() - julianday(active_time))
else
clock
end
@@ -509,8 +620,8 @@ create view time_control_view as
join players using(game_id)
where
status = 1
- and active in ( 'Both', role )
- and clock - (julianday() - julianday(mtime)) < 0
+ and is_active
+ and clock - (julianday() - julianday(players.active_time)) < 0
;
-- Export game state as JSON
@@ -590,12 +701,6 @@ create table if not exists tm_banned (
time datetime default current_timestamp
);
-create table if not exists tm_timeout (
- user_id integer,
- game_id integer,
- time datetime default current_timestamp
-);
-
create table if not exists tm_queue (
user_id integer,
seed_id integer,
@@ -704,6 +809,7 @@ begin
set score = (
case
when new.result is null then null
+ when new.result = 'None' then null
when new.result = role then 2
when new.result = 'Draw' then 1
when instr(new.result, role) then 1
@@ -776,7 +882,7 @@ begin
with
tt as (
select
- round_count as threshold
+ (2 * round_count) / player_count as threshold
from
tm_seeds
where
@@ -851,15 +957,91 @@ begin
games.game_id = old.game_id;
end;
--- Trigger to track time spent!
+-- Triggers to track is_active and time spent!
-drop trigger if exists trigger_time_used_update;
-create trigger trigger_time_used_update before update of active on games
+drop trigger if exists trigger_game_started;
+create trigger trigger_game_started after update of status on games
+ when old.status = 0 and new.status = 1
begin
- update players
- set clock = clock - (julianday() - julianday(old.mtime))
+ update
+ players
+ set
+ clock = (
+ case old.pace
+ when 1 then 1
+ when 2 then 3
+ when 3 then 3
+ else 21
+ end
+ )
where
- players.game_id = old.game_id and players.role in ( 'Both', old.active );
+ players.game_id = old.game_id
+ ;
+end;
+
+drop trigger if exists trigger_active_changed;
+create trigger trigger_active_changed after update of active on games
+begin
+ update
+ players
+ set
+ is_active = ( new.active = 'Both' or instr(new.active, players.role) )
+ where
+ players.game_id = old.game_id
+ ;
+end;
+
+drop trigger if exists trigger_player_to_active;
+create trigger trigger_player_to_active after update of is_active on players
+ when old.is_active is not true and new.is_active
+begin
+ update
+ players
+ set
+ active_time = julianday()
+ where
+ players.game_id = old.game_id and players.role = old.role
+ ;
+end;
+
+drop trigger if exists trigger_player_to_inactive;
+create trigger trigger_player_to_inactive after update of is_active on players
+ when old.is_active and (not new.is_active)
+begin
+ update
+ players
+ set
+ active_time = null,
+ clock = (
+ case (select pace from games where games.game_id = players.game_id)
+ when 1 then min(clock - (julianday() - julianday(old.active_time)) + 4 / 24.0, 3)
+ when 2 then min(clock - (julianday() - julianday(old.active_time)) + 12 / 24.0, 5)
+ when 3 then min(clock - (julianday() - julianday(old.active_time)) + 36 / 24.0, 10)
+ else 21
+ end
+ )
+ where
+ players.game_id = old.game_id and players.role = old.role
+ ;
+ insert into user_move_hist (user_id, minutes)
+ select
+ old.user_id,
+ case
+ when minutes < 60 then ceil(minutes)
+ when minutes < 720 then round(minutes / 5) * 5
+ when minutes < 4320 then round(minutes / 15) * 15
+ when minutes < 7200 then round(minutes / 60) * 60
+ else round(minutes / 360) * 360
+ end as minutes
+ from (
+ select (julianday() - julianday(old.active_time)) * 1440 as minutes
+ )
+ where (
+ select is_opposed from games where games.game_id = old.game_id
+ )
+ on conflict do update
+ set frequency = frequency + 1
+ ;
end;
-- Trigger to remove game data when filing a game as archived
@@ -892,10 +1074,15 @@ end;
drop trigger if exists trigger_delete_on_users;
create trigger trigger_delete_on_users after delete on users
begin
+ delete from user_password where user_id = old.user_id;
+ delete from user_first_seen where user_id = old.user_id;
+ delete from user_last_seen where user_id = old.user_id;
+ delete from user_about where user_id = old.user_id;
+ delete from user_move_hist where user_id = old.user_id;
+ delete from user_timeout where user_id = old.user_id;
+ delete from webhooks where user_id = old.user_id;
delete from logins where user_id = old.user_id;
delete from tokens where user_id = old.user_id;
- delete from webhooks where user_id = old.user_id;
- delete from user_last_seen where user_id = old.user_id;
delete from read_threads where user_id = old.user_id;
delete from unread_chats where user_id = old.user_id;
delete from contacts where me = old.user_id or you = old.user_id;
diff --git a/server.js b/server.js
index 37a292e..5059bcc 100644
--- a/server.js
+++ b/server.js
@@ -66,7 +66,13 @@ var game_cookies = {}
let db = new sqlite3(process.env.DATABASE || "./db")
db.pragma("synchronous = NORMAL")
-const SQL_BEGIN = db.prepare("begin")
+let ENABLE_ARCHIVE = process.env.ARCHIVE | 0
+if (ENABLE_ARCHIVE) {
+ console.log("Attached to archive database.")
+ db.exec("attach database 'archive.db' as archive")
+}
+
+const SQL_BEGIN = db.prepare("begin immediate")
const SQL_COMMIT = db.prepare("commit")
const SQL_ROLLBACK = db.prepare("rollback")
@@ -200,6 +206,7 @@ app.locals.ENABLE_MAIL = !!mailer
app.locals.ENABLE_WEBHOOKS = !!WEBHOOKS
app.locals.ENABLE_FORUM = process.env.FORUM | 0
app.locals.ENABLE_TOURNAMENTS = process.env.TOURNAMENTS | 0
+app.locals.ENABLE_ARCHIVE = ENABLE_ARCHIVE
app.locals.EMOJI_PRIVATE = "\u{1F512}" // or 512
app.locals.EMOJI_MATCH = "\u{1f3c6}"
@@ -225,6 +232,9 @@ app.locals.PACE_TEXT = [
app.locals.human_date = human_date
app.locals.format_options = format_options
+app.locals.format_minutes = format_minutes
+
+app.locals.may_join_seed_level = may_join_seed_level
app.set("x-powered-by", false)
app.set("etag", false)
@@ -313,6 +323,15 @@ function human_date(date) {
return new Date(epoch_from_julianday(date)).toISOString().substring(0,10)
}
+function format_minutes(mins) {
+ if (mins > 59) {
+ var hh = mins / 60 | 0
+ var mm = mins % 60
+ return `${hh} hours ${mm} minutes`
+ }
+ return mins + " minutes"
+}
+
function is_valid_email(email) {
return REGEX_MAIL.test(email)
}
@@ -407,7 +426,7 @@ function must_pass_altcha(req, res, next) {
return next()
}
-app.get("/altcha-challenge", async (req, res) => {
+app.get("/altcha-challenge", function (_req, res) {
return res.json(altcha_create_challenge())
})
@@ -421,14 +440,15 @@ const SQL_BLACKLIST_NAME = SQL("select exists ( select 1 from blacklist_name whe
const SQL_EXISTS_USER_NAME = SQL("SELECT EXISTS ( SELECT 1 FROM users WHERE name=? )").pluck()
const SQL_EXISTS_USER_MAIL = SQL("SELECT EXISTS ( SELECT 1 FROM users WHERE mail=? )").pluck()
-const SQL_INSERT_USER = SQL("INSERT INTO users (name,mail,password,salt) VALUES (?,?,?,?) RETURNING user_id,name,mail")
+const SQL_INSERT_USER = SQL("INSERT INTO users (name,mail) VALUES (?,?) RETURNING user_id,name,mail")
const SQL_DELETE_USER = SQL("DELETE FROM users WHERE user_id = ?")
const SQL_SELECT_LOGIN = SQL("SELECT * FROM user_login_view WHERE user_id=?")
-const SQL_SELECT_USER_VIEW = SQL("SELECT * FROM user_view WHERE user_id=?")
-const SQL_SELECT_USER_BY_NAME = SQL("SELECT * FROM user_view WHERE name=?")
const SQL_SELECT_LOGIN_BY_MAIL = SQL("SELECT * FROM user_login_view WHERE mail=?")
const SQL_SELECT_LOGIN_BY_NAME = SQL("SELECT * FROM user_login_view WHERE name=?")
+
+const SQL_SELECT_USER_VIEW = SQL("SELECT * FROM user_view WHERE user_id=?")
+const SQL_SELECT_USER_BY_NAME = SQL("SELECT * FROM user_view WHERE name=?")
const SQL_SELECT_USER_PROFILE = SQL("SELECT * FROM user_profile_view WHERE name=?")
const SQL_SELECT_USER_DYNAMIC = SQL("select * from user_dynamic_view where user_id=?")
const SQL_SELECT_USER_ID = SQL("SELECT user_id FROM users WHERE name=?").pluck()
@@ -440,9 +460,11 @@ const SQL_UPDATE_USER_NOTIFY = SQL("UPDATE users SET notify=? WHERE user_id=?")
const SQL_UPDATE_USER_NAME = SQL("UPDATE users SET name=? WHERE user_id=?")
const SQL_UPDATE_USER_MAIL = SQL("UPDATE users SET mail=? WHERE user_id=?")
const SQL_UPDATE_USER_VERIFIED = SQL("UPDATE users SET is_verified=? WHERE user_id=?")
-const SQL_UPDATE_USER_ABOUT = SQL("UPDATE users SET about=? WHERE user_id=?")
-const SQL_UPDATE_USER_PASSWORD = SQL("UPDATE users SET password=?, salt=? WHERE user_id=?")
-const SQL_UPDATE_USER_LAST_SEEN = SQL("INSERT OR REPLACE INTO user_last_seen (user_id,atime,ip) VALUES (?,datetime(),?)")
+
+const SQL_UPDATE_USER_ABOUT = SQL("insert or replace into user_about (user_id,about) values (?,?)")
+const SQL_UPDATE_USER_PASSWORD = SQL("insert or replace into user_password (user_id,password,salt) values (?,?,?)")
+const SQL_UPDATE_USER_FIRST_SEEN = SQL("insert or replace into user_first_seen (user_id,ctime,ip) values (?,datetime(),?)")
+const SQL_UPDATE_USER_LAST_SEEN = SQL("insert or replace into user_last_seen (user_id,atime,ip) values (?,datetime(),?)")
const SQL_UPDATE_USER_IS_BANNED = SQL("update users set is_banned=? where name=?")
const SQL_SELECT_WEBHOOK = SQL("SELECT * FROM webhooks WHERE user_id=?")
@@ -543,6 +565,7 @@ app.post("/signup", must_pass_altcha, function (req, res) {
function err(msg) {
res.render("signup.pug", { flash: msg })
}
+ let ip = req.headers["x-real-ip"] || req.ip || req.connection.remoteAddress || "0.0.0.0"
let name = req.body.username
let mail = req.body.mail
let password = req.body.password
@@ -561,7 +584,9 @@ app.post("/signup", must_pass_altcha, function (req, res) {
return err("Password is too long!")
let salt = crypto.randomBytes(32).toString("hex")
let hash = hash_password(password, salt)
- let user = SQL_INSERT_USER.get(name, mail, hash, salt)
+ let user = SQL_INSERT_USER.get(name, mail)
+ SQL_UPDATE_USER_FIRST_SEEN.run(user.user_id, ip)
+ SQL_UPDATE_USER_PASSWORD.run(user.user_id, hash, salt)
login_insert(res, user.user_id)
res.redirect("/profile")
})
@@ -653,7 +678,7 @@ app.post("/reset-password", function (req, res) {
return err("Invalid or expired token!")
let salt = crypto.randomBytes(32).toString("hex")
let hash = hash_password(password, salt)
- SQL_UPDATE_USER_PASSWORD.run(hash, salt, user.user_id)
+ SQL_UPDATE_USER_PASSWORD.run(user.user_id, hash, salt)
SQL_UPDATE_USER_VERIFIED.run(1, user.user_id)
login_insert(res, user.user_id)
return res.redirect("/profile")
@@ -677,7 +702,7 @@ app.post("/change-password", must_be_logged_in, function (req, res) {
return res.render("change_password.pug", { user: req.user, flash: "Wrong password!" })
let salt = crypto.randomBytes(32).toString("hex")
let hash = hash_password(newpass, salt)
- SQL_UPDATE_USER_PASSWORD.run(hash, salt, user.user_id)
+ SQL_UPDATE_USER_PASSWORD.run(user.user_id, hash, salt)
return res.redirect("/profile")
})
@@ -805,7 +830,7 @@ app.get("/change-about", must_be_logged_in, function (req, res) {
})
app.post("/change-about", must_be_logged_in, function (req, res) {
- SQL_UPDATE_USER_ABOUT.run(req.body.about, req.user.user_id)
+ SQL_UPDATE_USER_ABOUT.run(req.user.user_id, req.body.about)
return res.redirect("/profile")
})
@@ -813,13 +838,22 @@ app.get("/user/:who_name", function (req, res) {
let who = SQL_SELECT_USER_PROFILE.get(req.params.who_name)
if (who) {
let games = QUERY_LIST_PUBLIC_GAMES_OF_USER.all({ user_id: who.user_id })
+ let ratings = SQL_USER_RATINGS.all(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, active_pools, finished_pools })
+ res.render("user.pug", {
+ user: req.user,
+ who,
+ relation,
+ games,
+ active_pools,
+ finished_pools,
+ ratings,
+ })
} else {
return res.status(404).send("User not found.")
}
@@ -1270,10 +1304,11 @@ function format_options(options_json) {
function get_game_roles(title_id, scenario, options) {
let roles = RULES[title_id].roles
- if (typeof options === "string")
- options = parse_game_options(options)
- if (typeof roles === "function")
+ if (typeof roles === "function") {
+ if (typeof options === "string")
+ options = parse_game_options(options)
return roles(scenario, options)
+ }
return roles
}
@@ -1379,6 +1414,8 @@ const SQL_SELECT_REWIND = SQL("select snap_id, state->>'$.active' as active, sta
const SQL_UPDATE_GAME_ACTIVE = SQL("update games set active=?,mtime=datetime(),moves=moves+1 where game_id=?")
const SQL_UPDATE_GAME_SCENARIO = SQL("update games set scenario=? where game_id=?")
+const ARCHIVE_SELECT_GAME_STATE = ENABLE_ARCHIVE ? SQL("select state from archive.game_state where game_id=?").pluck() : null
+
const SQL_SELECT_GAME_STATE = SQL("select state from game_state where game_id=?").pluck()
const SQL_INSERT_GAME_STATE = SQL("insert or replace into game_state (game_id,state) values (?,?)")
@@ -1436,6 +1473,78 @@ const SQL_SELECT_REPLAY = SQL(`
where game_id = ?
`).pluck()
+const ARCHIVE_SELECT_REPLAY = ENABLE_ARCHIVE ? SQL(`
+ select
+ json_object(
+ 'players',
+ (select json_group_array(
+ json_object('role', role, 'name', name)
+ )
+ from players
+ left join users using(user_id)
+ where game_id = outer.game_id
+ ),
+ 'state',
+ (select json(state)
+ from archive.game_state
+ where game_id = outer.game_id
+ ),
+ 'replay',
+ (select json_group_array(
+ case when arguments is null then
+ json_array(role, action)
+ else
+ json_array(role, action, json(arguments))
+ end
+ )
+ from archive.game_replay
+ where game_id = outer.game_id
+ )
+ ) as export
+ from games as outer
+ where game_id = ?
+`).pluck() : null
+
+const ARCHIVE_SELECT_EXPORT = ENABLE_ARCHIVE ? SQL(`
+ select
+ game_id,
+ json_object(
+ 'setup', json_object(
+ 'game_id', game_id,
+ 'title_id', title_id,
+ 'scenario', scenario,
+ 'options', json(options),
+ 'player_count', player_count,
+ 'notice', notice
+ ),
+ 'players',
+ (select json_group_array(
+ json_object('role', role, 'name', name)
+ )
+ from players
+ left join users using(user_id)
+ where game_id = outer.game_id
+ ),
+ 'state',
+ (select json(state)
+ from archive.game_state
+ where game_id = outer.game_id
+ ),
+ 'replay',
+ (select json_group_array(
+ case when arguments is null then
+ json_array(role, action)
+ else
+ json_array(role, action, json(arguments))
+ end
+ )
+ from archive.game_replay
+ where game_id = outer.game_id
+ )
+ ) as export
+ from games as outer
+`).pluck() : null
+
const SQL_SELECT_EXPORT = SQL("select export from game_export_view where game_id=?").pluck()
const SQL_SELECT_GAME = SQL("SELECT * FROM games WHERE game_id=?")
@@ -1531,7 +1640,8 @@ const QUERY_NEXT_GAME_OF_USER = SQL(`
join players using(game_id)
where
status = ${STATUS_ACTIVE}
- and active in (role, 'Both')
+ -- and active in (role, 'Both')
+ and ( active = 'Both' or instr(active, role) > 0 )
and user_id = ?
and is_opposed
order by mtime
@@ -1801,7 +1911,7 @@ app.get("/create/:title_id", function (req, res) {
user: req.user,
title: title,
limit: req.user ? check_create_game_limit(req.user) : null,
- scenarios: RULES[title_id].scenarios,
+ rules: RULES[title_id],
})
})
@@ -1987,6 +2097,11 @@ app.get("/join/:game_id", function (req, res) {
if (!game)
return res.status(404).send("Invalid game ID.")
+ if (ENABLE_ARCHIVE) {
+ if (game.status === STATUS_ARCHIVED && game.moves >= game.player_count * 3)
+ game.status = STATUS_FINISHED
+ }
+
let roles = get_game_roles(game.title_id, game.scenario, game.options)
let players = SQL_SELECT_PLAYER_VIEW.all(game_id)
@@ -2189,13 +2304,11 @@ function start_game(game) {
state = RULES[game.title_id].setup(seed, game.scenario, options)
- SQL_START_GAME.run(state.active, game.game_id)
+ SQL_START_GAME.run(String(state.active), game.game_id)
let replay_id = put_replay(game.game_id, null, ".setup", [ seed, game.scenario, options ])
put_snap(game.game_id, replay_id, state)
SQL_INSERT_GAME_STATE.run(game.game_id, JSON.stringify(state))
- SQL_UPDATE_PLAYERS_INIT_TIME.run(game.game_id)
-
SQL_COMMIT.run()
} finally {
if (db.inTransaction)
@@ -2214,6 +2327,10 @@ app.get("/api/replay/:game_id", function (req, res) {
return res.status(404).send("Invalid game ID.")
if (game.status < STATUS_FINISHED && (!req.user || req.user.user_id !== 1))
return res.status(401).send("Not authorized to debug.")
+ if (ENABLE_ARCHIVE) {
+ if (game.status === STATUS_ARCHIVED)
+ return res.type("application/json").send(ARCHIVE_SELECT_REPLAY.get(game_id))
+ }
return res.type("application/json").send(SQL_SELECT_REPLAY.get(game_id))
})
@@ -2224,6 +2341,10 @@ app.get("/api/export/:game_id", function (req, res) {
return res.status(404).send("Invalid game ID.")
if (game.status < STATUS_FINISHED && (!req.user || req.user.user_id !== 1))
return res.status(401).send("Not authorized to debug.")
+ if (ENABLE_ARCHIVE) {
+ if (game.status === STATUS_ARCHIVED)
+ return res.type("application/json").send(ARCHIVE_SELECT_EXPORT.get(game_id))
+ }
return res.type("application/json").send(SQL_SELECT_EXPORT.get(game_id))
})
@@ -2240,7 +2361,7 @@ function rewind_game_to_snap(game_id, snap_id) {
SQL_DELETE_GAME_REPLAY.run(game_id, snap.replay_id)
SQL_INSERT_GAME_STATE.run(game_id, JSON.stringify(snap_state))
- SQL_REWIND_GAME.run(snap_id - 1, snap_state.active, game_id)
+ SQL_REWIND_GAME.run(snap_id - 1, String(snap_state.active), game_id)
SQL_REWIND_GAME_CLOCK.run(game_id)
update_join_clients(game_id)
@@ -2292,7 +2413,7 @@ const SQL_CLONE_1 = SQL(`
`).pluck()
const SQL_CLONE_2 = [
- SQL(`insert into players(game_id,role,user_id) select $new_game_id,role,user_id from players where game_id=$old_game_id`),
+ SQL(`insert into players(game_id,role,user_id,is_active) select $new_game_id,role,user_id,is_active from players where game_id=$old_game_id`),
SQL(`insert into game_state(game_id,state) select $new_game_id,state from game_state where game_id=$old_game_id`),
SQL(`insert into game_replay(game_id,replay_id,role,action,arguments) select $new_game_id,replay_id,role,action,arguments from game_replay where game_id=$old_game_id`),
SQL(`insert into game_snap(game_id,snap_id,replay_id,state) select $new_game_id,snap_id,replay_id,state from game_snap where game_id=$old_game_id`),
@@ -2336,8 +2457,8 @@ function is_winner(role, result) {
return (result === "Draw" || result === role || result.includes(role))
}
-function elo_k(a) {
- return a.count < 10 ? 60 : 30
+function elo_k(_) {
+ return 30
}
function elo_ev(a, players) {
@@ -2492,6 +2613,10 @@ function message_link(msg_id) {
return SITE_URL + "/message/read/" + msg_id
}
+function tour_pool_link(pool_id) {
+ return SITE_URL + "/tm/pool/" + pool_id
+}
+
function send_notification(user, link, message) {
if (WEBHOOKS) {
let webhook = SQL_SELECT_WEBHOOK_SEND.get(user.user_id)
@@ -2526,6 +2651,10 @@ function send_play_notification(user, game_id, message) {
send_notification(user, game_play_link(game_id, title_id, user), `${title_name} #${game_id} (${user.role}) - ${message}`)
}
+function send_tour_notification(user, pool_name, message) {
+ send_notification(user, tour_pool_link(pool_name), `${pool_name} - ${message}`)
+}
+
function send_chat_activity_notification(game_id, p) {
send_play_notification(p, game_id, "Chat activity")
}
@@ -2533,7 +2662,7 @@ function send_chat_activity_notification(game_id, p) {
function send_game_started_notification(game_id, active) {
let players = SQL_SELECT_PLAYERS.all(game_id)
for (let p of players) {
- let p_is_active = active === p.role || active === "Both"
+ let p_is_active = is_role_active(active, p.role)
if (p_is_active)
send_play_notification(p, game_id, "Started - Your turn")
else
@@ -2541,15 +2670,15 @@ function send_game_started_notification(game_id, active) {
}
}
-function send_your_turn_notification_to_offline_users(game_id, old_active, active) {
+function send_your_turn_notification_to_offline_users(game_id, old_active, new_active) {
// Only send notifications when the active player changes.
- if (old_active === active)
+ if (!is_changed_active(old_active, new_active))
return
let players = SQL_SELECT_PLAYERS.all(game_id)
for (let p of players) {
- let p_was_active = old_active === p.role || old_active === "Both"
- let p_is_active = active === p.role || active === "Both"
+ let p_was_active = is_role_active(old_active, p.role)
+ let p_is_active = is_role_active(new_active, p.role)
if (!p_was_active && p_is_active) {
if (!is_player_online(game_id, p.user_id))
send_play_notification(p, game_id, "Your turn")
@@ -2672,46 +2801,22 @@ setTimeout(purge_game_ticker, 89 * 1000)
* TIME CONTROL
*/
-const SQL_UPDATE_PLAYERS_INIT_TIME = SQL(`
- update players
- set clock = (
- case (select pace from games where games.game_id = players.game_id)
- when 1 then 1
- when 2 then 3
- when 3 then 3
- else 21
- end
- )
- where
- players.game_id = ?
-`)
-
-const SQL_UPDATE_PLAYERS_ADD_TIME = SQL(`
- update players
- set clock = (
- case (select pace from games where games.game_id = players.game_id)
- when 1 then min(clock + ${4 / 24}, 3)
- when 2 then min(clock + ${12 / 24}, 5)
- when 3 then min(clock + ${36 / 24}, 10)
- else 21
- end
- )
- where
- players.game_id = ? and players.role = ?
-`)
-
+// SQL_UPDATE_PLAYERS_INIT_TIME is handled by trigger
+// SQL_UPDATE_PLAYERS_ADD_TIME is handled by trigger
// SQL_UPDATE_PLAYERS_USE_TIME is handled by trigger
const SQL_SELECT_TIME_CONTROL = SQL("select * from time_control_view")
+const SQL_INSERT_TIMEOUT = SQL("insert into user_timeout (user_id, game_id) values (?, ?)")
+
function time_control_ticker() {
for (let item of SQL_SELECT_TIME_CONTROL.all()) {
if (item.is_opposed) {
console.log("TIMED OUT GAME:", item.game_id, item.role)
- do_resign(item.game_id, item.role, "timed out")
+ do_timeout(item.game_id, item.role, item.role + " timed out.")
+ SQL_INSERT_TIMEOUT.run(item.user_id, item.game_id)
if (item.is_match) {
console.log("BANNED FROM TOURNAMENTS:", item.user_id)
- TM_INSERT_TIMEOUT.run(item.user_id, item.game_id)
TM_INSERT_BANNED.run(item.user_id)
TM_DELETE_QUEUE_ALL.run(item.user_id)
}
@@ -2736,7 +2841,6 @@ const designs = require("./designs.js")
const TM_SELECT_BANNED = SQL("select exists ( select 1 from tm_banned where user_id=? )").pluck()
const TM_INSERT_BANNED = SQL("insert or ignore into tm_banned (user_id, time) values (?, datetime())")
-const TM_INSERT_TIMEOUT = SQL("insert into tm_timeout (user_id, game_id) values (?, ?)")
const TM_DELETE_QUEUE_ALL = SQL("delete from tm_queue where user_id=?")
@@ -2762,6 +2866,33 @@ const TM_MAY_JOIN_SEED = SQL(`
where seed_id=?
`).pluck()
+const TM_MAY_JOIN_SEED_LEVEL = SQL(`
+ with
+ win_cte as (
+ select
+ count(1) as n_win
+ from
+ tm_winners
+ join tm_pools using(pool_id)
+ where
+ level = @level - 1 and user_id = @user_id and seed_id = @seed_id
+ ),
+ play_cte as (
+ select
+ count(distinct pool_id) as n_play
+ from
+ tm_rounds
+ join tm_pools using(pool_id)
+ join players using(game_id)
+ where
+ level = @level and user_id = @user_id and seed_id = @seed_id
+ )
+ select
+ coalesce(n_win, 0) > coalesce(n_play, 0) as may_join
+ from
+ win_cte, play_cte
+`).pluck()
+
function is_banned_from_tournaments(user_id) {
return TM_SELECT_BANNED.get(user_id)
}
@@ -2774,6 +2905,14 @@ function may_join_seed(seed_id) {
return TM_MAY_JOIN_SEED.get(seed_id)
}
+function may_join_seed_level(user_id, seed_id, level) {
+ if (level === 1)
+ return true
+ if (level >= 2)
+ return TM_MAY_JOIN_SEED_LEVEL.get({ level, user_id, seed_id })
+ return false
+}
+
const TM_SEED_LIST_ALL = SQL(`
select
tm_seeds.*,
@@ -2810,8 +2949,6 @@ const TM_SEED_LIST_USER = SQL(`
order by seed_name
`)
-const TM_POOL_LIST_ACTIVE = SQL("select * from tm_pool_active_view")
-
const TM_POOL_LIST_USER_ACTIVE = SQL(`
select * from tm_pool_active_view
where not is_finished and pool_id in (
@@ -2862,9 +2999,11 @@ const TM_SELECT_QUEUE_BLACKLIST = SQL(`
with qq as (
select user_id from tm_queue_view where seed_id=? and level=?
)
- select me, you
+ select me, you, u_me.name as me_name, u_you.name as you_name
from contacts
join qq on qq.user_id = me
+ join users u_me on u_me.user_id=me
+ join users u_you on u_you.user_id=you
where relation < 0 and exists (select 1 from qq where user_id = you)
`)
@@ -2890,6 +3029,7 @@ const TM_SELECT_GAMES = SQL(`
tm_rounds.*,
games.status,
games.moves,
+ games.status > 1 and games.result = 'None' as is_abandoned,
json_group_object(role, coalesce(name, 'null')) as role_names,
json_group_object(role, score) as role_scores
from
@@ -2903,7 +3043,16 @@ const TM_SELECT_GAMES = SQL(`
game_id
`)
-const TM_SELECT_WINNERS = SQL("select user_id from tm_winners where pool_id = ?").pluck()
+const TM_SELECT_PLAYERS_IN_POOL = SQL(`
+ select
+ user_view.*
+ from
+ tm_rounds
+ join players using(game_id)
+ join user_view using(user_id)
+ group by
+ user_id
+`)
const TM_SELECT_PLAYERS_2P = SQL(`
with
@@ -3022,10 +3171,9 @@ const TM_FIND_NEXT_GAME_TO_START = SQL(`
const TM_SELECT_ENDED_POOLS = SQL(`
select
- pool_id, seed_id, level, pool_name, level_count
+ pool_id, pool_name
from
tm_pools
- join tm_seeds using(seed_id)
join tm_rounds using(pool_id)
join games using(game_id)
where
@@ -3053,10 +3201,7 @@ const TM_SELECT_SEED_READY_MINI_CUP = SQL(`
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")
- let active_pools = TM_POOL_LIST_ACTIVE.all()
- let pools_by_seed = object_group_by(active_pools, "seed_name")
- res.render("tm_list.pug", { user: req.user, seeds, seeds_by_title, active_pools, pools_by_seed })
+ res.render("tm_list.pug", { user: req.user, seeds })
})
app.get("/tm/seed/:seed_name", function (req, res) {
@@ -3103,11 +3248,12 @@ app.get("/tm/pool/:pool_name", function (req, res) {
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 })
+ res.render("tm_pool.pug", { user: req.user, seed, pool, roles, players, games, games_by_round })
})
-app.post("/api/tm/register/:seed_id", must_be_logged_in, function (req, res) {
+app.post("/api/tm/register/: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
if (is_banned_from_tournaments(req.user.user_id))
return res.status(401).send("You may not join any tournaments.")
@@ -3115,7 +3261,9 @@ app.post("/api/tm/register/:seed_id", must_be_logged_in, function (req, res) {
return res.status(401).send("You may not join any tournaments right now.")
if (!may_join_seed(seed_id))
return res.status(401).send("This tournament is closed.")
- TM_INSERT_QUEUE.run(user_id, seed_id, 1)
+ if (!may_join_seed_level(req.user.user_id, seed_id, level))
+ return res.status(401).send("You may not join this tournament.")
+ TM_INSERT_QUEUE.run(user_id, seed_id, level)
return res.redirect(req.headers.referer)
})
@@ -3261,7 +3409,7 @@ function make_concurrent_rounds(v, k, n) {
let rbibd = designs.resolvable_bibd(v, k)
if (rbibd)
- return rbibd.slice(0, n).flat()
+ return [ rbibd.slice(0, n).flat() ]
throw new Error("cannot create rounds for this configuration")
}
@@ -3374,6 +3522,7 @@ function start_tournament_seed_mc(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)
+ console.log("TM BLACKLIST", blacklist)
let players = filter_queue_through_blacklist(queue, seed.pool_size, blacklist)
if (!players) {
@@ -3416,23 +3565,15 @@ function start_tournament_seed(seed_id, level) {
}
function tm_reap_pools() {
- // reap pools that are finished (and promote winners)
+ // reap pools that are finished (and notify players)
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()
- }
+ console.log("TM POOL FINISHED", item.pool_name)
+ TM_UPDATE_POOL_FINISHED.run(item.pool_id)
+
+ let players = TM_SELECT_PLAYERS_IN_POOL.all(item.pool_id)
+ for (let user of players)
+ send_tour_notification(user, item.pool_name, "Finished")
}
}
@@ -3472,6 +3613,26 @@ if (app.locals.ENABLE_TOURNAMENTS) {
* GAME SERVER
*/
+function is_role_active(active, role) {
+ return active === role || active === "Both" || active.includes(role)
+}
+
+function is_nobody_active(active) {
+ return !active || active === "None"
+}
+
+function is_multi_active(active) {
+ if (!active)
+ return false
+ if (Array.isArray(active))
+ return true
+ return active === "Both" || active.includes(",")
+}
+
+function is_changed_active(old_active, new_active) {
+ return String(old_active) !== String(new_active)
+}
+
function is_player_online(game_id, user_id) {
if (game_clients[game_id])
for (let other of game_clients[game_id])
@@ -3493,13 +3654,14 @@ function send_state(socket, state) {
view.log_start = view.log.length
socket.seen = view.log.length
view.log = view.log.slice(view.log_start)
- if (state.state === "game_over")
- view.game_over = 1
let this_view = JSON.stringify(view)
if (view.actions || socket.last_view !== this_view) {
socket.send('["state",' + this_view + "," + game_cookies[socket.game_id] + "]")
socket.last_view = this_view
}
+ if (is_nobody_active(state.active)) {
+ socket.send('["finished"]')
+ }
} catch (err) {
console.log(err)
return send_message(socket, "error", err.toString())
@@ -3508,6 +3670,10 @@ function send_state(socket, state) {
function get_game_state(game_id) {
let game_state = SQL_SELECT_GAME_STATE.get(game_id)
+ if (ENABLE_ARCHIVE) {
+ if (!game_state)
+ game_state = ARCHIVE_SELECT_GAME_STATE.get(game_id)
+ }
if (!game_state)
throw new Error("No game with that ID")
return JSON.parse(game_state)
@@ -3539,9 +3705,11 @@ function put_replay(game_id, role, action, args) {
}
function dont_snap(rules, state, old_active) {
- if (state.active === old_active)
+ if (is_nobody_active(state.active))
return true
- if (state.state === "game_over")
+ if (is_multi_active(old_active) && is_multi_active(state.active))
+ return true
+ if (!is_changed_active(old_active, state.active))
return true
if (rules.dont_snap && rules.dont_snap(state))
return true
@@ -3555,19 +3723,14 @@ function put_snap(game_id, replay_id, state) {
send_message(other, "snapsize", snap_id)
}
-function put_game_state(game_id, state, old_active, current_role) {
+function put_game_state(game_id, state, old_active) {
// TODO: separate state, undo, and log entries (and reuse "snap" json stringifaction?)
-
SQL_INSERT_GAME_STATE.run(game_id, JSON.stringify(state))
- if (state.active !== old_active) {
- SQL_UPDATE_GAME_ACTIVE.run(state.active, game_id)
+ if (is_changed_active(old_active, state.active))
+ SQL_UPDATE_GAME_ACTIVE.run(String(state.active), game_id)
- // add time for the player who took the current action
- SQL_UPDATE_PLAYERS_ADD_TIME.run(game_id, current_role)
- }
-
- if (state.state === "game_over") {
+ if (is_nobody_active(state.active)) {
SQL_FINISH_GAME.run(state.result, game_id)
if (state.result && state.result !== "None")
update_elo_ratings(game_id)
@@ -3582,15 +3745,16 @@ function put_new_state(title_id, game_id, state, old_active, role, action, args)
if (!dont_snap(RULES[title_id], state, old_active))
put_snap(game_id, replay_id, state)
- put_game_state(game_id, state, old_active, role)
+ put_game_state(game_id, state, old_active)
- if (state.active !== old_active)
+ if (is_changed_active(old_active, state.active))
update_join_clients(game_id)
+
if (game_clients[game_id])
for (let other of game_clients[game_id])
send_state(other, state)
- if (state.state === "game_over")
+ if (is_nobody_active(state.active))
send_game_finished_notification_to_offline_users(game_id, state.result)
else
send_your_turn_notification_to_offline_users(game_id, old_active, state.active)
@@ -3616,11 +3780,11 @@ function on_action(socket, action, args, cookie) {
try {
let state = get_game_state(socket.game_id)
- let old_active = state.active
+ let old_active = String(state.active)
// Don't update cookie during simultaneous turns, as it results
// in many in-flight collisions.
- if (old_active !== "Both")
+ if (!is_multi_active(old_active))
game_cookies[socket.game_id] ++
state = RULES[socket.title_id].action(state, socket.role, action, args)
@@ -3634,73 +3798,51 @@ function on_action(socket, action, args, cookie) {
function on_resign(socket) {
SLOG(socket, "RESIGN")
try {
- do_resign(socket.game_id, socket.role, "resigned")
+ do_resign(socket.game_id, socket.role)
} catch (err) {
console.log(err)
return send_message(socket, "error", err.toString())
}
}
-function do_resign(game_id, role, how) {
+function do_timeout(game_id, role) {
let game = SQL_SELECT_GAME.get(game_id)
let state = get_game_state(game_id)
- let old_active = state.active
+ let old_active = String(state.active)
+ state = finish_game_state(game.title_id, state, "None", role + " timed out.")
+ put_new_state(game.title_id, game_id, state, old_active, role, ".timeout", null)
+}
- let result = "None"
+function do_resign(game_id, role) {
+ let game = SQL_SELECT_GAME.get(game_id)
+ let state = get_game_state(game_id)
+ let old_active = String(state.active)
- let roles = get_game_roles(game.title_id, game.scenario, game.options)
+ let result = "None"
if (game.player_count === 2) {
+ let roles = get_game_roles(game.title_id, game.scenario, game.options)
for (let r of roles)
if (r !== role)
result = r
- } else {
- result = roles.filter(r => r !== role).join(", ")
}
- state.state = "game_over"
- state.active = "None"
- state.result = result
- state.victory = role + " " + how + "."
- state.log.push("")
- state.log.push(state.victory)
+ state = finish_game_state(game.title_id, state, result, role + " resigned.")
- put_new_state(game.title_id, game_id, state, old_active, role, ".resign", null)
+ put_new_state(game.title_id, game_id, state, old_active, role, ".resign", result)
}
-function on_restore(socket, state_text) {
- if (!DEBUG)
- send_message(socket, "error", "Debugging is not enabled on this server.")
- SLOG(socket, "RESTORE")
- try {
- let state = JSON.parse(state_text)
-
- // reseed!
- state.seed = random_seed()
-
- // resend full log!
- for (let other of game_clients[socket.game_id])
- other.seen = 0
-
- put_new_state(socket.title_id, socket.game_id, state, null, null, "$restore", state)
- } catch (err) {
- console.log(err)
- return send_message(socket, "error", err.toString())
- }
-}
-
-function on_save(socket) {
- if (!DEBUG)
- send_message(socket, "error", "Debugging is not enabled on this server.")
- SLOG(socket, "SAVE")
- try {
- let game_state = SQL_SELECT_GAME_STATE.get(socket.game_id)
- if (!game_state)
- return send_message(socket, "error", "No game with that ID.")
- send_message(socket, "save", game_state)
- } catch (err) {
- console.log(err)
- return send_message(socket, "error", err.toString())
+function finish_game_state(title_id, state, result, message) {
+ if (typeof RULES[title_id].finish === "function") {
+ state = RULES[title_id].finish(state, result, message)
+ } else {
+ state.state = "game_over"
+ state.active = "None"
+ state.result = result
+ state.victory = message
+ state.log.push("")
+ state.log.push(message)
}
+ return state
}
function on_query(socket, q, params) {
@@ -3861,12 +4003,6 @@ function handle_player_message(socket, cmd, arg) {
case "querysnap":
on_query_snap(socket, arg[0], arg[1], arg[2])
break
- case "save":
- on_save(socket)
- break
- case "restore":
- on_restore(socket, arg)
- break
default:
send_message(socket, "error", "Invalid server command: " + cmd)
break
@@ -4080,12 +4216,12 @@ const SQL_USER_STATS = SQL(`
`)
const SQL_USER_RATINGS = SQL(`
- select title_name, rating, count, date(last) as last
+ select title_id, title_name, rating, count, date(last) as last
from ratings
join titles using(title_id)
where user_id = ?
- and count >= 5
- order by rating desc
+ and count >= 3
+ order by count desc
`)
const SQL_GAME_RATINGS = SQL(`
diff --git a/tools/elo.js b/tools/elo.js
index ad0dbad..f6064f1 100644
--- a/tools/elo.js
+++ b/tools/elo.js
@@ -14,7 +14,7 @@ function is_winner(role, result) {
}
function elo_k(n) {
- return n < 10 ? 60 : 30
+ return 30
}
function elo_ev(a, players) {
diff --git a/tools/import-game.js b/tools/import-game.js
index 59b26a7..42391e1 100755
--- a/tools/import-game.js
+++ b/tools/import-game.js
@@ -40,8 +40,9 @@ for (let file of input) {
let db = new sqlite3("db")
let insert_game = db.prepare("insert into games(status,owner_id,title_id,scenario,options,player_count,active,moves,notice) values (1,1,:title_id,:scenario,:options,:player_count,:active,:moves,:notice) returning game_id").pluck()
- let insert_player = db.prepare("insert into players(game_id,role,user_id) values (?,?,?)")
+ let insert_player = db.prepare("insert into players(game_id,role,user_id,clock) values (?,?,?,21)")
let insert_state = db.prepare("insert into game_state(game_id,state) values (?,?)")
+ let update_active_trigger = db.prepare("update games set active=active where game_id=?")
let select_user = db.prepare("select user_id from users where name=?").pluck()
@@ -57,6 +58,7 @@ for (let file of input) {
for (let p of game.players)
insert_player.run(game_id, p.role, find_user(p.name))
insert_state.run(game_id, JSON.stringify(game.state))
+ update_active_trigger.run(game_id)
if (game.replay) {
let insert_replay = db.prepare("insert into game_replay(game_id,replay_id,role,action,arguments) values (?,?,?,?,?)")
diff --git a/tools/lift-bans.sh b/tools/lift-bans.sh
new file mode 100644
index 0000000..e76f621
--- /dev/null
+++ b/tools/lift-bans.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+sqlite3 db <<EOF
+
+begin immediate;
+
+.mode column
+
+create temporary view tm_lift_ban_view as
+ select
+ user_id,
+ name,
+ date(timeout_last),
+ timeout_total,
+ games_since_timeout,
+ (games_since_timeout > timeout_total) and (julianday() > julianday(timeout_last)+14) as lift_ban
+ from
+ user_profile_view
+ where
+ user_id in (select user_id from tm_banned)
+ order by lift_ban desc, timeout_last asc
+;
+
+select * from tm_lift_ban_view;
+
+delete from tm_banned where user_id in (select user_id from tm_lift_ban_view where lift_ban) returning user_id;
+
+commit;
+
+EOF
diff --git a/tools/patchgame.js b/tools/patchgame.js
index fef07a0..de8721e 100755
--- a/tools/patchgame.js
+++ b/tools/patchgame.js
@@ -71,10 +71,30 @@ function snapshot(state) {
return snap
}
+function is_role_active(active, role) {
+ return active === role || active === "Both" || active.includes(role)
+}
+
+function is_nobody_active(active) {
+ return !active || active === "None"
+}
+
+function is_multi_active(active) {
+ if (!active)
+ return false
+ if (Array.isArray(active))
+ return true
+ return active === "Both" || active.includes(",")
+}
+
+function is_changed_active(old_active, new_active) {
+ return String(old_active) !== String(new_active)
+}
+
function is_valid_action(rules, state, role, action, arg) {
if (action === "undo") // for jc, hots, r3, and cr compatibility
return true
- if (state.active !== role && state.active !== "Both")
+ if (!is_role_active(state.active, role))
return false
let view = rules.view(state, role)
let va = view.actions[action]
@@ -88,15 +108,45 @@ function is_valid_action(rules, state, role, action, arg) {
}
function dont_snap(rules, state, old_active) {
- if (state.state === "game_over")
+ if (is_nobody_active(state.active))
return true
- if (state.active === old_active)
+ if (is_multi_active(old_active) && is_multi_active(state.active))
+ return true
+ if (!is_changed_active(old_active, state.active))
return true
if (rules.dont_snap && rules.dont_snap(state))
return true
return false
}
+function get_game_roles(rules, scenario, options) {
+ let roles = rules.roles
+ if (typeof roles === "function") {
+ if (typeof options === "string")
+ options = JSON.parse(options)
+ return roles(scenario, options)
+ }
+ return roles
+}
+
+function get_resign_result(roles, role) {
+ return roles.filter(r => r !== role).join(", ")
+}
+
+function finish_game(rules, state, result, message) {
+ if (typeof rules.finish === "function") {
+ state = RULES[title_id].finish(state, result, message)
+ } else {
+ state.state = "game_over"
+ state.active = "None"
+ state.result = result
+ state.victory = message
+ state.log.push("")
+ state.log.push(message)
+ }
+ return state
+}
+
function patch_game(game_id, {validate_actions=true, save_snaps=true, delete_undo=false, delete_invalid=false}, verbose) {
let game = select_game.get(game_id)
if (!game) {
@@ -106,6 +156,7 @@ function patch_game(game_id, {validate_actions=true, save_snaps=true, delete_und
let title_id = game.title_id
let rules = require("../public/" + title_id + "/rules.js")
+ let roles = get_game_roles(rules, game.scenario, game.options)
let replay = select_replay.all(game_id)
if (replay.length === 0)
@@ -129,8 +180,13 @@ function patch_game(game_id, {validate_actions=true, save_snaps=true, delete_und
case ".setup":
state = rules.setup(...args)
break
+ case ".timeout":
+ finish_game(rules, state, "None", item.role + " timed out.")
+ break
+ case ".abandon":
+ finish_game(rules, state, "None", item.role + " abandoned the game.")
case ".resign":
- state = rules.resign(state, item.role)
+ finish_game(rules, state, get_resign_result(roles, item.role), item.role + " resigned.")
break
default:
if (validate_actions) {
@@ -185,7 +241,7 @@ function patch_game(game_id, {validate_actions=true, save_snaps=true, delete_und
insert_snap.run(game_id, ++snap_id, item.replay_id, item.state)
}
- update_active.run(state.active, game_id)
+ update_active.run(String(state.active), game_id)
update_state.run(JSON.stringify(state), game_id)
if (state.state === "game_over")
diff --git a/tools/purge.sql b/tools/purge.sql
index 3e0c898..2a95f01 100644
--- a/tools/purge.sql
+++ b/tools/purge.sql
@@ -2,6 +2,8 @@
attach database 'db' as live;
+pragma live.busy_timeout=10000;
+
create temporary view prune_snap_list as
select
distinct game_id
@@ -26,12 +28,8 @@ create temporary view prune_all_list as
)
;
-begin;
-
select 'PURGE SNAPS FROM ' || count(1) from prune_snap_list;
delete from live.game_snap where game_id in (select game_id from prune_snap_list);
select 'PURGE ALL FROM ' || count(1) from prune_all_list;
update live.games set status = 3 where game_id in (select game_id from prune_all_list);
-
-commit;
diff --git a/views/create-index.pug b/views/create-index.pug
index d038810..c508be7 100644
--- a/views/create-index.pug
+++ b/views/create-index.pug
@@ -3,7 +3,7 @@ doctype html
html
head
include head
- title= SITE_NAME
+ title Create game
body
include header
article
diff --git a/views/create.pug b/views/create.pug
index d45903e..c4c5c2a 100644
--- a/views/create.pug
+++ b/views/create.pug
@@ -19,27 +19,36 @@ html
p.error You are not logged in!
form(method="post" action="/create/"+title.title_id)
- if Array.isArray(scenarios)
- if scenarios.length > 1
+ if Array.isArray(rules.scenarios)
+ if rules.scenarios.length > 1
p Scenario:
br
select(name="scenario")
- each scenario in scenarios
- option(value=scenario)= scenario
+ each scenario in rules.scenarios
+ if scenario === rules.default_scenario
+ option(value=scenario selected)= scenario
+ else
+ option(value=scenario)= scenario
else
- input(type="hidden" name="scenario" value=scenarios[0])
+ input(type="hidden" name="scenario" value=rules.scenarios[0])
else
p Scenario:
br
select(name="scenario")
- each list, name in scenarios
+ each list, name in rules.scenarios
if name === ""
each scenario in list
- option(value=scenario)= scenario
+ if scenario === rules.default_scenario
+ option(value=scenario selected)= scenario
+ else
+ option(value=scenario)= scenario
else
optgroup(label=name)
each scenario in list
- option(value=scenario)= scenario
+ if scenario === rules.default_scenario
+ option(value=scenario selected)= scenario
+ else
+ option(value=scenario)= scenario
| !{ title.create_html }
diff --git a/views/games_finished.pug b/views/games_finished.pug
index 8d71717..90ec9a2 100644
--- a/views/games_finished.pug
+++ b/views/games_finished.pug
@@ -3,7 +3,10 @@ doctype html
html
head
include head
- title= SITE_NAME
+ if user && user.user_id === who.user_id
+ title Your finished games
+ else
+ title #{who.name}&rsquo;s finished games
body
include header
article.wide
diff --git a/views/games_public.pug b/views/games_public.pug
index 54591cd..8cfd541 100644
--- a/views/games_public.pug
+++ b/views/games_public.pug
@@ -3,7 +3,7 @@ doctype html
html
head
include head
- title= SITE_NAME
+ title Public room
if user
meta(http-equiv="refresh" content=900)
body
diff --git a/views/head.pug b/views/head.pug
index 61ca688..75e1135 100644
--- a/views/head.pug
+++ b/views/head.pug
@@ -109,7 +109,16 @@ mixin gamelist(list,hide_title=0)
else
a(class="command" href=`/${item.title_id}/play.html?game=${item.game_id}`) Review
when 3
- | Archived
+ if ENABLE_ARCHIVE
+ if item.is_yours
+ if item.your_role
+ a(class="command" href=`/${item.title_id}/play.html?game=${item.game_id}&role=${encodeURIComponent(item.your_role)}`) Archived
+ else
+ a(class="command" href="/join/"+item.game_id) Archived
+ else
+ a(class="command" href=`/${item.title_id}/play.html?game=${item.game_id}&role=Observer`) Archived
+ else
+ | Archived
div.game_main
div.game_info
diff --git a/views/profile.pug b/views/profile.pug
index 391fca0..4ecf289 100644
--- a/views/profile.pug
+++ b/views/profile.pug
@@ -3,13 +3,13 @@ doctype html
html
head
include head
- title= SITE_NAME
+ title Profile
body
include header
article
h1= SITE_NAME
- p Welcome, #{user.name}!
+ p Welcome, <a class="black" href="/user/#{user.name}">#{user.name}</a>!
p Your mail address is #{user.mail}
if ENABLE_MAIL
diff --git a/views/tm_active.pug b/views/tm_active.pug
index cc821b2..625eac2 100644
--- a/views/tm_active.pug
+++ b/views/tm_active.pug
@@ -3,11 +3,11 @@ doctype html
html
head
include head
- title= SITE_NAME
+ title Your tournaments
body
include header
article.wide
- h1 Your Tournaments
+ h1 Your tournaments
if seeds && seeds.length > 0
+seedlist(seeds, "Registrations")
diff --git a/views/tm_finished.pug b/views/tm_finished.pug
index efe193d..5d077c6 100644
--- a/views/tm_finished.pug
+++ b/views/tm_finished.pug
@@ -3,7 +3,10 @@ doctype html
html
head
include head
- title= SITE_NAME
+ if user && user.user_id === who.user_id
+ title Your finished tournaments
+ else
+ title #{who.name}&rsquo;s finished tournaments
body
include header
article.wide
diff --git a/views/tm_list.pug b/views/tm_list.pug
index bc7fd83..f5efa26 100644
--- a/views/tm_list.pug
+++ b/views/tm_list.pug
@@ -12,7 +12,3 @@ html
p See <a href="/docs/tournaments.html">tournament information</a>.
+seedlist(seeds, "Mini Cup")
-
- h2 Active
- each pools, seed_name in pools_by_seed
- +poollist(pools, `<a href="/tm/seed/${seed_name}">${seed_name}</a>`)
diff --git a/views/tm_pool.pug b/views/tm_pool.pug
index 1690535..e6c7715 100644
--- a/views/tm_pool.pug
+++ b/views/tm_pool.pug
@@ -49,9 +49,9 @@ html
tr
td Started
td= human_date(pool.start_date)
- tr
- td Finished
- if pool.finish_date
+ if pool.finish_date
+ tr
+ td Finished
td= human_date(pool.finish_date)
if seed.player_count === 2
@@ -84,7 +84,10 @@ html
if ix > 0
| &nbsp;
if gs[1] === null
- a.black(href="/join/" + gs[0]) &minus;
+ if games.find(game => game.game_id === gs[0]).is_abandoned
+ a.black(href="/join/" + gs[0]) &#xd7;
+ else
+ a.black(href="/join/" + gs[0]) &minus;
else
a.black(href="/join/" + gs[0])= gs[1]
td.r= row.points
@@ -116,7 +119,10 @@ html
each gs in result
td.c
if gs[1] === null
- a.black(href="/join/" + gs[0]) &minus;
+ if games.find(game => game.game_id === gs[0]).is_abandoned
+ a.black(href="/join/" + gs[0]) &#xd7;
+ else
+ a.black(href="/join/" + gs[0]) &minus;
else
a.black(href="/join/" + gs[0])= gs[1]
td.r= row.points
@@ -150,10 +156,16 @@ html
if game.status > 1
td.w.r
- each role, ix in roles
- if ix > 0
- | &nbsp;:&nbsp;
- | #{role_scores[role]}
+ if game.is_abandoned
+ | None
+ else
+ each role, ix in roles
+ if ix > 0
+ | &nbsp;:&nbsp;
+ if role_scores[role] === null
+ | &#xd7;
+ else
+ | #{role_scores[role]}
else
td.r
if game.status > 0
diff --git a/views/tm_seed.pug b/views/tm_seed.pug
index 9902f4f..ea31a41 100644
--- a/views/tm_seed.pug
+++ b/views/tm_seed.pug
@@ -70,8 +70,8 @@ html
form(method="post" action="/api/tm/withdraw/" + seed.seed_id + "/" + (ix+1))
button(disabled) Register
button(type="submit") Withdraw
- else if ix === 0 && may_register
- form(method="post" action="/api/tm/register/" + seed.seed_id)
+ else if may_register && may_join_seed_level(user.user_id, seed.seed_id, ix+1)
+ form(method="post" action="/api/tm/register/" + seed.seed_id + "/" + (ix+1))
button(type="submit") Register
button(disabled) Withdraw
else
diff --git a/views/user.pug b/views/user.pug
index fc55b42..c3e8925 100644
--- a/views/user.pug
+++ b/views/user.pug
@@ -42,6 +42,38 @@ html
br
a(href="/contacts/add-enemy/"+who.name) Add to blacklist
+ if (who.move_time_mean !== null)
+ h3 Response time
+ div Average response time: #{format_minutes(who.move_time_mean)}
+ if (who.move_time_q2 !== null)
+ div Median response time: #{format_minutes(who.move_time_q2)}
+ if (who.move_time_q1 !== null && who.move_time_q2 !== null)
+ div Middle half of response times: #{format_minutes(who.move_time_q1)} to #{format_minutes(who.move_time_q3)}
+
+ h3 Timeouts
+ div Total number of timeouts: #{who.timeout_total}
+ div Games completed since last timeout: #{who.games_since_timeout}
+
+ if ratings.length > 0
+ h3 Most played games
+ table
+ thead
+ tr
+ th Title
+ th Count
+ th Last played
+ if user && user.user_id === 1
+ th Elo
+ tbody
+ each row in ratings
+ tr
+ td
+ a.black(href="/" + row.title_id)= row.title_name
+ td.r= row.count
+ td.r= row.last
+ if user && user.user_id === 1
+ td.r= row.rating
+
+tourlist(null, active_pools, finished_pools)
if open_games.length > 0