ostinato: a chess engine written in Scala that runs in the Browser, Docker and the REPL



TL;DR

In this blogpost, I introduce the ostinato project: a Chess engine written in Scala.

It’s not the fastest engine around, nor the hardest AI to beat. However, it enables some use cases that I haven’t found good free online sources for, like parsing notations into matches, converting between notations, playing against the AI from a given step in a parsed match, as well as the ability to solve chess puzzles and problems via elegant one-liners on the Scala REPL.

By cross-compiling into JS using ScalaJS, the same Scala code can run in the browser as a JavaScript library, which is great for two reasons:

  1. the tools can be available online for free with Github service quality
  2. JS devs can use the library without any Scala knowledge

There is an ostinato Docker image available on Docker Hub (or it can be built locally) which exposes the same API as the JS library via an Akka HTTP server.

Because ostinato is 100% stateless, it’s a perfect candidate for Kubernetes deployments: each individual API request (e.g. an AI move) can be load balanced over a set of pods, and pod count can be auto-scaled based on CPU load average. This makes it attractive for AI research and as a backend for chess sites.

Repository

Scaladoc

Demos

Chess Game

Play a Chess Match against the AI.

Game Parser

Tool to paste any chess match in any known notation and browse through the moves via Chessboard. Also play from any board state against the AI, or convert to any other notation.

Notation Converter

Convert any pasted chess match between the following notations: PGN, Algebraic, Figurine, Coordinate, Descriptive, ICCF, Smith and FEN (with variations).

Auto-play

Two AI’s playing each other (making random moves).

Meta

I’ve recently upset some good people of the Scala community with this blogpost that became a language war. Partly, for me, working on this blogpost means showing some of the nice use-cases Scala enables, in particular one that Golang can’t do: solving complex domain-specific problems with elegant one-liners. Hopefully we can focus on the engineering this time. Sorry that I didn’t foresee or do more to prevent this on the previous blogpost.

Solving puzzles in the REPL

A quite unique feature of ostinato is that it leverages the Scala differentials in terms of succintness and elegance for actually solving problems. As a result, it becomes feasible to solve chess brain teasers directly in the REPL with only a few one-liners.

Let’s try a checkmate in two moves. Here’s an example puzzle:

Example: mate in two moves

White moves first and it should make checkmate in the second move.

Let’s solve it!

First, we need to compile the jar so we can play with it in the REPL. This should be easy: (alternatively, just download the latest jar from Releases)

$ sbt pack

We should be able to start a REPL with the ostinato jar in the classpath (the exact path could change over time so just ls the folder and find the ostinato_2.12-*.jar):

$ scala -cp target/pack/lib/ostinato_2.12-1.0.2.jar

Import the core, so we can use the chess classes:

scala> import ostinato.chess.core._
import ostinato.chess.core._

OK. Let’s generate a ChessBoard identical to the one in the puzzle:

scala> val b = ChessGame.fromGridString(
   """........
     |....♕...
     |.♟..♟.♟♚
     |♟.....♘♛
     |...♙♟...
     |.♙....♙.
     |♙....♙♔.
     |........""".stripMargin, turn = WhiteChessPlayer).get.board
b: ostinato.chess.core.ChessBoard =
........
....♕...
.♟..♟.♟♚
♟.....♘♛
...♙♟...
.♙....♙.
♙....♙♔.
........

All the ChessGame.from... methods return a Try[ChessGame], because parsing could fail. In this case, I know it’ll work so I just .get, and get the board from it.

Note that the Unicode symbols were made with the assumption of black foreground and white background. In the REPL, this is often times backwards. This might confuse you as to which colour is which. Use this as a safe copy-paste.

We’re gonna need to ask ostinato to be optimistic. If we calculate all actions on this board, ostinato will include White resigns and White claims draw, which we’re not interested in. Same will apply for the next moves. Let’s create a ChessOptimisations instance and pass it around:

scala> val o = ChessOptimisations.beOptimistic

Here’s the meaty part. We need to express the following statement in code:

