Skip to content

Commit

Permalink
API documented, and repo prepped for 0.1.0 release
Browse files Browse the repository at this point in the history
  • Loading branch information
Perlkonig committed Oct 21, 2021
1 parent a5503de commit 42156c7
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 14 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ 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.1.0] - 2021-10-21

### Added

- Support library for rectangular grids created.
- The game "Amazons" has been implemented, including a rudimentary and very slow AI.
- The game "Blam!" has been implemented, including a rudimentary AI.
- The game "Cannon" has been implemented, including a rudimentary AI.
- The game "Martian Chess" (2-player only, including "Of Knights and Kings" variant) has been implemented, including a rudimentary AI.
- Playground added.
- Public API documented
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Abstract Play Games Library

TypeScript implementations of the core Abstract Play games, intended to be wrapped by the front- and backends.

## Contributing

Currently all Abstract Play games must be coded in TypeScript and included in this library. Eventually externally hosted games will hopefully be supported. Until then, pull requests are welcome.

## Contact

Expand All @@ -18,3 +20,4 @@ This is a basic NPM module; it's just private. It's not meant to be generally us
- `npm run test` (makes sure everything is working)
- `npm run build` (compiles the TypeScript files into the `./build` folder)
- `npm run dist-dev` (or `dist-prod` if you want it minified; bundles everything for the browser into the `./dist` folder)
- The public-facing API is documented in `./docs/api.md`.
102 changes: 102 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Games Library API

All of Abstract Play's internal games exist within this single Node project. This document describes the public-facing API that can be used by the browser and the API to interact with the game code. For concrete examples, see `playground/index.html`.

This document only looks at the public interface—the relatively small selection of variables and functions exported by the root module. Documentation for games developers will come later.

The AI code in this library is for testing only. It is not ready for release and is not documented here yet.

## Usage

In the browser, simply load `APGames.js` via a `<script>` tag. From within Node, simply import the variables and functions you need. Both methods give you access to the same API.

## API

The API currently consists of only one variable and one function:

* `gameinfo`: This variable contains the full details on all implemented games.
* `GameFactory`: This function accepts a game's `uid` an optional list of constructor arguments and returns an instance of that game object.

### `gameinfo`

The games are self-documenting. The variable itself is an ES6 `Map` of game uid to object matching the `gameinfo.json` schema in the `/schemas` folder. See the schema for details. In summary, the following information is provided:

* unique id (the "primary key" used throughout the system)
* full name
* a Markdown-encoded description of the game, including any relevant implementation notes and sometimes a rules summary
* a list of URLs related to the game
* a list of people involved in the game
* a list of supported player counts
* a list of supported variants

### `GameFactory`

This function is how you instantiate a particular game. Pass it the game's `uid` and any constructor parameters to receive the game instance. Passing it an existing state object (described more below) is how you load a game in progress. Otherwise you'll get a brand new game.

## The Game Object

All games implement a core set of features, which make up the public API.

### State

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

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

```ts
export interface IAPGameState {
game: string;
numplayers: number;
variants?: string[];
gameover: boolean;
winner: any[];
stack: Array<IIndividualState>;
}

export interface IIndividualState {
_version: string;
_results: APMoveResult[];
[key: string]: any;
}
```

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.

