Radosław Miernik
Open source? Embrace, understand, develop.
AI2Games, November 6th, 2024
Rules
(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)))
)
)
}
#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)
)*
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()
}
}
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);
(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))
Ludii | RBG | HRG | RG | GDL | |
---|---|---|---|---|---|
Definition size | 😃 | 😃 | 😐 | 😐 | 😢 |
Definition complexity | 😐 | 😢 | 😃 | 😐 | 😐 |
Runtime complexity | 😢 | 🫠 | 🙂 | 😃 | 😃 |
Specification complexity | 😢 | 😐 | 😐 | 😃 | 😐 |
Tooling support | 😃 | 😐 | 😃 | 😃 | 😢 |
Translation to RG | 🤷 | 😐 | 😃 | 😢 |
A game consists of five lists:
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.
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.
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 😅
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;
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);
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)
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.
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.
It is basically the same as Regular Games with a few exceptions:
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
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)
board : Position -> Piece = {
P(I, J) = empty
where I in 0..2, J in 0..2
}
graph findNonempty() {
forall p:Position {
check(board[p] == empty)
}
}
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 { ... }
}
graph rules() {
loop {
turn(x)
turn(o)
}
}
Write a game in HRG.
(Scoring will be detailed later.)
By Radosław Miernik
AI2Games lecture on Regular Games, 2024-11-06