Regular Games

AI2Games, November 6th, 2024

Breakthrough

Rules

  • Played on an 8x8 board with a double contingent of chess pawns.
  • Pieces move forward one orthogonally or diagonally.
  • Pieces can capture by moving diagonally.
  • The first player to reach the opponent's edge of the board wins.
  • A player also can win if they capture all of the opponent's pieces.

Breakthrough

Why it makes a great GGP example?
  • Rules are short.
  • Rules are local (looking at pawns, not the entire board, is enough).
  • Human-playable and similar to a real-life game (Checkers/Droughs).
  • Easily scalable to any rectangular board (defaults to 8x8).

Breakthrough in Ludii

(game "Breakthrough"
  ("TwoPlayersNorthSouth")
  (equipment {
    (board (square 8))
    (piece "Pawn" Each (or { ... }))
    (regions P1 (sites Top))
    (regions P2 (sites Bottom))
  })
  (rules
    (start {
      (place "Pawn1" (expand (sites Bottom)))
      (place "Pawn2" (expand (sites Top)))
    })
    (play (forEach Piece))
    (end ("ReachWin" (sites Mover) Mover))
  )
)
{
  "StepForwardToEmpty"
  (move
    Step
    (directions {FR FL})
    (to if:(or
        (is Empty (to))
        ("IsEnemyAt" (to))
      )
      (apply (remove (to)))
    )
  )
}

Breakthrough in RBG

#players = white(100), black(100)
#pieces = e, w, b
#variables =
#board = rectangle(up,down,left,right,
         [b, b, b, b, b, b, b, b]
         [b, b, b, b, b, b, b, b]
         [e, e, e, e, e, e, e, e]
         [e, e, e, e, e, e, e, e]
         [e, e, e, e, e, e, e, e]
         [e, e, e, e, e, e, e, e]
         [w, w, w, w, w, w, w, w]
         [w, w, w, w, w, w, w, w])
#anySquare = ((up* + down*)(left* + right*))
#turn(me; myPawn; opp; oppPawn; forward) =
    ->me anySquare {myPawn}
    [e] forward ({e} + (left+right) {e,oppPawn})
    ->> [$ me=100] [$ opp=0]
    ({? forward} [myPawn] + {! forward} ->> {})
#rules = (
  turn(white; w; black; b; up)
  turn(black; b; white; w; down)
)*

Breakthrough in HRG

domain Piece = blackpawn | empty | whitepawn
domain Player = black | white
domain Score = 0 | 100
domain Position = null | V(X, Y) where X in 0..7, Y in 0..7

left : Position -> Position
left(null) = null
left(V(X, Y)) = if X == 0 then null else V(X - 1, Y)

right : Position -> Position
right(null) = null
right(V(X, Y)) = if X == 7 then null else V(X + 1, Y)

up : Position -> Position
up(null) = null
up(V(_, 0)) = null
up(V(X, Y)) = V(X, Y - 1)

down : Position -> Position
down(null) = null
down(V(_, 7)) = null
down(V(X, Y)) = V(X, Y + 1)

direction : Player -> Position -> Position
direction(white) = up
direction(_) = down

piece : Player -> Piece
piece(white) = whitepawn
piece(_) = blackpawn

opponent : Player -> Player
opponent(white) = black
opponent(_) = white

board : Position -> Piece = {
  V(X, Y) = if Y < 2
    then blackpawn
    else if Y > 5
      then whitepawn
      else empty
  where X in 0..7, Y in 0..7
}

me : Player = white

position : Position
graph move(me: Player) {
  forall p:Position {
    check(p != null && board[p] == piece(me))
    board[p] = empty
    position = direction(me)(p)
    $ p
  }
  branch {
    check(board[position] == empty)
    $ F
  } or {
    branch {
      position = left(position)
      $ L
    } or {
      position = right(position)
      $ R
    }
    check(position != null)
    check(board[position] != piece[me])
  }
}

graph turn() {
  player = me
  move(me)
  board[position] = piece(me)
  player = keeper
  if direction(me)(position) == null || not(reachable(move(opponent(me)))) {
    goals[me] = 100
    goals[opponent(me)] = 0
    end()
  }
  me = opponent(me)
}

