/**
* @fileoverview This client of the main module can't update the state in the board object directly, so has to use the localStorage.game_mq for that
* @author Robert Laing
* @module server
*/
/**
* @namespace module:server.jsonIn
* @property {string} action - "player_move" or "game_over"
* @property {string} game - prefix for localStorage and server database entries
* @property {integer} move_count - used to id messages to the server so turns aren't inadvertantly skipped
* @property {Array} [state] - player_move to tell server the current state.
* @property {Object} [move] - player_move selected by human player, eg {"red": ["move", "a", 1, "b", 2]} or {"red": "noop"}
* @property {Array} [init] - starting state stored in localStorage to update server utilities
* @property {Object[]} [moves] - array of moves to provide history of a game played to completion.
* @property {Object} [goal] - didn't simply call this utility to distinguish from non terminal state values.
*/
/**
* @namespace module:server.jsonOut
* @property {string} game - prefix for localStorage and server database entries
* @property {Array} state - A nested array of bases describing the current state of the board and who's to play
* @property {Object} last_move - {"red": ["move", "a", 1, "b", 2], "blue": "noop"}
* @property {boolean} terminal - true if game over
* @property {Array} [legals] - Possible moves for the human player, used to help explain rules
* @property {Object} [utility] - final score if game over
* @property {integer} move_count - used to id messages to the server so turns aren't inadvertantly skipped
*/
let jsonOut;
let mq = [];
let rpcQueue = [];
let busy = false;
/**
* Exporting jsonOut directly instead of via a getter function caused the client to only see undefined without updates.
*/
function jsonOutGet() {
return jsonOut;
}
function send(message) {
mq.push(message);
}
function init() {
jsonOut = undefined;
mq = [];
rpcQueue = [];
busy = false;
}
function update() {
const action = mq.shift();
if (action !== undefined && Object.hasOwn(action[1], "game")) {
switch (action[0]) {
case "rpc":
if (busy) {
rpcQueue.push(action[1]);
} else {
rpc(action[1]);
}
break;
default:
window.container.textContent = `Unknown state action ${action[1]}`
}
}
if (rpcQueue.length > 0 && !busy) {
const jsonIn = rpcQueue.shift();
rpc(jsonIn);
}
}
/**
* Private procedure sending JSON to the server and processing response
* @function module:state~rpc
* @param {object} jsonIn
*/
async function rpc(jsonIn) {
try {
busy = true;
const response1 = await fetch("/move", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(jsonIn)
});
if (!response1.ok) {
throw new Error(`Response status: ${response1.status}`);
}
const response2 = response1.clone();
try {
jsonOut = await response1.json();
if (jsonOut.state !== null) { // hack to avoid null states getting returned
const moves = JSON.parse(localStorage[`${jsonOut.game}_moves`]);
moves.push(jsonOut.last_move);
localStorage[`${jsonOut.game}_moves`] = JSON.stringify(moves);
localStorage[`${jsonOut.game}_state`] = JSON.stringify(jsonOut.state);
localStorage[`${jsonOut.game}_legals`] = JSON.stringify(jsonOut.legals);
// don't wait to request AI player to start thinking when it's its turn without, ie no bothering about animationBusy
if (jsonOut.last_move) {
jsonOut.move_count = jsonIn.move_count + 1;
}
const player = localStorage[`${jsonOut.game}_player`];
if (jsonOut.legals && JSON.stringify(JSON.parse(`[{"${player}": "noop"}]`)) === JSON.stringify(jsonOut.legals)) {
mq.push(["rpc", {
"action": "player_move",
"game": jsonOut.game,
"state": jsonOut.state,
"move": JSON.parse(`{"${player}": "noop"}`),
"move_count": jsonOut.move_count
}]);
}
// request server to update game tree if terminal without bothering about animationBusy
if (jsonOut.terminal) {
mq.push(["rpc", {
"action": "game_over",
"game": jsonOut.game,
"init": JSON.parse(localStorage[`${jsonOut.game}_init`]),
"moves": JSON.parse(localStorage[`${jsonOut.game}_moves`]),
"goal": jsonOut.utility
}]);
}
} else {
rpc(jsonIn); // potential infinite loop here, should include limit
}
} catch (error) {
const textOut = await response2.text();
if (textOut !== `{"response": "ok"}`) {
window.container.textContent = `json_error ${textOut} ${JSON.stringify(jsonIn)}`;
}
}
} catch (error) {
window.container.textContent = `rpc error ${error.name} ${error.message} ${JSON.stringify(jsonIn)}`;
rpc(jsonIn);
} finally {
busy = false;
}
}
export default Object.freeze({jsonOutGet, send, init, update});