“Find the actions such that for whichever following action, there’s at least one follow-up checkmate”

If the puzzle was constructed properly, there should be only one action that satisfies those conditions:

scala> b.doAllActions(o).filter(
        _.doAllActions(o).forall(
            _.doAllActions.exists(
                _.isLossFor(BlackChessPlayer)
            )
        )
    )
res1: scala.collection.immutable.Set[ostinato.chess.core.ChessBoard] =
Set(.....♕..
........
.♟..♟.♟♚
♟.....♘♛
...♙♟...
.♙....♙.
♙....♙♔.
........)

Yes! There’s exactly one ChessBoard in the resulting Set!

Let’s extract the action so we can apply it to our ChessBoard. That can be accomplished by appending .head.history.head.action.get to our previous line (i.e. getting the first action in the history).

ChessAction has a nice toString implementation.

scala> val a = res1.head.history.head.action.get
a: ostinato.chess.core.ChessAction = White's Queen moves to f8

Let’s apply the action on the board. Not all actions can be applied on a board, so doAction returns an Option[ChessBoard]. For brevity we’ll just .get it here:

scala> val b2 = b.doAction(a).get
b2: ostinato.chess.core.ChessBoard =
.....♕..
........
.♟..♟.♟♚
♟.....♘♛
...♙♟...
.♙....♙.
♙....♙♔.
........

Lastly, let’s see the full action history.

We’ll have to:

  • pick any black action (head in this case)
  • filter the white checkmate action
  • retrieve the action history
  • history is stored backwards so we’ll have to reverse it to read it properly
scala> b2.doAllActions(o).head.doAllActions.
    filter(_.isLossFor(BlackChessPlayer)).head.
    history.flatMap(_.action).reverse
res5: List[ostinato.chess.core.ChessAction] = 
List(
    White's Queen moves to f8, 
    Black's King captures White's Knight, 
    White's Queen moves to f4
)

Profit! Do compare with the puzzle site, but the solutions match.

Within the REPL, ostinato becomes a swiss-army knife for chess-related queries.

Taking the red pill

If you’re gonna be playing around with advanced cases in the REPL, you’ll probably need some time to get used to the code (you should know Scala; no chance otherwise). These examples, the Scaladoc and the hundreds of tests are good starting points, but I’d be happy to help you on my spare time so let me know; my details are at the top of the page and/or post a comment below. There’s a very reduced set of voodoo code overall, and I’m not proud of it (anymore, that is).

In the browser: ostinato.js

Although ~97% of the ostinato codebase is written in Scala, it leverages ScalaJS as a facade to enable JS use cases. By cross-compiling the code, ScalaJS produces an ostinato.js file. This strategy makes all the free tools feasible.

Most of the demos also leverage the popular and solid ChessboardJS library for UI. Because ChessboardJS’ notation conventions make a lot of sense, ostinato’s API was exposed in a way that is compatible with it.

The best part of JS support is that JS developers can use the chess engine without any Scala knowledge.

Just download the latest js file in the releases section.

Using ostinato.js is really simple and easy. For a great working intro, check the auto-play demo.

Here’s the meaty part. You’ll have to pardon my js, but I think it should be quite clear:

<script>
var boardUi = null
var initialBoard = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
var board = initialBoard

var init = function() {

    var update = function() {
        aiMove = ostinato.chess.js.Js().randomAiMove(board)
        board = (!aiMove.success || aiMove.isCheckMate ||
          aiMove.isDraw) ? initialBoard : aiMove.board
        boardUi.position(board)
        window.setTimeout(update, 600)
    }

    boardUi = ChessBoard('board', { moveSpeed: 'fast' })
    boardUi.start()
    update()
}
</script>

The only ostinato line in there is ostinato.chess.js.Js().randomAiMove(board). The rest is ChessboardJS and plain JavaScript.

Here’s what’s available at the moment from JavaScript: Js.scala