graph rules() {
  loop {
    turn()
  }
}

Breakthrough in RG

type Player = {white, black};
type Score = {0, 100};
type Piece = {e, b, w};
type Board = Position -> Piece;
type PieceOfPlayer = Player -> Piece;
type PieceToBool = Piece -> Bool;
type PlayerToPieceToBool = Player -> PieceToBool;
type Direction = Position -> Position;
type PlayerToPlayer = Player -> Player;
type PlayerToDirection = Player -> Direction;

type Position = {
    null,
    v00, v10, v20, v30, v40, v50, v60, v70,
    v01, v11, v21, v31, v41, v51, v61, v71,
    v02, v12, v22, v32, v42, v52, v62, v72,
    v03, v13, v23, v33, v43, v53, v63, v73,
    v04, v14, v24, v34, v44, v54, v64, v74,
    v05, v15, v25, v35, v45, v55, v65, v75,
    v06, v16, v26, v36, v46, v56, v66, v76,
    v07, v17, v27, v37, v47, v57, v67, v77
};

const left: Direction = {:null,
    v10:v00, v20:v10, v30:v20, v40:v30, v50:v40, v60:v50, v70:v60,
    v11:v01, v21:v11, v31:v21, v41:v31, v51:v41, v61:v51, v71:v61,
    v12:v02, v22:v12, v32:v22, v42:v32, v52:v42, v62:v52, v72:v62,
    v13:v03, v23:v13, v33:v23, v43:v33, v53:v43, v63:v53, v73:v63,
    v14:v04, v24:v14, v34:v24, v44:v34, v54:v44, v64:v54, v74:v64,
    v15:v05, v25:v15, v35:v25, v45:v35, v55:v45, v65:v55, v75:v65,
    v16:v06, v26:v16, v36:v26, v46:v36, v56:v46, v66:v56, v76:v66,
    v17:v07, v27:v17, v37:v27, v47:v37, v57:v47, v67:v57, v77:v67
};
const right: Direction = {...};
const up: Direction = {...};
const down: Direction = {...};

const whiteOrEmpty: PieceToBool = {w:1, e:1, :0};
const blackOrEmpty: PieceToBool = {b:1, e:1, :0};
const opponentOrEmpty: PlayerToPieceToBool = {white:blackOrEmpty, :whiteOrEmpty};
const pieceOfPlayer: PieceOfPlayer = {white:w, :b};
const directionOfPlayer: PlayerToDirection = {white:up, :down};
const opponent: PlayerToPlayer = {white:black, :white};

var turnPlayer: Player = white;
var board: Board = {
    v00:b, v10:b, v20:b, v30:b, v40:b, v50:b, v60:b, v70:b,
    v01:b, v11:b, v21:b, v31:b, v41:b, v51:b, v61:b, v71:b,
    :e,
    v06:w, v16:w, v26:w, v36:w, v46:w, v56:w, v66:w, v76:w,
    v07:w, v17:w, v27:w, v37:w, v47:w, v57:w, v67:w, v77:w
};
var pos: Position = v00;
begin, turn: ;
turn, move: ? move -> moved;
turn, lose: ! move -> moved;

move, selectPos: player = PlayerOrKeeper(turnPlayer);
selectPos, selectedPos(position:Position): $ position;
selectedPos(position:Position), setPos(position:Position): position != Position(null);
setPos(position:Position), setFinished: pos = Position(position);
setFinished, checkOwn: board[pos] == pieceOfPlayer[turnPlayer];
checkOwn, forward: board[pos] = Piece(e);
forward, selectDirection: pos = Position(directionOfPlayer[turnPlayer][pos]);

selectDirection, directionForward: $ F;
directionForward, directionOK: board[pos] == Piece(e);

selectDirection, directionLeft: $ L;
directionLeft, directionLeftChecked: left[pos] != Position(null);
directionLeftChecked, directionOK: pos = Position(left[pos]);

selectDirection, directionRight: $ R;
directionRight, directionRightChecked: right[pos] != Position(null);
directionRightChecked, directionOK: pos = Position(right[pos]);

