From 758043c4275498c94eeed26213536052349dd449 Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Sat, 1 Jul 2023 15:02:17 +0200 Subject: 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. --- tools/patchgame.js | 263 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 203 insertions(+), 60 deletions(-) mode change 100644 => 100755 tools/patchgame.js (limited to 'tools/patchgame.js') diff --git a/tools/patchgame.js b/tools/patchgame.js old mode 100644 new mode 100755 index 42b6c57..9751d26 --- 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 \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 '{options}'\n") + process.stderr.write(" or: ./tools/patchgame.js '{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) -- cgit v1.2.3