Skip to content

Repository files navigation

cheesse

Chess engine as a Go package, HTTP server, CLI and WebAssembly binary: parse, convert and play chess in any notation.

Release CI License: MIT Go Version Go Reference

Live demos

Notation Converter · Match Parser · Play vs AI · AI Auto-play

All four run the engine as WebAssembly in your browser and are redeployed on every merge to master.

Features

  • Full rules engine on bitboards: legal move generation (perft-validated), castling, en passant, promotions, check/checkmate/stalemate, double and discovered check detection.
  • Complete draw handling: threefold repetition, insufficient material, the 50-move rule, and claimable/agreed draws.
  • Six notations, parsed and printed: Algebraic (SAN), Figurine, Descriptive, Coordinate, ICCF and Smith, plus PGN with tag pairs, comments, NAGs and (...) variations.
  • Notation auto-detection with partial-parse tolerance: invalid input still yields the valid prefix, the most likely notation, and why parsing stopped.
  • Conversion of a game from any notation to any notation, optionally from a custom starting position (FEN or board strings).
  • AI move selection: random, or minimax with alpha-beta pruning at easy/medium/hard.
  • Battle-tested: 5,553 real PGN games in CI, plus ostinato's engine and notation corpora (Chernev endings, six-notation suite).

API

Six functions, identical across the Go package, HTTP server, CLI and WASM binary:

DefaultGame() OutputGame
ParseGame(game InputGame) (OutputGame, error)
DoAction(game InputGame, action InputAction) (OutputGame, OutputAction, error)

// Auto-detects the notation: Algebraic (incl. Figurine and PGN), Coordinate, Descriptive, ICCF, Smith
ParseNotation(game InputGame, notationString string) (OutputGame, OutputParseResult, error)

// Auto-detects the source notation and re-renders every move in the target notation:
// one of {Algebraic|Figurine|Descriptive|Coordinate|ICCF|Smith}
ConvertNotation(game InputGame, notationString string, targetNotation string) (OutputGame, OutputParseResult, error)

// mode is one of {random|easy|medium|hard}; ok is false when the game is over
AIMove(game InputGame, mode string) (game OutputGame, action OutputAction, ok bool, err error)

Go package

All examples assume:

import "github.com/marianogappa/cheesse/api"

a := api.New()
Convert a game between notations
// The source notation is auto-detected
_, result, _ := a.ConvertNotation(api.InputGame{}, "1. P-K4 P-K3\n2. P-Q4 P-Q4", "ICCF")
fmt.Println(result.NotationName) // Descriptive Notation
for _, step := range result.Steps {
	fmt.Print(step.ActionString, " ") // 5254 5756 4244 4745
}
Parse a game, auto-detecting the notation
_, result, _ := a.ParseNotation(api.InputGame{}, "1. e4 e5 2. Nf3")
fmt.Println(result.NotationName)     // Algebraic Notation
fmt.Println(result.ValidActionCount) // 3
for _, step := range result.Steps {
	fmt.Print(step.ActionString, " ") // e4 e5 Nf3
}
Parse a PGN with headers, comments and variations
pgn := `[Event "F/S Return Match"]
[White "Fischer, Robert J."]

1. e4 e5 2. Nf3 {main line} Nc6 (2... d6) 3. Bb5 1/2-1/2`

