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 _jumps = [ CellPosition(-2, -2), CellPosition(2, -2), CellPosition(-2, 2), CellPosition(2, 2), ]; final List _dirs = [ CellPosition(-1, -1), CellPosition(1, -1), CellPosition(-1, 1), CellPosition(1, 1), ]; class Checker extends Element { static final Logger log = Logger("Checker"); @override bool interactable = true; @override bool canMove = true; List 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? _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 queue = [to]; Set visited = new Set(); // TODO: Revisit: Assuming only one path possible in Checkers game visited.add(to); Map path = new Map(); Map pawns = new Map(); List _traceBack(CellPosition? origin) { List 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 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 { @override late Board board; @override late int maximumPlayers; @override late int requiredPlayers; @override late CheckerSettings settings = CheckerSettings(); final List 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 getWinners() { return [currentPlayer!]; } @override bool hasGameFinished() { return board.elements.map((e) => e.getOwners().first).toSet().length == 1; } }