directionOK, moved: opponentOrEmpty[turnPlayer][board[pos]] == Bool(1);

@unique begin;
@unique move;
@unique selectedPos(position:Position);
@unique directionForward;
@unique directionLeft;
@unique directionRight;
@unique wincheck;

moved, done: board[pos] = pieceOfPlayer[turnPlayer];
done, wincheck: player = PlayerOrKeeper(keeper);

@unique wincheck;

wincheck, win: directionOfPlayer[turnPlayer][pos] == Position(null);
wincheck, continue: directionOfPlayer[turnPlayer][pos] != Position(null);
continue, turn: turnPlayer = opponent[turnPlayer];

lose, win: turnPlayer = opponent[turnPlayer];
win, score: goals[turnPlayer] = Score(100);
score, finish: goals[opponent[turnPlayer]] = Score(0);
finish, end: player = PlayerOrKeeper(keeper);

Breakthrough in GDL

(role white)
(role black)
(<= (base (cellHolds ?x ?y ?p))
    (index ?x)
    (index ?y)
    (role ?p))
(<= (base (control ?p))
    (role ?p))
(<= (input ?p noop)
    (role ?p))
(<= (input white (move ?x ?y1 ?x ?y2))
    (index ?x)
    (succ ?y1 ?y2))
(<= (input white (move ?x1 ?y1 ?x2 ?y2))
    (succ ?y1 ?y2)
    (succ ?x1 ?x2))
(<= (input white (move ?x1 ?y1 ?x2 ?y2))
    (succ ?y1 ?y2)
    (succ ?x2 ?x1))
(<= (input black (move ?x ?y1 ?x ?y2))
    (index ?x)
    (succ ?y2 ?y1))
(<= (input black (move ?x1 ?y1 ?x2 ?y2))
    (succ ?y2 ?y1)
    (succ ?x1 ?x2))
(<= (input black (move ?x1 ?y1 ?x2 ?y2))
    (succ ?y2 ?y1)
    (succ ?x2 ?x1))
(init (cellHolds 1 1 white))
(init (cellHolds 2 1 white))
(init (cellHolds 3 1 white))
(init (cellHolds 4 1 white))
(init (cellHolds 5 1 white))
(init (cellHolds 6 1 white))
(init (cellHolds 7 1 white))
(init (cellHolds 8 1 white))
(init (cellHolds 1 2 white))
(init (cellHolds 2 2 white))
(init (cellHolds 3 2 white))
(init (cellHolds 4 2 white))
(init (cellHolds 5 2 white))
(init (cellHolds 6 2 white))
(init (cellHolds 7 2 white))
(<= (cellEmpty ?x ?y)
    (cell ?x ?y)
    (not (true (cellHolds ?x ?y white)))
    (not (true (cellHolds ?x ?y black))))
(<= (distinctCell ?x1 ?y1 ?x2 ?y2)
    (cell ?x1 ?y1)
    (cell ?x2 ?y2)
    (distinct ?x1 ?x2))
(<= (distinctCell ?x1 ?y1 ?x2 ?y2)
    (cell ?x1 ?y1)
    (cell ?x2 ?y2)
    (distinct ?y1 ?y2))
(<= whiteWin
    (index ?x)
    (true (cellHolds ?x 8 white)))
(<= blackWin
    (index ?x)
    (true (cellHolds ?x 1 black)))
(<= whiteWin
  (not blackCell))
(<= blackWin
  (not whiteCell))
(<= whiteCell
  (cell ?x ?y)
  (true (cellHolds ?x ?y white)))
(<= blackCell
  (cell ?x ?y)
  (true (cellHolds ?x ?y black)))    
