diff options
author | Tor Andersson <tor@ccxvii.net> | 2023-07-01 15:02:17 +0200 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2023-07-01 15:02:17 +0200 |
commit | 758043c4275498c94eeed26213536052349dd449 (patch) | |
tree | 7f0364552138b2fd148b4140f0faeb10b2681612 /tools/patchgame.js | |
parent | a07c8e521a06ec7228edba8ca65c6ec9b81a1f7e (diff) | |
download | server-758043c4275498c94eeed26213536052349dd449.tar.gz |
Add "snapshot" replay view during play.
Snapshots store game state without undo and only log length.
Combined with the final game state's log we can recreate the view
from any snapshot quickly.
Move replay code into separate script file, loaded only when used.
Prefix system "setup", "resign", and "restore" actions with a period.
Diffstat (limited to 'tools/patchgame.js')
-rwxr-xr-x[-rw-r--r--] | tools/patchgame.js | 263 |
1 files changed, 203 insertions, 60 deletions
diff --git a/tools/patchgame.js b/tools/patchgame.js index 42b6c57..9751d26 100644..100755 --- a/tools/patchgame.js +++ b/tools/patchgame.js @@ -1,76 +1,219 @@ -#!/usr/bin/env node +#!/usr/bin/env -S node -const VERIFY = true +const sqlite3 = require("better-sqlite3") -const fs = require('fs') -const sqlite3 = require('better-sqlite3') +let db = new sqlite3("db") -if (process.argv.length !== 3) { - process.stderr.write("usage: ./tools/patchgame.js <game_id>\n") - process.exit(1) -} +let select_game = db.prepare("select * from games where game_id=?") -let db = new sqlite3("./db") +let select_replay = db.prepare("select * from game_replay where game_id=?") +let delete_replay = db.prepare("delete from game_replay where game_id=?") +let insert_replay = db.prepare("insert into game_replay (game_id,replay_id,role,action,arguments) values (?,?,?,?,?)") -let game_id = process.argv[2] -let title_id = db.prepare("select title_id from games where game_id=?").pluck().get(game_id) -let rules = require("../public/" + title_id + "/rules.js") -let log = db.prepare("select * from game_replay where game_id=?").all(game_id) +let delete_snap = db.prepare("delete from game_snap where game_id=?") +let insert_snap = db.prepare("insert into game_snap(game_id,snap_id,state) values (?,?,?)") -let save = db.prepare("select state from game_state where game_id=?").pluck().get(game_id) -fs.writeFileSync("backup-" + game_id + ".txt", save) +let update_state = db.prepare("update game_state set active=?, state=? where game_id=?") -function is_valid_action(rules, game, role, action, a) { - if (action !== 'undo') - if (game.active !== role && game.active !== "Both" && game.active !== "All") - return false - let view = rules.view(game, role) - let va = view.actions[action] - if (va === undefined) +const CRC32C_TABLE = new Int32Array([ + 0x00000000, 0xf26b8303, 0xe13b70f7, 0x1350f3f4, 0xc79a971f, 0x35f1141c, 0x26a1e7e8, 0xd4ca64eb, + 0x8ad958cf, 0x78b2dbcc, 0x6be22838, 0x9989ab3b, 0x4d43cfd0, 0xbf284cd3, 0xac78bf27, 0x5e133c24, + 0x105ec76f, 0xe235446c, 0xf165b798, 0x030e349b, 0xd7c45070, 0x25afd373, 0x36ff2087, 0xc494a384, + 0x9a879fa0, 0x68ec1ca3, 0x7bbcef57, 0x89d76c54, 0x5d1d08bf, 0xaf768bbc, 0xbc267848, 0x4e4dfb4b, + 0x20bd8ede, 0xd2d60ddd, 0xc186fe29, 0x33ed7d2a, 0xe72719c1, 0x154c9ac2, 0x061c6936, 0xf477ea35, + 0xaa64d611, 0x580f5512, 0x4b5fa6e6, 0xb93425e5, 0x6dfe410e, 0x9f95c20d, 0x8cc531f9, 0x7eaeb2fa, + 0x30e349b1, 0xc288cab2, 0xd1d83946, 0x23b3ba45, 0xf779deae, 0x05125dad, 0x1642ae59, 0xe4292d5a, + 0xba3a117e, 0x4851927d, 0x5b016189, 0xa96ae28a, 0x7da08661, 0x8fcb0562, 0x9c9bf696, 0x6ef07595, + 0x417b1dbc, 0xb3109ebf, 0xa0406d4b, 0x522bee48, 0x86e18aa3, 0x748a09a0, 0x67dafa54, 0x95b17957, + 0xcba24573, 0x39c9c670, 0x2a993584, 0xd8f2b687, 0x0c38d26c, 0xfe53516f, 0xed03a29b, 0x1f682198, + 0x5125dad3, 0xa34e59d0, 0xb01eaa24, 0x42752927, 0x96bf4dcc, 0x64d4cecf, 0x77843d3b, 0x85efbe38, + 0xdbfc821c, 0x2997011f, 0x3ac7f2eb, 0xc8ac71e8, 0x1c661503, 0xee0d9600, 0xfd5d65f4, 0x0f36e6f7, + 0x61c69362, 0x93ad1061, 0x80fde395, 0x72966096, 0xa65c047d, 0x5437877e, 0x4767748a, 0xb50cf789, + 0xeb1fcbad, 0x197448ae, 0x0a24bb5a, 0xf84f3859, 0x2c855cb2, 0xdeeedfb1, 0xcdbe2c45, 0x3fd5af46, + 0x7198540d, 0x83f3d70e, 0x90a324fa, 0x62c8a7f9, 0xb602c312, 0x44694011, 0x5739b3e5, 0xa55230e6, + 0xfb410cc2, 0x092a8fc1, 0x1a7a7c35, 0xe811ff36, 0x3cdb9bdd, 0xceb018de, 0xdde0eb2a, 0x2f8b6829, + 0x82f63b78, 0x709db87b, 0x63cd4b8f, 0x91a6c88c, 0x456cac67, 0xb7072f64, 0xa457dc90, 0x563c5f93, + 0x082f63b7, 0xfa44e0b4, 0xe9141340, 0x1b7f9043, 0xcfb5f4a8, 0x3dde77ab, 0x2e8e845f, 0xdce5075c, + 0x92a8fc17, 0x60c37f14, 0x73938ce0, 0x81f80fe3, 0x55326b08, 0xa759e80b, 0xb4091bff, 0x466298fc, + 0x1871a4d8, 0xea1a27db, 0xf94ad42f, 0x0b21572c, 0xdfeb33c7, 0x2d80b0c4, 0x3ed04330, 0xccbbc033, + 0xa24bb5a6, 0x502036a5, 0x4370c551, 0xb11b4652, 0x65d122b9, 0x97baa1ba, 0x84ea524e, 0x7681d14d, + 0x2892ed69, 0xdaf96e6a, 0xc9a99d9e, 0x3bc21e9d, 0xef087a76, 0x1d63f975, 0x0e330a81, 0xfc588982, + 0xb21572c9, 0x407ef1ca, 0x532e023e, 0xa145813d, 0x758fe5d6, 0x87e466d5, 0x94b49521, 0x66df1622, + 0x38cc2a06, 0xcaa7a905, 0xd9f75af1, 0x2b9cd9f2, 0xff56bd19, 0x0d3d3e1a, 0x1e6dcdee, 0xec064eed, + 0xc38d26c4, 0x31e6a5c7, 0x22b65633, 0xd0ddd530, 0x0417b1db, 0xf67c32d8, 0xe52cc12c, 0x1747422f, + 0x49547e0b, 0xbb3ffd08, 0xa86f0efc, 0x5a048dff, 0x8ecee914, 0x7ca56a17, 0x6ff599e3, 0x9d9e1ae0, + 0xd3d3e1ab, 0x21b862a8, 0x32e8915c, 0xc083125f, 0x144976b4, 0xe622f5b7, 0xf5720643, 0x07198540, + 0x590ab964, 0xab613a67, 0xb831c993, 0x4a5a4a90, 0x9e902e7b, 0x6cfbad78, 0x7fab5e8c, 0x8dc0dd8f, + 0xe330a81a, 0x115b2b19, 0x020bd8ed, 0xf0605bee, 0x24aa3f05, 0xd6c1bc06, 0xc5914ff2, 0x37faccf1, + 0x69e9f0d5, 0x9b8273d6, 0x88d28022, 0x7ab90321, 0xae7367ca, 0x5c18e4c9, 0x4f48173d, 0xbd23943e, + 0xf36e6f75, 0x0105ec76, 0x12551f82, 0xe03e9c81, 0x34f4f86a, 0xc69f7b69, 0xd5cf889d, 0x27a40b9e, + 0x79b737ba, 0x8bdcb4b9, 0x988c474d, 0x6ae7c44e, 0xbe2da0a5, 0x4c4623a6, 0x5f16d052, 0xad7d5351 +]) + +function crc32c(data) { + let x = 0 + for (let i = 0, n = data.length; i < n; ++i) + x = CRC32C_TABLE[(x ^ data.charCodeAt(i)) & 0xff] ^ (x >>> 8) + return x ^ -1 +} + +function snapshot(state) { + let save_undo = state.undo + let save_log = state.log + state.undo = undefined + state.log = save_log.length + let snap = JSON.stringify(state) + state.undo = save_undo + state.log = save_log + return snap +} + +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") return false - if (a === undefined || a === null) - return (va === 1) || (typeof va === 'string') - if (Array.isArray(a)) - a = a[0] - if (!Array.isArray(va)) - throw new Error("action list not array:" + JSON.stringify(view.actions)) - return va.includes(a) + let view = rules.view(state, role) + let va = view.actions[action] + if (va) { + if (Array.isArray(arg)) + arg = arg[0] + if (arg === undefined || arg === null) + return (va === 1 || va === true || typeof va === "string") + if (Array.isArray(va) && va.includes(arg)) + return true + } + return false } -let game = { state: null, active: null } -let view = null -let i = 0 -try { - log.forEach(item => { - let args = JSON.parse(item.arguments) - if (item.action === 'setup') - game = rules.setup(args[0], args[1], args[2]) - else if (item.action === 'resign') - game = rules.resign(game, item.role) - else { - console.log("ACTION", i, game.state, game.active, ">", item.role, item.action, item.arguments) - if (VERIFY) { - if (!is_valid_action(rules, game, item.role, item.action, args)) { - console.log(`invalid action: ${item.role} ${item.action} ${item.arguments}`) - console.log("\t", game.state, game.active, JSON.stringify(rules.view(game, item.role).actions)) - throw "invalid action" +function patch_game(game_id, {validate_actions=true, save_snaps=true, delete_undo=true, delete_invalid=false}, verbose) { + let game = select_game.get(game_id) + if (!game) { + console.error("game not found:", game_id) + return + } + + let title_id = game.title_id + let rules = require("../public/" + title_id + "/rules.js") + + let replay = select_replay.all(game_id) + if (replay.length === 0) + return + + console.log("processing", game_id, title_id) + + try { + let state = null + let old_active = null + let need_to_rewrite = false + + for (let i = 0; i < replay.length; ++i) { + let item = replay[i] + + if (verbose) + console.log(item.replay_id, item.role, item.action, item.arguments) + + let args = JSON.parse(item.arguments) + switch (item.action) { + case ".setup": + state = rules.setup(...args) + break + case ".resign": + state = rules.resign(state, item.role) + break + default: + if (validate_actions) { + if (!is_valid_action(rules, state, item.role, item.action, args)) { + console.error(`invalid action: ${item.role} ${item.action} ${item.arguments}`) + console.error("\t", state.state, state.active, JSON.stringify(rules.view(state, item.role).actions)) + if (i < replay.length) { + console.log("BROKEN ENTRIES: %d", replay.length-i) + console.log(`sqlite3 db "delete from game_replay where game_id=${game_id} and replay_id>=${replay[i].replay_id}"`) + } + throw "invalid action" + } + } + state = rules.action(state, item.role, item.action, args) + break + } + + item.state = snapshot(state) + item.checksum = crc32c(item.state) + if (old_active !== state.active) + item.save = 1 + old_active = state.active + + if (delete_undo) { + if (item.action === "undo") { + for (let k = i-1; k >= 0; --k) { + if (replay[k].checksum === item.checksum) { + need_to_rewrite = true + for (let z = k+1; z <= i; ++z) + replay[z].remove = 1 + break + } + } } } - game = rules.action(game, item.role, item.action, args) } - ++i - }) - console.log("SUCCESS %d", log.length) - db.prepare("update game_state set active=?, state=? where game_id=?").run(game.active, JSON.stringify(game), game_id) -} catch (err) { - console.log("FAILED %d/%d", i+1, log.length) - console.log(err) - delete game.log - delete game.undo - console.log(game) + + db.exec("begin") + + if (need_to_rewrite) { + delete_replay.run(game_id) + let replay_id = 0 + for (item of replay) + if (!item.remove) + insert_replay.run(game_id, ++replay_id, item.role, item.action, item.arguments) + } + + if (save_snaps) { + delete_snap.run(game_id) + let snap_id = 0 + for (item of replay) + if (item.save) + insert_snap.run(game_id, ++snap_id, item.state) + } + + update_state.run(state.active, JSON.stringify(state), game_id) + + db.exec("commit") + + } catch (err) { + if (err !== "invalid action") + console.error("ERROR", game_id, title_id, err.message) + if (delete_invalid) { + delete_replay.run(game_id) + delete_snap.run(game_id) + } + } +} + +function patch_all(options) { + for (let game_id of db.prepare("select game_id from games where status=1").pluck().all()) + patch_game(game_id, options, false) } -if (i < log.length) { - console.log("BROKEN ENTRIES: %d", log.length-i) - console.log(`sqlite3 db "delete from game_replay where game_id=${game_id} and replay_id>=${log[i].replay_id}"`) +function patch_title(title_id, options) { + for (let game_id of db.prepare("select game_id from games where status=1 and title_id=?").pluck().all(title_id)) + patch_game(game_id, options, false) } + +if (process.argv.length < 3) { + process.stderr.write("usage: ./tools/patchgame.js <game_id> '{options}'\n") + process.stderr.write(" or: ./tools/patchgame.js <title_id> '{options}'\n") + process.stderr.write(" or: ./tools/patchgame.js all '{options}'\n") + process.stderr.write('options: { "validate_actions":true, "delete_invalid":false, "save_snaps":true, "delete_undo":true }\n') + process.exit(1) +} + +let options = {} +if (process.argv.length === 4) + options = JSON.parse(process.argv[3]) + +if (process.argv[2] === 'all') + patch_all(options) +else if (isNaN(process.argv[2])) + patch_title(process.argv[2], options) +else + patch_game(parseInt(process.argv[2]), options, true) |