Radosław Miernik
Open source? Embrace, understand, develop.
Język Ogólnego Opisu Gier Oparty o Automaty Skończone
DUKAI, 12 grudnia 2025
Osobny program dla każdej gry.
Jeden program dla wszystkich gier.
Niestety, jest to dość niszowa dziedzina.
AAAI 2019
Formalnie, gra jest zadana wyrażeniem regularnym, opisującym wszystkie legalne ciągi ruchów.
Opisywalna klasa gier obejmuje wszystkie skończone turowe gry z pełną informacją. W praktyce mieści się tutaj większość popularnych gry planszowe jak Go czy Szachy, ale nie ma tu gier karcianych jak Poker.
AAAI 2019
ECAI 2020
Formalnie, gra jest złożona z ludemów, opisującym jakąś część gry, jak pionki czy warunek zakończenia.
ECAI 2020
Opisywalna klasa gier obejmuje wszystkie skończone turowe gry z niepełną informacją. W porównaniu z Regular Boardgames możemy opisać Pokera ale też nowoczesne strategiczne gry planszowe jak Catan czy Monopoly.
Formalnie, gra zadana jest automatem skończonym z etykietami na krawędziach, opisującym legalne ciągi ruchów.
Opisywalna klasa gier jest taka sama jak w przypadku Ludii, więc wszystkie skończone turowe gry z niepełną informacją.
Gry z ruchami symultanicznymi można symulować korzystając z niepełnej informacji.
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.
A label is either a skip, an assignment, a comparison, a reachability, or a tag.
turn, move: ? checkForEmpty -> emptyExists;
move, chooseX: player = PlayerOrSystem(playerTurn);
chooseX, choosenX: posX = Coord(*);
choosenX, chooseY: $$ posX;
check, set: board[posX][posY] == Piece(e);
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 comparison is legal when both sides are equal.
A skip, assignment, and tag are always legal.
A reachability is (not) legal when the second node is (not) reachable from the first node. All assignments are discarded afterwards.
Opis gry składa się z:
Sam automat służy nam jako tak zwany forward model, pozwalający symulować rozgrywki.
start: () => State
moves: (State) => Move[]
apply: (State, Move) => State
is_terminal: (State) => boolean
get_scores: (State) => Scores
7
4
7
0
-3
-2
Ruch zaczyna i kończy się gdy gracz przejmuje kontrolę.
Ruch opisujemy ciągiem tagów, czyli specjalnym rodzajem krawędzi.
Pozostałe krawędzie nie są ważne, ale wszystkie muszą być legalne.
Idea języka jest taka sama jak w Regular Games, ale naszym celem jest łatwość użycia:
Haskell
Ada
Haskell
Haskell
JavaScript
Inspiracja
Idea języka jest taka sama jak w Regular Games, ale naszym celem jest łatwość użycia:
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
}
reusable graph existsNonempty() {
position = Position(*)
check(board[position] == empty)
}
graph turn(me: Player) {
player = me
position = Position(*)
check(board[position] == empty)
board[position] = me
$$ position
player = keeper
if reachable(win()) {
goals[me] = 100
goals[op[me]] = 0
end()
}
if not(reachable(existsNonempty())) {
end()
}
}
reusable graph win(p: Position) {
branch {
check(position != next_d1(position))
check(board[position] == board[next_d1(position)])
check(board[position] == board[next_d1(next_d1(position))])
} or {
check(board[position] == board[next_v(position)])
check(board[position] == board[next_v(next_v(position))])
} or { ... }
}
graph rules() {
loop {
turn(x)
turn(o)
}
}
type Position = {
V__0_0, V__0_1, ..., V__0_7,
V__1_0, V__1_1, ..., V__1_7,
...,
V__7_0, V__7_1, ..., V__7_7
};
type PositionOrNull = {
null,
V__0_0, V__0_1, ..., V__0_7,
V__1_0, V__1_1, ..., V__1_7,
...,
V__7_0, V__7_1, ..., V__7_7
};
domain Position = V(X, Y) where X in 0..7, Y in 0..7
domain PositionOrNull = null | Position
HRG
RG
We wszystkich grach, Regular Games jest szybsze niż Regular Boardgames i Ludii.
Co więcej, dużo gier napisanych w Regular Boardgames jest szybsza po translacji do Regular Games.
Interpreter dostępny w przeglądarce jest oczywiście wolniejszy, ale wystarczająco szybki do eksperymentowania.
By Radosław Miernik