(index 1)
(index 2)
(index 3)
(index 4)
(index 5)
(index 6)
(index 7)
(index 8)
(succ 1 2)
(succ 2 3)
(succ 3 4)
(succ 4 5)
(succ 5 6)
(succ 6 7)
(succ 7 8)
(init (cellHolds 8 2 white))
(init (cellHolds 1 7 black))
(init (cellHolds 2 7 black))
(init (cellHolds 3 7 black))
(init (cellHolds 4 7 black))
(init (cellHolds 5 7 black))
(init (cellHolds 6 7 black))
(init (cellHolds 7 7 black))
(init (cellHolds 8 7 black))
(init (cellHolds 1 8 black))
(init (cellHolds 2 8 black))
(init (cellHolds 3 8 black))
(init (cellHolds 4 8 black))
(init (cellHolds 5 8 black))
(init (cellHolds 6 8 black))
(init (cellHolds 7 8 black))
(init (cellHolds 8 8 black))
(init (control white))
(<= (legal white (move ?x ?y1 ?x ?y2))
    (true (control white))
    (true (cellHolds ?x ?y1 white))
    (succ ?y1 ?y2)
    (cellEmpty ?x ?y2))
(<= (legal white (move ?x1 ?y1 ?x2 ?y2))
    (true (control white))
    (true (cellHolds ?x1 ?y1 white))
    (succ ?y1 ?y2)
    (succ ?x1 ?x2)
    (not (true (cellHolds ?x2 ?y2 white))))
(<= (legal white (move ?x1 ?y1 ?x2 ?y2))
    (true (control white))
    (true (cellHolds ?x1 ?y1 white))
    (succ ?y1 ?y2)
    (succ ?x2 ?x1)
    (not (true (cellHolds ?x2 ?y2 white))))
(<= (legal black (move ?x ?y1 ?x ?y2))
    (true (control black))
    (true (cellHolds ?x ?y1 black))
    (succ ?y2 ?y1)
    (cellEmpty ?x ?y2))
(<= (legal black (move ?x1 ?y1 ?x2 ?y2))
    (true (control black))
    (true (cellHolds ?x1 ?y1 black))
    (succ ?y2 ?y1)
    (succ ?x1 ?x2)
    (not (true (cellHolds ?x2 ?y2 black))))
(<= (legal black (move ?x1 ?y1 ?x2 ?y2))
    (true (control black))
    (true (cellHolds ?x1 ?y1 black))
    (succ ?y2 ?y1)
    (succ ?x2 ?x1)
    (not (true (cellHolds ?x2 ?y2 black))))
(<= (legal white noop)
    (true (control black)))
(<= (legal black noop)
    (true (control white)))
(<= (next (cellHolds ?x2 ?y2 ?player))
    (role ?player)
    (does ?player (move ?x1 ?y1 ?x2 ?y2)))
(<= (next (cellHolds ?x3 ?y3 ?state))
    (true (cellHolds ?x3 ?y3 ?state))
    (role ?player)
    (does ?player (move ?x1 ?y1 ?x2 ?y2))
    (distinctCell ?x1 ?y1 ?x3 ?y3)
    (distinctCell ?x2 ?y2 ?x3 ?y3))
(<= (next (control white))
    (true (control black)))
(<= (next (control black))
    (true (control white)))
(<= terminal 
    whiteWin)
(<= terminal
    blackWin)
(<= (goal white 100)
    whiteWin)
(<= (goal white 0)
    (not whiteWin))
(<= (goal black 100)
    blackWin)
(<= (goal black 0)
    (not blackWin))
(<= (cell ?x ?y)
    (index ?x)
    (index ?y))

Comparison

Ludii RBG HRG RG GDL
Definition size 😃 😃 😐 😐 😢
Definition complexity 😐 😢 😃 😐 😐
Runtime complexity 😢 🫠 🙂 😃 😃
Specification complexity 😢 😐 😐 😃 😐
Tooling support 😃 😐 😃 😃 😢
Translation to RG 🤷 😐 😃 😢

Regular Games

A game consists of five lists: 

  1. Typedefs, defining the types used elsewhere.
  2. Variables, storing values.
  3. Constants, mapping keys to values.
  4. Edges, describing the automaton.
  5. Pragmas, hinting some optimizations the interpreter and the compiler.

Regular Games

A typedef is a type alias.
It has an identifier and the type itself.

type Piece = {e, b, w};
type Player = {white, black};
type PlayerPiece = Player -> Piece;
type IsOpponentPiece = Player -> Piece -> Bool;

A type is either a set of symbols or a mapping between two types.

Regular Games

A variable stores values of a given type. It always has an initial value.