From here, you should be able to go to the other examples. If you’re interested in developing an advanced use case, I’d be happy to help you on my spare time. Find my contact details at the top of this page and/or post a comment below.

Server-side

Running ostinato on the JVM has several pros:

  • It runs faster than its ScalaJS counterpart
  • Because ostinato is 100% stateless, one server can play several games at a time
  • No high CPU usage on the client; responsiveness is up to the server

Via sbt

Try starting the server via sbt:

$ sbt ostinatoJVM/run

You can check if it’s working by trying the healthcheck:

(which could be used as a livenessProbe on a Kubernetes deployment)

$ curl localhost:51234/healthcheck
OK!

Or get an initial board to start playing from:

$ curl localhost:51234/initialBoard
{"board":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"}

And then make the computer play from it:

$ curl localhost:51234/randomAiMove -d \
'{"board":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"}' \
-H "Content-Type: application/json"
{
  "success":true,
  "isCheck":false,
  "isDraw":false,
  "board":"rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1 7163",
  "isCheckmate":false,
  "action":"Nf3"
}

Let’s try something more fun

This site provides “mate in one move” puzzles. Here’s one example:

Chess Puzzles: Mate in one

Ostinato server expects the input boards in “ostinato notation”, which is simply FEN notation plus (optionally) the history in ICCF notation.

I’m too lazy to translate a board to FEN manually, so I looked around (for way longer that it would have taken me to just do it manually -.-) and I found this Android app that OCRs the board and producess the FEN, which was:

r1b1qb1r/1p1n2pp/p2P4/4N2k/7n/1Q4P1/PP2NP1P/R1B2RK1 w - - 0 1

I would expect the basic AI to want to win as soon as possible, so let’s see if it finds the checkmate:

$ curl localhost:51234/basicAiMove -d \
'{"board":"r1b1qb1r/1p1n2pp/p2P4/4N2k/7n/1Q4P1/PP2NP1P/R1B2RK1 w - - 0 1"}' \
-H "Content-Type:application/json"
{
  "success": true,
  "isCheck": true,
  "isDraw": false,
  "board": "r1b1qb1r/1p1n2pp/p2P4/4N2k/6Pn/1Q6/PP2NP1P/R1B2RK1 b - - 0 1 7374",
  "isCheckmate": true,
  "action": "g4+"
}

Luckily, the AI didn’t disappoint me :). It recommended advancing the pawn on the g file (above the white king), and it seems adamant that it’s a checkmate.

From a geeky perspective, I find it useful to have this decoupled mini-tool at my disposal.

Also, I recently switched the JSON engine to spray-json which patiently reminded me of the JSON object names and types I was supposed to provide to the API call until I got them right. Props to Mathias. If somebody could please ask him how can I unmarshal the request’s JSON payload when the Content-Type is not application/json, I’d really appreciate it. I’ve asked around on Gitter and SO and couldn’t get a good solution.

Playing a demo game using the JVM as back-end

Simply use the provided demo chess game and add the following query parameters:

useServer=localhost:51234 // location of your ostinato back-end
depth=1 // complexity of the AI (from 0, but 3 is already slow)
debug=1 // optional: logs AI rationale to STDOUT

e.g. https://marianogappa.github.io/ostinato-examples/play?useServer=localhost:51234&depth=1

I’m not a particularly good chess player, but depth 3 beats me. I was so happy when I reached that milestone <3

Docker

You can start an ostinato container right from Docker Hub with:

  $ docker run -p 51234:51234 marianogappa/ostinato:latest

Once it’s up, you can try the healthcheck and other examples described above. Note that Docker may not open the port on localhost; this depends on your Docker installation.

If you want to experiment with the code and build the image locally (for which you will need sbt & Docker), you can use the provided script on the root folder:

  $ ./docker-build.sh

The script will kindly ask you to run sbt packArchiveZip if you haven’t done so, or if the last produced artifact was last modified half and hour ago or more.

The script always builds the ostinato image with the tag latest. If you want to specify a custom tag, you can do it like this:

  $ TAG=v1.2.3 ./docker-build.sh

