Skip to content

Commit

Permalink
Serialization is now working properly. Playground updated to use the …
Browse files Browse the repository at this point in the history
…new 'serialize()' function. API change documented. Prepped for 0.2.0 release.
  • Loading branch information
Perlkonig committed Oct 29, 2021
1 parent 58886ea commit edae7b7
Show file tree
Hide file tree
Showing 13 changed files with 133 additions and 62 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [0.2.0] - 2021-10-29

### Added

- Games now produce valid game reports.
- Homeworlds has been implemented. No move generation or AI.

### Changed

- Public API tweaked a little to hide unnecessary details. The `serialize()` function will return a string that can now be handed to the constructor.

## [0.1.0] - 2021-10-21

Expand Down
10 changes: 8 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,14 @@ All games implement a core set of features, which make up the public API.
### State

Functions:
* `state() => IAPGameState`

* `serialize() => string`
* `state() => IAPGameState`
* `load(idx?: number = -1) => GameBase`
* `render() => APRenderRep`

The `serialize()` function is how to persist states. It produces a simple string that can be stored. It abstracts away any nuances of the internal representation (e.g., "replacer" or "reviver" helpers). The resulting string can then be passed to the constructor to rehydrate.

The `state()` function will return an object of type `IAPGameState`, described below:

```ts
Expand All @@ -63,7 +67,7 @@ export interface IIndividualState {
}
```

If you wish to persist game state, this is what you save. Editing the state object should never be done except for manipulating the stack. Changing the `variants`, for example, would fully corrupt the game record.
Editing the state object should never be done except for manipulating the stack. Changing the `variants`, for example, would fully corrupt the game record.

* `game` is the uid of the game the state represents. Trying to load a saved state into the wrong game code will throw an error.
* `numplayers` tells you how many players are involved in this particular game instance.
Expand All @@ -79,6 +83,7 @@ You can get a graphical representation of the loaded state using the `render()`
### Game Play

Functions:

* `move(m: string) => GameBase`
* `undo() => GameBase`
* `resign(player: number) => GameBase`
Expand All @@ -92,6 +97,7 @@ The `resign` function accepts a player number and removes that person from the g
### Game History

Functions:

* `moveHistory() => string[][]`
* `resultsHistory() => APMoveResult[][]`
* `genRecord(data: IRecordDetails) => APGameRecord | undefined`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@abstractplay/gameslib",
"version": "0.1.0",
"version": "0.2.0",
"description": "TypeScript implementations of the core Abstract Play games, intended to be wrapped by the front- and backends",
"main": "build/index.js",
"types": "build/index.d.ts",
Expand Down
61 changes: 21 additions & 40 deletions playground/index.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<!--
To use this in your own environment, build the `APRender.js` bundle and
put it and this HTML file somewhere a browser can see it. You can then
render arbitrary JSON for testing.
To use this in your own environment, build the `APRender.js` and `APGames.js` bundles
and put them and this HTML file somewhere a browser can see it.
-->
<html lang="en">
<head>
Expand All @@ -13,26 +12,6 @@
<script src=" https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.1/mini-default.min.css">
<script type="text/javascript">
/* Code for JSON parsing MAPs, from SO: https://stackoverflow.com/questions/29085197/how-do-you-json-stringify-an-es6-map */
function replacer(key, value) {
if(value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else {
return value;
}
}
function reviver(key, value) {
if(typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return new Map(value.value);
}
}
return value;
}

