From 83eb5ac1c5043f2f4ceff4333b5691402cb4ff16 Mon Sep 17 00:00:00 2001 From: savinmax Date: Sat, 14 Jun 2025 14:34:26 +0200 Subject: [PATCH] Initial commit --- .gitignore | 44 ++++++ lib/common/cell-position.dart | 47 +++++++ lib/common/geometry.dart | 22 +++ lib/common/mixins.dart | 30 +++++ lib/common/uuid.dart | 1 + lib/commons.dart | 3 + lib/core.dart | 5 + lib/games.dart | 1 + lib/games/checkers.dart | 247 ++++++++++++++++++++++++++++++++++ lib/src/board.dart | 37 +++++ lib/src/element.dart | 44 ++++++ lib/src/game.dart | 38 ++++++ lib/src/player.dart | 11 ++ lib/src/settings.dart | 3 + pubspec.lock | 53 ++++++++ pubspec.yaml | 10 ++ 16 files changed, 596 insertions(+) create mode 100644 .gitignore create mode 100644 lib/common/cell-position.dart create mode 100644 lib/common/geometry.dart create mode 100644 lib/common/mixins.dart create mode 100644 lib/common/uuid.dart create mode 100644 lib/commons.dart create mode 100644 lib/core.dart create mode 100644 lib/games.dart create mode 100644 lib/games/checkers.dart create mode 100644 lib/src/board.dart create mode 100644 lib/src/element.dart create mode 100644 lib/src/game.dart create mode 100644 lib/src/player.dart create mode 100644 lib/src/settings.dart create mode 100644 pubspec.lock create mode 100644 pubspec.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/lib/common/cell-position.dart b/lib/common/cell-position.dart new file mode 100644 index 0000000..85de5b9 --- /dev/null +++ b/lib/common/cell-position.dart @@ -0,0 +1,47 @@ +import 'dart:math'; + +class CellPosition { + final int column; + final int row; + const CellPosition(this.column, this.row); + + double distance(CellPosition to) { + return sqrt(pow(to.column - column, 2) + pow(to.row - row, 2)); + } + + CellPosition decreaseBy(int diff) { + int newCol = + column < 0 ? column + diff * diff.sign : column - diff * diff.sign; + int newRow = row < 0 ? row + diff * diff.sign : row - diff * diff.sign; + + return CellPosition(newCol, newRow); + } + + @override + int get hashCode => "${column}:${row}".hashCode; + + bool operator ==(Object pos) { + return pos is CellPosition && this.column == pos.column && this.row == pos.row; + } + + CellPosition operator -(CellPosition pos) { + return CellPosition(column - pos.column, row - pos.row); + } + + CellPosition operator +(CellPosition pos) { + return CellPosition(column + pos.column, row + pos.row); + } + + @override + String toString() { + return "Position(${column}x${row})"; + } + + toJSON() { + return { + "type": "BoardCell", + "column": column, + "row": row, + }; + } +} diff --git a/lib/common/geometry.dart b/lib/common/geometry.dart new file mode 100644 index 0000000..3fd237e --- /dev/null +++ b/lib/common/geometry.dart @@ -0,0 +1,22 @@ +import 'package:boardgames_core/commons.dart'; + +// Hexagon | Square | Circle | ... +abstract class Geometry { + const Geometry(); + bool isOnBoard(CellPosition position); +} + +class Rectangular extends Geometry { + final int columns; + final int rows; + + const Rectangular(this.columns, this.rows); + + @override + bool isOnBoard(CellPosition position) { + return position.column >= 0 && + position.row >= 0 && + position.column < columns && + position.row < rows; + } +} diff --git a/lib/common/mixins.dart b/lib/common/mixins.dart new file mode 100644 index 0000000..0edc536 --- /dev/null +++ b/lib/common/mixins.dart @@ -0,0 +1,30 @@ +import 'package:boardgames_core/core.dart'; + +mixin Ownable { + List _owners = []; + + int addOwner(Player player) { + _owners.add(player); + return _owners.length; + } + + removeOwner(Player player) { + _owners.remove(player); + } + + bool isOwner(Player player) { + return _owners.contains(player); + } + + bool hasOwners() { + return _owners.length > 0; + } + + List getOwners() { + return _owners.sublist(0).toList(); + } +} + +mixin Renderable { + void render(); +} diff --git a/lib/common/uuid.dart b/lib/common/uuid.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/common/uuid.dart @@ -0,0 +1 @@ + diff --git a/lib/commons.dart b/lib/commons.dart new file mode 100644 index 0000000..36ab300 --- /dev/null +++ b/lib/commons.dart @@ -0,0 +1,3 @@ +export 'common/geometry.dart'; +export 'common/mixins.dart'; +export 'common/cell-position.dart'; diff --git a/lib/core.dart b/lib/core.dart new file mode 100644 index 0000000..600e2ff --- /dev/null +++ b/lib/core.dart @@ -0,0 +1,5 @@ +export './src/board.dart'; +export './src/element.dart'; +export './src/player.dart'; +export './src/game.dart'; +export './src/settings.dart'; \ No newline at end of file diff --git a/lib/games.dart b/lib/games.dart new file mode 100644 index 0000000..e3c5721 --- /dev/null +++ b/lib/games.dart @@ -0,0 +1 @@ +export './games/checkers.dart'; diff --git a/lib/games/checkers.dart b/lib/games/checkers.dart new file mode 100644 index 0000000..d91439a --- /dev/null +++ b/lib/games/checkers.dart @@ -0,0 +1,247 @@ +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; + } +} diff --git a/lib/src/board.dart b/lib/src/board.dart new file mode 100644 index 0000000..9a0bc68 --- /dev/null +++ b/lib/src/board.dart @@ -0,0 +1,37 @@ +import 'package:boardgames_core/commons.dart'; +import 'package:boardgames_core/core.dart'; + +class Board { + final G geometry; + final List elements = []; + late Game game; + + Board(this.geometry); + + setGame(Game g) { + game = g; + } + + addElementOnBoard(E element) { + var board = element.board; + element.board = this; + if (!element.canMoveTo(element.position)) { + element.board = board; + throw new Exception( + "Cannot add element to ${element.position} on the board. There are elements that cannot share this position with it."); + } + elements.add(element); + } + + List getElementsAtPosition(CellPosition position) { + // TODO: optimize with hash table + return this + .elements + .where((element) => element.position == position) + .toList(); + } + + S get settings { + return game.settings as S; + } +} diff --git a/lib/src/element.dart b/lib/src/element.dart new file mode 100644 index 0000000..1d398aa --- /dev/null +++ b/lib/src/element.dart @@ -0,0 +1,44 @@ +import 'package:boardgames_core/core.dart'; +import 'package:boardgames_core/commons.dart'; +import 'package:uuid/uuid.dart'; + +abstract class Element with Ownable { + static final uuid = Uuid(); + final CellPosition initialPosition; + final String id; + CellPosition position; + Board? board; + abstract bool interactable; + abstract bool canMove; + + Element(this.position) + : initialPosition = position, + id = uuid.v4(); + + /// Implements rules of the Game + canSharePosition(Element element); + + bool canMoveTo(CellPosition position) { + if (board == null) { + throw new Exception("Element is not on the board"); + } + if (!interactable || !canMove) { + return false; + } + final elements = board!.getElementsAtPosition(position); + return !elements.any((element) => !element.canSharePosition(this)); + } + + void moveTo(CellPosition newPosition) { + if (canMoveTo(newPosition)) { + position = newPosition; + } + } + + List getAvailableMoves(); + + @override + String toString() { + return "${this.runtimeType.toString()}#${position}"; + } +} diff --git a/lib/src/game.dart b/lib/src/game.dart new file mode 100644 index 0000000..915500d --- /dev/null +++ b/lib/src/game.dart @@ -0,0 +1,38 @@ +import 'package:boardgames_core/core.dart'; +import 'package:boardgames_core/commons.dart'; +import 'package:uuid/uuid.dart'; + +abstract class Game { + static final uuid = Uuid(); + int turn = 0; + abstract Board board; + abstract int requiredPlayers; + abstract int maximumPlayers; + abstract S settings; + final String id; + List players = []; + Player? currentPlayer; + + Game(): id = uuid.v4(); + + Future beginTurn() async { + if (hasGameFinished()) { + return; + } + await beforeTurn(); + currentPlayer = players[turn % players.length]; + } + + Future endTurn() async { + await afterTurn(); + if (!hasGameFinished()) { + turn += 1; + } + } + + Future beforeTurn() async {} + Future afterTurn() async {} + bool hasGameFinished(); + List getWinners(); +} diff --git a/lib/src/player.dart b/lib/src/player.dart new file mode 100644 index 0000000..1dc94a2 --- /dev/null +++ b/lib/src/player.dart @@ -0,0 +1,11 @@ + +import 'package:boardgames_core/core.dart'; + +class Player { + static int _counter = 0; + final int id; + final List bank = []; + final List jail = []; + + Player() : id = ++_counter; +} diff --git a/lib/src/settings.dart b/lib/src/settings.dart new file mode 100644 index 0000000..71c55c6 --- /dev/null +++ b/lib/src/settings.dart @@ -0,0 +1,3 @@ +abstract class GameSettings { + // Some common settings? +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..2583472 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,53 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + logging: + dependency: "direct main" + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: e03928880bdbcbf496fb415573f5ab7b1ea99b9b04f669c01104d085893c3134 + url: "https://pub.dev" + source: hosted + version: "4.0.0" +sdks: + dart: ">=3.1.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..be0ff9d --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,10 @@ +name: boardgames_core +version: 0.1.0 +description: Board games core + +dependencies: + uuid: ^4.0.0 + logging: ^1.2.0 + +environment: + sdk: '>=3.1.0 <4.0.0'