Kubernetes

As previously mentioned, ostinato is 100% stateless. The server doesn’t save any state from previous requests, and all the information necessary to respond to a request is present in the request itself.

This means that:

  • Any given ostinato server can satisfy requests from multiple games
  • A game can be played by issuing requests to multiple ostinato servers

Thus, load balancing strategies become very simple with ostinato. Some popular solutions right now are:

At least with Kubernetes, there’s also the possibility of dynamically increasing the number of ostinato container replicas when the CPU usage exceeds a given threshold; this is called auto-scaling.

Ostinato doesn’t use any disk and doesn’t have any special configurations so you don’t need to mount anything. As a JVM application, though, it’s not the cheapest in terms of CPU, memory, and image size. It’s using the smallest base image I could find that has a JVM: the alpine-java one.

Here’s a deployment manifest to get you started:

---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: ostinato
spec:
  replicas: 2
  template:
    metadata:
      labels:
        name: ostinato
    spec:
      containers:
      - name: ostinato
        image: ostinato:latest
        imagePullPolicy: IfNotPresent
        resources:
          requests:
            cpu: 500m
            memory: 128Mi
          limits:
            cpu: 4000m
            memory: 512Mi
        livenessProbe:
          initialDelaySeconds: 15
          httpGet:
            path: "/healthcheck"
            port: 51234
        ports:
        - name: external
          protocol: TCP
          containerPort: 51234

The rationale behind low requests and high limits is that ostinato aggresively parallelises requests but they arrive infrequently, so bursts of usage can be multiplexed on the same Kubernetes minion. Mileage may vary; greater depths are memory hungry.

You’ll need a service to expose the server to your client code. Here’s a service manifest:

---
apiVersion: v1
kind: Service
metadata:
  name: ostinato
spec:
  selector:
    name: ostinato
  ports:
  - name: external
    port: 51234
    targetPort: 51234

Apply both manifests with kubectl:

$ kubectl apply -f ostinato-deployment.yml
$ kubectl apply -f ostinato-service.yml

Your ostinato instances should be reachable via the Kubernetes proxy API, e.g.:

https://kubernetes_domain_name/api/v1/proxy/services/ostinato:51234/healthcheck

Follow the auto-scaling guidelines for dynamic scaling. Here’s a quick trick:

$ kubectl autoscale deployment ostinato --min=2 --max=5

Developing ostinato and final thoughts

Developing ostinato was an amazing experience. I got to learn a lot about chess, a lot about Scala, a lot about OSS and a lot about software engineering.

I believe ostinato has enabled some free and open source chess-related use cases that were unavailable before; if you find them useful I’d be very happy to hear about it.

One thing I wanted to learn was how much I was profiting from OOP (after reading Lawrence Krubner’s famous OOP blogpost). Seeing chess as a poster child for OOP, I started the library as a “turn-based game library” rather than just a “chess library”, where ChessGame extended Game and ChessAction extended Action and so on. The resulting code scares me still.

Another thing I’ve learnt is that OOP and immutable design meet the hardware in a blurring mist, and while the succintness and elegance can aid in real-life problem solving, they also hide big O complexity greatly. At some inflection point, the indirection trade-off is not worth it. While not proud about it, I must link you to some compromises I’ve made to gain acceptable response times.

During extensive refactorings, I’ve learnt to love IntelliJ’s renaming features. If just for that, and with its shortcomings, I think IntelliJ has raised the bar in that respect. I’ve found it as convenient as Sublime Text’s multiple cursors.

I’ve learnt to beware of implicits. While they enable important use cases, they are used way more than they should be. I wasn’t able to completely get rid of them in the ostinato codebase (partly because I was bound by the superclass’ method signatures; thanks OOP), but overall I believe I’ve tamed the beast and learnt a valuable lesson.

Big thank you to Cecilia Fladung for designing the logo, and to the elite reviewers: @echojc, @therealplato and @chris_d_barret.

Thank you for reading this blogpost. KISS!