diff --git a/package.json b/package.json index 24d61a3c..3b49a288 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ptn-ninja", - "version": "3.4.19", + "version": "3.4.20", "description": "An editor and viewer for Portable Tak Notation", "productName": "PTN Ninja", "author": "Craig Laparo ", diff --git a/readme.md b/readme.md index 0de6e4b3..b5f157df 100644 --- a/readme.md +++ b/readme.md @@ -250,7 +250,7 @@ The structure of the URL is as follows: `https://ptn.ninja/&=&=[...]` -To get a shortened URL, send a POST request to `https://url.ptn.ninja/short` with request body `{ ptn, params (optional) }` where `ptn` is a string, and `params` is an optional object containing any of the parameters below. If the request is valid, you'll receive the complete shortenend URL as plain text in response. +To get a shortened URL, send a POST request to `https://url.ptn.ninja/short` with request body `{ ptn, params (optional) }` where `ptn` is a string, and `params` is an optional object containing any of the parameters below. If the request is valid, you'll receive the complete shortenend URL as plain text in response. If the URL is not accessed within 30 days, it will be deleted. ### URL Parameters diff --git a/src/Game/PTN/Tag.js b/src/Game/PTN/Tag.js index ea85d928..82018bbd 100644 --- a/src/Game/PTN/Tag.js +++ b/src/Game/PTN/Tag.js @@ -36,8 +36,7 @@ export const formats = { flats: /^\d+$/, flats1: /^\d+$/, flats2: /^\d+$/, - clock: - /^\d+min(\+\d+sec)$|^((((\d\s+)?\d\d?:)?\d\d?:)?\d\d?\s*)?(\+(((\d\s+)?\d\d?:)?\d\d?:)?\d\d?)?$/, + clock: /^[^"]+$/, date: /^\d{4}\.\d\d?\.\d\d?$/, event: /^[^"]+$/, komi: /^-?\d*(\.5)?$/, diff --git a/src/Game/base.js b/src/Game/base.js index a7b43167..c31a9b2d 100644 --- a/src/Game/base.js +++ b/src/Game/base.js @@ -78,6 +78,14 @@ export default class GameBase { return params; } + // Split multi-game PTN into separate games + static split(ptn) { + return ptn.match( + /^\s*(\[\w+\s"[^"]*"\]\s*)+((\{[^}]*\})*[/.\s\da-hCFRS!?"'><+-]+)+/gm + ); + } + + // #region Init init({ ptn, name, @@ -145,8 +153,10 @@ export default class GameBase { this.notes = {}; this.warnings = []; - // Parse HEAD + //#region Parse HEAD + try { + // Parse tags from PTN if (ptn) { let item, key; @@ -174,6 +184,7 @@ export default class GameBase { } } + // Parse tags from JSON if (tags) { each(tags, (value, key) => { if (value) { @@ -195,6 +206,7 @@ export default class GameBase { }); } + // Parse datetime if (this.tags.date) { this.datetime = Tag.toDate( this.tags.date.value, @@ -204,17 +216,21 @@ export default class GameBase { this.datetime = new Date(); } + // Get size if (this.tags.size) { this.size = this.tags.size.value; } else { if (this.tags.tps) { + // Derive from TPS this.size = this.tags.tps.value.size; this.tags.size = Tag.parse(`[Size "${this.size}"]`); } else { let error = "Missing board size"; if (ptn) { + // Fail if initializing from PTN throw new Error(error); } else { + // Use default size otherwise console.warn(error); this.warnings.push(error); this.size = this.defaultSize; @@ -223,10 +239,12 @@ export default class GameBase { } } + // Sanitize opening if (!this.tags.opening) { this.tags.opening = Tag.parse('[Opening "swap"]'); } + // Parse TPS if (this.tags.tps) { this.hasTPS = true; this.firstMoveNumber = this.tags.tps.value.linenum; @@ -271,8 +289,10 @@ export default class GameBase { this.updateConfig(); this.board = new Board(this, null, this.board ? this.board.output : null); - // Parse BODY + //#region Parse BODY + if (ptn) { + // Parse moves from PTN let item, ply; let branch = null; let move = new Move({ @@ -422,6 +442,7 @@ export default class GameBase { delete item.ptn; } } else if (moves) { + // Parse moves from JSON if (comments) { this.parseJSONComments(comments, -1); } @@ -431,6 +452,8 @@ export default class GameBase { return handleError(error); } + //#region Init Game + if (!this.moves[0]) { this.moves[0] = new Move({ game: this, @@ -451,6 +474,7 @@ export default class GameBase { this.name = this.generateName(); } + // Init TPS if (this.tags.tps || this.editingTPS) { this.board.doTPS(this.editingTPS); } @@ -518,6 +542,8 @@ export default class GameBase { } } + //#region Methods + get minState() { return this.board.minState; } @@ -735,6 +761,8 @@ export default class GameBase { } } + //#region Output + toString(options) { options = Object.assign( { diff --git a/src/components/drawers/BotSuggestions.vue b/src/components/drawers/BotSuggestions.vue index 773fed64..8d136163 100644 --- a/src/components/drawers/BotSuggestions.vue +++ b/src/components/drawers/BotSuggestions.vue @@ -202,35 +202,39 @@ - + /> + + + @@ -539,7 +543,7 @@ export default { } }, - // MARK: Tiltak Cloud + //#region Tiltak Cloud async analyzeGameTiltak() { if (this.isOffline || !this.game.ptn.branchPlies.length) { @@ -690,7 +694,7 @@ export default { return result; }, - // MARK: Tiltak WASM + //#region Tiltak WASM initTiltakInteractive(force = false) { if (force || !this.tiltakWorker) { @@ -923,7 +927,7 @@ export default { } }, - // MARK: Topaz + //#region Topaz initTopaz(force = false) { if (force || !this.topazWorker) { @@ -1023,7 +1027,7 @@ export default { } }, - // MARK: Init + //#region Init init() { // Load wasm bots @@ -1095,8 +1099,13 @@ export default { }, botSettings: { handler(settings) { + // Save preferences this.$store.dispatch("ui/SET_UI", ["botSettings", settings]); + + // Update current position/bot hash this.botSettingsHash = this.hashBotSettings(settings); + + // Stop interactive analysis when switching bots if (settings.bot !== "tiltak" && this.tiltakInteractive.isEnabled) { this.tiltakInteractive.isEnabled = false; } diff --git a/src/i18n/en-us/about.md b/src/i18n/en-us/about.md index 7325d11c..45fbcf79 100644 --- a/src/i18n/en-us/about.md +++ b/src/i18n/en-us/about.md @@ -1,6 +1,6 @@ # PTN Ninja -**Version [3.4.19](https://github.com/gruppler/PTN-Ninja/releases)** +**Version [3.4.20](https://github.com/gruppler/PTN-Ninja/releases)** This is an editor and viewer for [Portable Tak Notation (PTN)](https://ustak.org/portable-tak-notation/). It aims to be... diff --git a/src/store/game/actions.js b/src/store/game/actions.js index 26b62f8f..27cfe546 100644 --- a/src/store/game/actions.js +++ b/src/store/game/actions.js @@ -444,11 +444,41 @@ export const OPEN_FILES = async function ({ dispatch, state }, files) { resolve; } }; + const onInit = (game) => { games.push(game); finish(); }; + const parseGame = (ptn, name) => { + const onError = (error, plyID) => { + console.warn( + `Encountered an error in "${name}" at plyID:`, + plyID, + error + ); + notifyError(`${name}: ${error.message}`); + finish(); + }; + + const index = state.list.findIndex((g) => g.name === name); + if (index < 0 || this.state.ui.openDuplicate !== "replace") { + try { + new Game({ + ptn, + name, + onError, + onInit, + }); + } catch (error) { + console.error("Invalid game:", name, error); + } + } else { + dispatch("REPLACE_GAME", { index, ptn }); + finish(); + } + }; + files = Array.from(files); files = files.filter((file) => file && /(\.ptn|\.txt)+$/i.test(file.name)); if (!files.length) { @@ -462,32 +492,10 @@ export const OPEN_FILES = async function ({ dispatch, state }, files) { let reader = new FileReader(); reader.onload = (event) => { const name = file.name.replace(/(\.ptn|\.txt)+$/, ""); - const index = state.list.findIndex((g) => g.name === name); - const ptn = event.target.result; - const onError = (error, plyID) => { - console.warn( - `Encountered an error in "${name}" at plyID:`, - plyID, - error - ); - notifyError(`${name}: ${error.message}`); - finish(); - }; - if (index < 0 || this.state.ui.openDuplicate !== "replace") { - try { - new Game({ - ptn, - name, - onError, - onInit, - }); - } catch (error) { - console.error("Invalid game:", name, error); - } - } else { - dispatch("REPLACE_GAME", { index, ptn }); - finish(); - } + const games = Game.split(event.target.result); + games.forEach((ptn, i) => { + parseGame(ptn, `${name} - Game ${i + 1}`); + }); }; reader.onerror = notifyError; reader.readAsText(file);