function renderGame() {
var myNode = document.getElementById("drawing");
while (myNode.lastChild) {
Expand All @@ -54,7 +33,7 @@
var state = window.sessionStorage.getItem("state");
if (state !== null) {
var gamename = window.sessionStorage.getItem("gamename");
var game = APGames.GameFactory(gamename, JSON.parse(state, reviver));
var game = APGames.GameFactory(gamename, state);

// game board
var data = game.render();
Expand All @@ -66,7 +45,7 @@
div.innerHTML = movelst.map((x) => { return "[" + x.join(", ") + "]"; }).join(" ");

// game status
var status = game.status();// + "\n\n* " + game.resultsHistory().map((x) => { return JSON.stringify(x); }).join("\n* ");
var status = game.status();
var results = game.resultsHistory().reverse().slice(0, 5);
if (results.length > 0) {
status += "\n\n* " + results.map((x) => { return JSON.stringify(x); }).join("\n* ") + "\n\n&hellip;";
Expand Down Expand Up @@ -158,7 +137,7 @@
game = APGames.GameFactory(select.value);
}
}
window.sessionStorage.setItem("state", JSON.stringify(game.state(), replacer));
window.sessionStorage.setItem("state", game.serialize());
window.sessionStorage.setItem("gamename", select.value);
renderGame();
}, false);
Expand All @@ -169,7 +148,7 @@
var state = window.sessionStorage.getItem("state");
if (state !== null) {
var gamename = window.sessionStorage.getItem("gamename");
var game = APGames.GameFactory(gamename, JSON.parse(state, reviver));
var game = APGames.GameFactory(gamename, state);
try {
game.move(movebox.value);
} catch (err) {
Expand All @@ -178,7 +157,7 @@
console.log("Error message:" + err.message);
}
movebox.value = "";
window.sessionStorage.setItem("state", JSON.stringify(game.state(), replacer));
window.sessionStorage.setItem("state", game.serialize());
renderGame();
}
});
Expand All @@ -188,7 +167,11 @@
var state = window.sessionStorage.getItem("state");
if (state !== null) {
var gamename = window.sessionStorage.getItem("gamename");
var game = APGames.GameFactory(gamename, JSON.parse(state, reviver));
var game = APGames.GameFactory(gamename, state);
if (typeof game.moves !== 'function') {
alert("This game doesn't support random moves.")
return;
}
try {
var move = game.randomMove();
game.move(move);
Expand All @@ -197,7 +180,7 @@
console.log("Game state: "+state);
console.log("Error message:" + err.message);
}
window.sessionStorage.setItem("state", JSON.stringify(game.state(), replacer));
window.sessionStorage.setItem("state", game.serialize());
renderGame();
}
});
Expand All @@ -207,20 +190,19 @@
var state = window.sessionStorage.getItem("state");
if (state !== null) {
var gamename = window.sessionStorage.getItem("gamename");
var game = APGames.GameFactory(gamename, JSON.parse(state, reviver));
var game = APGames.GameFactory(gamename, state);
if (gamename !== null) {
var depth = APGames.aiFast.get(gamename);
if ( (depth !== undefined) && (depth !== null) ) {
var movebox = document.getElementById("moveEntry");
var state = JSON.parse(window.sessionStorage.getItem("state"), reviver);
if ( (state.numplayers !== undefined) && (state.numplayers !== 2) ) {
if ( (game.numplayers !== undefined) && (game.numplayers !== 2) ) {
alert("AI only works with 2-player games.");
return false;
}
var factory = APGames.AIFactory(gamename);
var move = factory.constructor.findmove(state, depth);
var move = factory.constructor.findmove(game.state(), depth);
game.move(move);
window.sessionStorage.setItem("state", JSON.stringify(game.state(), replacer));
window.sessionStorage.setItem("state", game.serialize());
renderGame();
} else {
alert("This game does not support fast AI.");
Expand All @@ -233,20 +215,19 @@
var state = window.sessionStorage.getItem("state");
if (state !== null) {
var gamename = window.sessionStorage.getItem("gamename");
var game = APGames.GameFactory(gamename, JSON.parse(state, reviver));
var game = APGames.GameFactory(gamename, state);
if (gamename !== null) {
var depth = APGames.aiSlow.get(gamename);
if ( (depth !== undefined) && (depth !== null) ) {
var movebox = document.getElementById("moveEntry");
var state = JSON.parse(window.sessionStorage.getItem("state"), reviver);
if ( (state.numplayers !== undefined) && (state.numplayers !== 2) ) {
if ( (game.numplayers !== undefined) && (game.numplayers !== 2) ) {
alert("AI only works with 2-player games.");
return false;
}
var factory = APGames.AIFactory(gamename);
var move = factory.constructor.findmove(state, depth);
var move = factory.constructor.findmove(game.state(), depth);
game.move(move);
window.sessionStorage.setItem("state", JSON.stringify(game.state(), replacer));
window.sessionStorage.setItem("state", game.serialize());
renderGame();
} else {
alert("This game does not support slow AI.");
Expand Down
20 changes: 20 additions & 0 deletions src/common/serialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* Code for JSON parsing MAPs, from SO: https://stackoverflow.com/questions/29085197/how-do-you-json-stringify-an-es6-map */
export function replacer(key: any, value: any) {
if(value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else {
return value;
}
}

export function reviver(key: any, value: any) {
if(typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return new Map(value.value);
}
}
return value;
}
10 changes: 8 additions & 2 deletions src/games/_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { APGamesInformation } from '../schemas/gameinfo';
import { APRenderRep } from "@abstractplay/renderer/src/schema";
import { APMoveResult } from '../schemas/moveresults';
import { APGameRecord } from "@abstractplay/recranks/src";
import { replacer } from '../common/serialization';

const columnLabels = "abcdefghijklmnopqrstuvwxyz".split("");

Expand Down Expand Up @@ -33,7 +34,7 @@ export interface IIndividualState {
export interface IAPGameState {
game: string;
numplayers: number;
variants?: string[];
variants: string[];
gameover: boolean;
winner: number[];
stack: Array<IIndividualState>;
Expand Down Expand Up @@ -88,8 +89,9 @@ export abstract class GameBase {
public abstract stack: Array<IIndividualState>;
public abstract gameover: boolean;
public abstract numplayers: number;
public abstract winner?: any[];
public abstract winner: any[];
public abstract results: Array<APMoveResult>;
public abstract variants: string[];

public abstract move(move: string): GameBase;
public abstract render(): APRenderRep;
Expand Down Expand Up @@ -308,4 +310,8 @@ export abstract class GameBase {

return rec;
}

public serialize(): string {
return JSON.stringify(this.state(), replacer);
}
}
10 changes: 8 additions & 2 deletions src/games/amazons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Directions } from "../common";
import { UndirectedGraph } from "graphology";
import bidirectional from 'graphology-shortest-path/unweighted';
import { APMoveResult } from "../schemas/moveresults";
import { reviver } from "../common/serialization";

const gameDesc:string = `# Amazons
Expand Down Expand Up @@ -97,12 +98,16 @@ export class AmazonsGame extends GameBase {
public gameover: boolean = false;
public winner: playerid[] = [];
public graph!: UndirectedGraph;
public stack: Array<IMoveState>;
public stack!: Array<IMoveState>;
public results: Array<APMoveResult> = [];
public variants: string[] = [];

constructor(state?: IAmazonsState) {
constructor(state?: IAmazonsState | string) {
super();
if (state !== undefined) {
if (typeof state === "string") {
state = JSON.parse(state, reviver) as IAmazonsState;
}
if (state.game !== AmazonsGame.gameinfo.uid) {
throw new Error(`The Amazons game code cannot process a game of '${state.game}'.`);
}
Expand Down Expand Up @@ -309,6 +314,7 @@ export class AmazonsGame extends GameBase {
return {
game: AmazonsGame.gameinfo.uid,
numplayers: 2,
variants: [],
gameover: this.gameover,
winner: [...this.winner],
stack: [...this.stack]
Expand Down
10 changes: 7 additions & 3 deletions src/games/blam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { RectGrid } from "../common";
import { APRenderRep } from "@abstractplay/renderer/src/schema";
import { Directions } from "../common";
import { APMoveResult } from "../schemas/moveresults";
import { reviver } from "../common/serialization";
// tslint:disable-next-line: no-var-requires
const clone = require("rfdc/default");

Expand Down Expand Up @@ -68,14 +69,14 @@ export class BlamGame extends GameBase {
public lastmove?: string;
public gameover: boolean = false;
public winner: playerid[] = [];
public variants?: string[];
public variants: string[] = [];
public scores!: number[];
public caps!: number[];
public stashes!: Map<playerid, number[]>;
public stack: Array<IMoveState>;
public stack!: Array<IMoveState>;
public results: Array<APMoveResult> = []

constructor(state: number | IBlamState, variants?: string[]) {
constructor(state: number | IBlamState | string, variants?: string[]) {
super();
if (typeof state === "number") {
this.numplayers = state;
Expand All @@ -98,6 +99,9 @@ export class BlamGame extends GameBase {
}
this.stack = [fresh];
} else {
if (typeof state === "string") {
state = JSON.parse(state, reviver) as IBlamState;
}
if (state.game !== BlamGame.gameinfo.uid) {
throw new Error(`The Blam! game code cannot process a game of '${state.game}'.`);
}
Expand Down
10 changes: 8 additions & 2 deletions src/games/cannon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { APRenderRep } from "@abstractplay/renderer/src/schema";
import { RectGrid } from "../common";
import { Directions } from "../common";
import { APMoveResult } from "../schemas/moveresults";
import { reviver } from "../common/serialization";

const gameDesc:string = `# Cannon
Expand Down Expand Up @@ -74,12 +75,16 @@ export class CannonGame extends GameBase {
public gameover: boolean = false;
public winner: playerid[] = [];
public placed: boolean = false;
public stack: Array<IMoveState>;
public stack!: Array<IMoveState>;
public results: Array<APMoveResult> = []
public variants: string[] = [];

constructor(state?: ICannonState) {
constructor(state?: ICannonState | string) {
super();
if (state !== undefined) {
if (typeof state === "string") {
state = JSON.parse(state, reviver) as ICannonState;
}
if (state.game !== CannonGame.gameinfo.uid) {
throw new Error(`The Cannon game code cannot process a game of '${state.game}'.`);
}
Expand Down Expand Up @@ -375,6 +380,7 @@ export class CannonGame extends GameBase {
return {
game: CannonGame.gameinfo.uid,
numplayers: 2,
variants: [...this.variants],
gameover: this.gameover,
winner: [...this.winner],
stack: [...this.stack]
Expand Down
Loading

0 comments on commit edae7b7

Please sign in to comment.