boardgames_core/lib/games/checkers.dart
2025-06-14 14:34:26 +02:00

248 lines
6.7 KiB
Dart

import 'package:boardgames_core/commons.dart';
import 'package:boardgames_core/core.dart';
import 'package:logging/logging.dart';
// 4 points to check if checker can move
final List<CellPosition> _jumps = [
CellPosition(-2, -2),
CellPosition(2, -2),
CellPosition(-2, 2),
CellPosition(2, 2),
];
final List<CellPosition> _dirs = [
CellPosition(-1, -1),
CellPosition(1, -1),
CellPosition(-1, 1),
CellPosition(1, 1),
];
class Checker extends Element<Rectangular> {
static final Logger log = Logger("Checker");
@override
bool interactable = true;
@override
bool canMove = true;
List<Checker> killed = [];
Checker(super.position);
/// Moving the pawn from->to position
/// Returns null if move not possible
/// or List of killed pawns (can be empty if moving to empty cell)
List<Checker>? _venturePosition(CellPosition from, CellPosition to) {
if (board == null || !board!.geometry.isOnBoard(to)) {
// Cannot move outside the board
log.finer("Cannot move outside the board $to");
return null;
}
final elems = board!.getElementsAtPosition(to);
// Cannot move where occupied
if (elems.isNotEmpty) {
log.finer("Cannot move where occupied");
return null;
}
final vector = from - to;
// TODO: Calculate settings.canMoveBackwards depending on player? Mirror board
// Need to decide how to render board
// final settings = board!.settings as CheckerSettings;
final distance = from.distance(to).floor();
// Can move to closest adjustent cell if empty (did check elems.isNotEmpty above)
if (distance == 1) {
// Can only move diagonal
return vector.column != 0 && vector.row != 0 ? [] : null;
}
log.finest("Starting tracing back from $to back to $from");
// If distance is greater than one cell then we need to trace back to selected pawn
final owner = this.getOwners().firstOrNull;
List<CellPosition> queue = [to];
Set<CellPosition> visited = new Set();
// TODO: Revisit: Assuming only one path possible in Checkers game
visited.add(to);
Map<CellPosition, CellPosition> path = new Map();
Map<CellPosition, Checker> pawns = new Map();
List<Checker> _traceBack(CellPosition? origin) {
List<Checker> res = [];
CellPosition? it = origin;
log.finest("======");
log.finest(path);
log.finest(pawns);
log.finest("======");
do {
if (pawns.containsKey(it)) {
res.add(pawns[it]!);
}
it = path[it];
} while (it != null);
return res;
}
while (queue.length > 0) {
final origin = queue.removeLast();
// Exit criteria - we traced back our pawn
if (origin == from) {
log.finest("Trace back succeeded!");
return _traceBack(origin);
}
for (final vector in _jumps) {
CellPosition checkPos = origin + vector;
if (!board!.geometry.isOnBoard(checkPos)) {
// Cannot move outside the board
continue;
}
if (visited.contains(checkPos)) {
log.finest("Already visited position $checkPos");
continue;
}
visited.add(checkPos);
var elems = board!.getElementsAtPosition(checkPos);
if (elems.isNotEmpty && checkPos != from) {
// Cannot move if position occupied
// allow original position to pass to fall into exit condition above
log.finest("Checker is blocking $checkPos, skipping");
continue;
}
// Check if we attack someone
final jumpPos = checkPos - vector.decreaseBy(1);
elems = board!.getElementsAtPosition(jumpPos);
final checker = elems.firstOrNull as Checker?;
if (checker == null) {
// Skip if no checkers on the path
log.finest("cannot jump over empty cell at $jumpPos, skipping");
continue;
}
// Check if it is not ours checker
if (owner == null || checker.isOwner(owner)) {
// Cannot attack yourself or jump over empty cell
log.finest(
"Player#${owner == null ? "null" : owner.id} at $jumpPos owns this pawn, skipping");
continue;
} else {
log.finest("You ate checker: $checker ($jumpPos)");
}
log.finest("Can jump to $checkPos (over $jumpPos), will check it too");
path[checkPos] = origin;
pawns[checkPos] = checker;
queue.add(checkPos);
}
}
return null;
}
@override
bool canMoveTo(CellPosition position) {
killed.clear();
bool globalCanMove = super.canMoveTo(position);
(board!.game as Checkers).bank.clear();
if (!globalCanMove) {
return false;
}
final res = this._venturePosition(this.position, position);
if (res == null) {
return false;
}
killed = res;
return true;
}
@override
void moveTo(CellPosition newPosition) {
super.moveTo(newPosition);
board!.game.currentPlayer!.jail.addAll(killed);
for (var elem in killed) {
board!.elements.remove(elem);
}
killed.clear();
}
@override
canSharePosition(Element element) {
// no shared position in checkers
return false;
}
@override
List<CellPosition> getAvailableMoves() {
// TODO: implement getAvailableMoves
throw UnimplementedError();
}
}
class CheckerSettings extends GameSettings {
bool canMoveBackwards = false;
bool canDenyChainAttacks = false;
bool canSwapToQueen = true;
bool queenCanMoveWholeBoard = true;
}
// 8x8 board, 24 checkers (12 per player)
class Checkers extends Game<Rectangular, Checker, CheckerSettings> {
@override
late Board<Rectangular, Checker, CheckerSettings> board;
@override
late int maximumPlayers;
@override
late int requiredPlayers;
@override
late CheckerSettings settings = CheckerSettings();
final List<Checker> bank = [];
Checkers() : super() {
board = Board(Rectangular(8, 8));
maximumPlayers = 2;
requiredPlayers = 2;
// Adding players
players.add(new Player());
players.add(new Player());
board.setGame(this);
// Filling board
int col = -1;
int row = 0;
for (int i = 0; i < 24; i++) {
if (i > 0 && i % 4 == 0) {
row += 1;
col = row % 2 == 0 ? 1 : 0;
} else {
col += 2;
}
final position = CellPosition(col, row);
final checker = new Checker(position);
board.addElementOnBoard(checker);
if (i < 12) {
checker.addOwner(players[0]);
} else {
checker.addOwner(players[1]);
}
if (i == 11) {
row += 2;
}
}
}
@override
List<Player> getWinners() {
return [currentPlayer!];
}
@override
bool hasGameFinished() {
return board.elements.map((e) => e.getOwners().first).toSet().length == 1;
}
}