* `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.
* `variants` is an optional list of variant uids in effect for this game.
* `gameover` indicates whether one of the end-of-game triggers have been met.
* `winner` is an array of numbers indicating the player numbers of those who won. Usually it's an array of length one, but in draws or ties, the list may be longer.
* `stack` is the list of individual game states after each move. You should never edit individual game states, but you could step backwards and forwards through the stack, and could pop the top element off for a quick "undo."

The individual state objects can be loaded using the `load(idx?: number = -1)` instance method. By default it loads the most recent state in the stack, but providing an index value (0 being the initial game state, 1 being after the first move, and so on) will let you interact with that state. You can use a negative index to load states from the most recent (-1 is the latest state, -2 the move before that, etc.).

You can get a graphical representation of the loaded state using the `render()` method. It returns an object ready to pass to the Abstract Play renderer module that matches the schema described there.

### Game Play

Functions:
* `move(m: string) => GameBase`
* `undo() => GameBase`
* `resign(player: number) => GameBase`

The `move` function is the primary method of changing game state. Pass it a string representing a valid move and it will either throw an error or return the modified game state.

The method `undo` removes the latest state of the stack and returns the game object. The engine persisting game states can do this itself by simply modifying the saved state as well.

The `resign` function accepts a player number and removes that person from the game. This will usually result in the game ending. The modified game state is returned.

### Game History

Functions:
* `moveHistory() => string[][]`
* `resultsHistory() => APMoveResult[][]`

At any point during a game, you can request a compilation of all the moves made using `moveHistory()`. It returns a list of moves grouped by "round," meaning in a two player game, each array will contain the first and second player's moves together. **This is not the same as a formal game report.** Once the records and ranking module is implemented and API stabilized, methods will be added to generate supported formats.

Sometimes things happen in a game that are not easily rendered on a static graphical representation. To make it easier to report to players what happened during a move, and to make future analysis of games easier, each move generates one more more "results," described in the schema `moveresults.json` in the `/schemas` folder. the `resultsHistory()` method returns a complete list of results for each move of the game.

Results are things like `place` (for placing a piece), `deltaScore` (representing a change in the current player's score), and `eog` (signalling the game ended in this move). This sort of structured data can then be translated into localized written descriptions of state changes that make up a written game log.
15 changes: 12 additions & 3 deletions src/games/_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface IAPGameState {
numplayers: number;
variants?: string[];
gameover: boolean;
winner: any[];
winner: number[];
stack: Array<IIndividualState>;
}

Expand Down Expand Up @@ -73,14 +73,23 @@ export abstract class GameBase {

public abstract move(move: string): GameBase;
public abstract render(): APRenderRep;
public abstract state(): any;
public abstract state(): IAPGameState;
public abstract load(idx: number): GameBase;
public abstract resign(player: number): GameBase;
protected abstract moveState(): any;
public abstract load(idx: number): void;

protected saveState(): void {
this.stack.push(this.moveState());
}

public undo(): GameBase {
if (this.stack.length < 1) {
throw new Error("You can't undo the initial game state");
}
this.stack.pop();
return this;
}

public moveHistory(): string[][] {
const moves: string[][] = [];
for (let i = 1; i < this.stack.length; i += this.numplayers) {
Expand Down
3 changes: 2 additions & 1 deletion src/games/amazons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export class AmazonsGame extends GameBase {
this.load();
}

public load(idx: number = -1): void {
public load(idx: number = -1): AmazonsGame {
if (idx < 0) {
idx += this.stack.length;
}
Expand All @@ -147,6 +147,7 @@ export class AmazonsGame extends GameBase {
this.board = new Map(state.board);
this.lastmove = state.lastmove;
this.graph = this.buildGraph();
return this;
}

public moves(player?: 1|2): string[] {
Expand Down
5 changes: 3 additions & 2 deletions src/games/blam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export class BlamGame extends GameBase {
this.load();
}

public load(idx: number = -1): void {
public load(idx: number = -1): BlamGame {
if (idx < 0) {
idx += this.stack.length;
}
Expand All @@ -125,7 +125,8 @@ export class BlamGame extends GameBase {
this.lastmove = state.lastmove;
this.scores = [...state.scores];
this.caps = [...state.caps];
}
return this;
}

public moves(player?: playerid): string[] {
if (player === undefined) {
Expand Down
3 changes: 2 additions & 1 deletion src/games/cannon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export class CannonGame extends GameBase {
this.load();
}

public load(idx: number = -1): void {
public load(idx: number = -1): CannonGame {
if (idx < 0) {
idx += this.stack.length;
}
Expand All @@ -143,6 +143,7 @@ export class CannonGame extends GameBase {
this.board = new Map(state.board);
this.lastmove = state.lastmove;
this.placed = state.placed;
return this;
}

public moves(player?: 1|2): string[] {
Expand Down
4 changes: 2 additions & 2 deletions src/games/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { APGamesInformation } from "../schemas/gameinfo";
import { GameBase } from "./_base";
import { GameBase, IAPGameState } from "./_base";
import { AmazonsGame, IAmazonsState } from "./amazons";
import { BlamGame, IBlamState } from "./blam";
import { CannonGame, ICannonState } from "./cannon";
import { MchessGame, IMchessState } from "./mchess";

export {
APGamesInformation, GameBase,
APGamesInformation, GameBase, IAPGameState,
AmazonsGame, IAmazonsState,
BlamGame, IBlamState,
CannonGame, ICannonState,
Expand Down
3 changes: 2 additions & 1 deletion src/games/mchess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class MchessGame extends GameBase {
this.load();
}

public load(idx: number = -1): void {
public load(idx: number = -1): MchessGame {
if (idx < 0) {
idx += this.stack.length;
}
Expand All @@ -120,6 +120,7 @@ export class MchessGame extends GameBase {
this.board = new Map(state.board);
this.lastmove = state.lastmove;
this.scores = [...state.scores];
return this;
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { APGamesInformation } from './schemas/gameinfo';
import { games, GameFactory } from "./games";
import { APMoveResult } from './schemas/moveresults';
import { games, GameFactory, IAPGameState } from "./games";
import { AIFactory, supportedGames as aiSupported, fastGames as aiFast, slowGames as aiSlow } from './ais';

export {GameFactory, AIFactory, aiSupported, aiFast, aiSlow};
export {GameFactory, IAPGameState, APMoveResult, APGamesInformation, AIFactory, aiSupported, aiFast, aiSlow};

const gameinfo: Map<string, APGamesInformation> = new Map();
games.forEach((v, k) => {
Expand Down

0 comments on commit 42156c7

Please sign in to comment.