_, result, _ := a.ParseNotation(api.InputGame{}, pgn)
fmt.Println(result.NotationName)      // PGN
fmt.Println(result.Metadata["White"]) // Fischer, Robert J.
Make a move and inspect the position
game, action, _ := a.DoAction(api.InputGame{}, api.InputAction{FromSquare: "e2", ToSquare: "e4"})
fmt.Println(action.ActionString) // e4
fmt.Println(game.FENString)      // rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1
Ask the AI for a move
game, action, ok, _ := a.AIMove(api.InputGame{}, "hard") // random|easy|medium|hard
fmt.Println(ok, action.ActionString) // true Nc3
fmt.Println(game.FENString)          // rnbqkbnr/pppppppp/8/8/8/2N5/PPPPPPPP/R1BQKBNR b KQkq - 1 1
Load any position from FEN
game, _ := a.ParseGame(api.InputGame{FENString: "rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3"})
fmt.Println(game.IsCheckmate) // true
Tolerate a partially valid game
_, result, _ := a.ParseNotation(api.InputGame{}, "1. e4 e5 2. Nf3 Nc6 3. Zz9")
fmt.Println(result.ParseWasSuccessful) // false
fmt.Println(result.ValidActionCount)   // 4 (the valid prefix is in result.Steps)
fmt.Println(result.Error)              // at move 3: unexpected input (no token matched)
Enumerate every available action
game := a.DefaultGame()
fmt.Println(len(game.Actions)) // 22: 20 legal moves, plus resign and draw offer
first := game.Actions[0]
fmt.Println(first.FromPieceSquare, first.ToSquare) // a2 a4

Server

cheesse -serve 8080

One endpoint per API function (/defaultGame, /parseGame, /doAction, /parseNotation, /convertNotation, /aiMove); requests and responses are JSON.

Get the default game as a Unicode board
curl -s localhost:8080/defaultGame | jq .game.board.board
[
  "♜♞♝♛♚♝♞♜",
  "♟♟♟♟♟♟♟♟",
  "        ",
  "        ",
  "        ",
  "        ",
  "♙♙♙♙♙♙♙♙",
  "♖♘♗♕♔♗♘♖"
]
Ask the AI for a move
curl -s localhost:8080/aiMove -d '{"game":{},"mode":"hard"}' | jq .action.actionString
"Nc3"
Convert a game over HTTP
curl -s localhost:8080/convertNotation -d '{"game":{},"notationString":"1. P-K4 P-K3","targetNotation":"Smith"}' | jq -c '[.parseResult.steps[].actionString]'
["e2e4","e7e6"]

CLI

Same JSON in, JSON out, one flag per API function.

Detect the notation of a pasted game
cheesse -parseNotation '{"game":{},"notationString":"1. e4 e5 2. Nf3"}' | jq .parseResult.notationName
"Algebraic Notation"
Convert a game to Smith notation
cheesse -convertNotation '{"game":{},"notationString":"1. P-K4 P-K3","targetNotation":"Smith"}' | jq -c '[.parseResult.steps[].actionString]'
["e2e4","e7e6"]

WebAssembly

Prebuilt cheesse.wasm, cheesse.js and wasm_exec.js are attached to every release (with checksums.txt), or build from source:

GOOS=js GOARCH=wasm go build -tags tinygo -o cheesse.wasm .
Call the API from JavaScript

The binary exposes the full API as synchronous JS globals. Every function takes a Uint8Array containing a JSON request and returns a Uint8Array containing a JSON response (same shapes as the HTTP endpoints; {"error": "..."} on failure):

const enc = new TextEncoder(), dec = new TextDecoder();
const call = (fn, obj) => JSON.parse(dec.decode(fn(enc.encode(JSON.stringify(obj)))));

JSON.parse(dec.decode(cheesseDefaultGame()));
call(cheesseParseGame,       {game: {fenString: "..."}});
call(cheesseDoAction,        {game: {}, action: {fromSquare: "e2", toSquare: "e4"}});
call(cheesseParseNotation,   {game: {}, notationString: "1. e4 e5"});
call(cheesseConvertNotation, {game: {}, notationString: "1. e4 e5", targetNotation: "ICCF"});
call(cheesseAIMove,          {game: {}, mode: "random"}); // random|easy|medium|hard

Performance

Benchmark results (auto-updated on every push)

Updated by CI on the next merge to master.

Why is it called "cheesse"?

That's roughly how kiwi people pronounce chess.

About

Package, server, cli tool and WebAssembly binary for chess use cases.

Resources

Stars

67 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages