Initial commit
This commit is contained in:
commit
83eb5ac1c5
44
.gitignore
vendored
Normal file
44
.gitignore
vendored
Normal file
@ -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
|
||||
47
lib/common/cell-position.dart
Normal file
47
lib/common/cell-position.dart
Normal file
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
22
lib/common/geometry.dart
Normal file
22
lib/common/geometry.dart
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
30
lib/common/mixins.dart
Normal file
30
lib/common/mixins.dart
Normal file
@ -0,0 +1,30 @@
|
||||
import 'package:boardgames_core/core.dart';
|
||||
|
||||
mixin Ownable {
|
||||
List<Player> _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<Player> getOwners() {
|
||||
return _owners.sublist(0).toList();
|
||||
}
|
||||
}
|
||||
|
||||
mixin Renderable {
|
||||
void render();
|
||||
}
|
||||
1
lib/common/uuid.dart
Normal file
1
lib/common/uuid.dart
Normal file
@ -0,0 +1 @@
|
||||
|
||||
3
lib/commons.dart
Normal file
3
lib/commons.dart
Normal file
@ -0,0 +1,3 @@
|
||||
export 'common/geometry.dart';
|
||||
export 'common/mixins.dart';
|
||||
export 'common/cell-position.dart';
|
||||
5
lib/core.dart
Normal file
5
lib/core.dart
Normal file
@ -0,0 +1,5 @@
|
||||
export './src/board.dart';
|
||||
export './src/element.dart';
|
||||
export './src/player.dart';
|
||||
export './src/game.dart';
|
||||
export './src/settings.dart';
|
||||
1
lib/games.dart
Normal file
1
lib/games.dart
Normal file
@ -0,0 +1 @@
|
||||
export './games/checkers.dart';
|
||||
247
lib/games/checkers.dart
Normal file
247
lib/games/checkers.dart
Normal file
@ -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<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;
|
||||
}
|
||||
}
|
||||
37
lib/src/board.dart
Normal file
37
lib/src/board.dart
Normal file
@ -0,0 +1,37 @@
|
||||
import 'package:boardgames_core/commons.dart';
|
||||
import 'package:boardgames_core/core.dart';
|
||||
|
||||
class Board<G extends Geometry, E extends Element, S extends GameSettings> {
|
||||
final G geometry;
|
||||
final List<E> 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<E> 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;
|
||||
}
|
||||
}
|
||||
44
lib/src/element.dart
Normal file
44
lib/src/element.dart
Normal file
@ -0,0 +1,44 @@
|
||||
import 'package:boardgames_core/core.dart';
|
||||
import 'package:boardgames_core/commons.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
abstract class Element<G extends Geometry> with Ownable {
|
||||
static final uuid = Uuid();
|
||||
final CellPosition initialPosition;
|
||||
final String id;
|
||||
CellPosition position;
|
||||
Board<G, Element, GameSettings>? 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<CellPosition> getAvailableMoves();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "${this.runtimeType.toString()}#${position}";
|
||||
}
|
||||
}
|
||||
38
lib/src/game.dart
Normal file
38
lib/src/game.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:boardgames_core/core.dart';
|
||||
import 'package:boardgames_core/commons.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
abstract class Game<G extends Geometry, E extends Element,
|
||||
S extends GameSettings> {
|
||||
static final uuid = Uuid();
|
||||
int turn = 0;
|
||||
abstract Board<G, E, S> board;
|
||||
abstract int requiredPlayers;
|
||||
abstract int maximumPlayers;
|
||||
abstract S settings;
|
||||
final String id;
|
||||
List<Player> players = [];
|
||||
Player? currentPlayer;
|
||||
|
||||
Game(): id = uuid.v4();
|
||||
|
||||
Future<void> beginTurn() async {
|
||||
if (hasGameFinished()) {
|
||||
return;
|
||||
}
|
||||
await beforeTurn();
|
||||
currentPlayer = players[turn % players.length];
|
||||
}
|
||||
|
||||
Future<void> endTurn() async {
|
||||
await afterTurn();
|
||||
if (!hasGameFinished()) {
|
||||
turn += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> beforeTurn() async {}
|
||||
Future<void> afterTurn() async {}
|
||||
bool hasGameFinished();
|
||||
List<Player> getWinners();
|
||||
}
|
||||
11
lib/src/player.dart
Normal file
11
lib/src/player.dart
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
import 'package:boardgames_core/core.dart';
|
||||
|
||||
class Player {
|
||||
static int _counter = 0;
|
||||
final int id;
|
||||
final List<Element> bank = [];
|
||||
final List<Element> jail = [];
|
||||
|
||||
Player() : id = ++_counter;
|
||||
}
|
||||
3
lib/src/settings.dart
Normal file
3
lib/src/settings.dart
Normal file
@ -0,0 +1,3 @@
|
||||
abstract class GameSettings {
|
||||
// Some common settings?
|
||||
}
|
||||
53
pubspec.lock
Normal file
53
pubspec.lock
Normal file
@ -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"
|
||||
10
pubspec.yaml
Normal file
10
pubspec.yaml
Normal file
@ -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'
|
||||
Loading…
x
Reference in New Issue
Block a user