var playerTurn: Player = X;
var coord: Coord = 0;
var board: Coord -> Piece = {
    v00:b, v10:w,
    v01:w, v11:b,
    :e
};

A value is either a symbol or a map. Every map has a default value.

Regular Games

A constant is just like a variable but cannot be assigned to.

const whiteOrEmpty: PieceToBool = {w:1, e:1, :0};
const blackOrEmpty: PieceToBool = {b:1, e:1, :0};
const opponentOrEmpty: PlayerToPieceToBool = {
    white:blackOrEmpty,
         :whiteOrEmpty
};

That's it 😅

Regular Games

An edge is a label joining two nodes. A node is a symbol with optional (type) bindings.

A label is either a skip, an assignment, a comparison, a reachability, or a tag.

begin, move: ;

move, selectPos: player = PlayerOrKeeper(turnPlayer);

selectedPos(position:Position), setPos(position:Position):
    position != Position(null);

turn, move: ? move -> moved;

selectPos, selectedPos(position:Position): $ position;

Regular Games

Using bindings allows us to create one edge for every element in a set type.

It may seem like some syntactic sugar, but it also makes more analyses possible.

chooseX, chooseX(coordX: Coord): $ coordX;
chooseX(coordX: Coord), chooseY: posX = Coord(coordX);
chooseX, chooseX__bind__0: $ 0;
chooseX, chooseX__bind__1: $ 1;
chooseX, chooseX__bind__2: $ 2;
chooseX__bind__0, chooseY: posX = Coord(0);
chooseX__bind__1, chooseY: posX = Coord(1);
chooseX__bind__2, chooseY: posX = Coord(2);

Regular Games

Both assignments and comparisons consist of two expressions.

An expression is either a reference, a map access, or a type cast

some_constant[symbol] == TypeName(some_variable)

Regular Games

A move starts when a player is given control ("switched to"). It ends with a switch as well.

A move consists of a series of tags. The exact edges are not important, but all labels have to be legal.

Regular Games

A comparison is legal when both sides are (not) equal.

A skip, assignment, and tag are always legal.

A reachability is legal when the second node is (not) reachable from the first node. All assignments are discarded afterwards.

High-level Regular Games

It is basically the same as Regular Games with a few exceptions:

  1. Types support unions and numeric ranges.
  2. Constants support pattern matching.
  3. Map values are defined as comprehensions.
  4. There are no edges. The automaton is defined in a programming language-like manner.

High-level Regular Games

domain Piece = empty | x | o
domain Player = x | o
domain Position = P(I, J)
    where I in 0..2, J in 0..2
domain Score = 50 | 0 | 100

High-level Regular Games

next_d2 : Position -> Position
next_d2(P(I, J)) = if I + J == 2
    then P((I + 1) % 3, (J - 1) % 3)
    else P(I, J)

next_h : Position -> Position
next_h(P(I, J)) = P(I, (J + 1) % 3)

High-level Regular Games

board : Position -> Piece = {
    P(I, J) = empty
    where I in 0..2, J in 0..2
}

High-level Regular Games

graph findNonempty() {
  forall p:Position {
    check(board[p] == empty)
  }
}

High-level Regular Games

graph turn(me: Player) {
  player = me
  forall p:Position {
    check(board[p] == empty)
    board[p] = me
    position = p
    $ p
  }

  player = keeper
  if reachable(win(position)) {
    goals[me] = 100
    goals[op[me]] = 0
    end()
  }

  if not(reachable(findNonempty())) {
    end()
  }
}
graph win(p: Position) {
  branch {
    check(p != next_d1(p))
    check(board[p] == board[d1(p)])
    check(board[p] == board[d1(d1(p))])
  } or {
    check(p != d2(p))
    check(board[p] == board[d2(p)])
    check(board[p] == board[d2(d2(p))])
  } or { ... }
}

High-level Regular Games

graph rules() {
  loop {
    turn(x)
    turn(o)
  }
}

Assignment

Write a game in HRG.
(Scoring will be detailed later.)

(AI2Games) Regular Games

By Radosław Miernik

(AI2Games) Regular Games

AI2Games lecture on Regular Games, 2024-11-06

  • 40