Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add board class similar to field but without the tuples #27

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions test/board_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Import the test package
import 'package:test/test.dart';
import '../utils/board.dart';

/// Board tests
/// Needs more tests
void main() {
// Group your tests together
group('Board Tests', () {
// Test case for addition of positive numbers
test('create a board and validate', () {
final board = Board<String>(
field: [
['00', '01', '02'],
['10', '11', '12'],
['20', '21', '22'],
],
); // Assuming Board is a class you want to test

expect(board.boardWidth, equals(3));
expect(board.boardHeight, equals(3));
expect(board.getValueAt(row: 1, col: 1), equals('11'));
expect(board.getRow(0), equals(['00', '01', '02']));
expect(board.getColumn(0), equals(['00', '10', '20']));
expect(
board.adjacent(const AbsoluteCoordinate(row: 0, col: 0)),
equals([
const AbsoluteCoordinate(row: 0, col: 1),
const AbsoluteCoordinate(row: 1, col: 0),
]),
);
});

test('AbsoluteCoordinate', () {
const coord = AbsoluteCoordinate(row: 2, col: 3);
expect(coord.row, equals(2));
expect(coord.col, equals(3));
const coord2 = AbsoluteCoordinate(row: 2, col: 3);
expect(coord, equals(coord2));
const coord3 = AbsoluteCoordinate(row: 3, col: 3);
expect(coord, isNot(coord3));

const coord4 = OffsetCoordinate(row: 2, col: 3);
expect(coord, isNot(coord4));
});

test('Offsetoordinate', () {
const coord = OffsetCoordinate(row: 2, col: 3);
expect(coord.row, equals(2));
expect(coord.col, equals(3));
const coord2 = OffsetCoordinate(row: 2, col: 3);
expect(coord, equals(coord2));
const coord3 = OffsetCoordinate(row: 3, col: 3);
expect(coord, isNot(coord3));

const coord4 = AbsoluteCoordinate(row: 2, col: 3);
expect(coord, isNot(coord4));

expect(
coord.absoluteFrom(const AbsoluteCoordinate(row: 1, col: 1)),
equals(const AbsoluteCoordinate(row: 3, col: 4)),
);
});
});
}
329 changes: 329 additions & 0 deletions utils/board.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
import 'package:meta/meta.dart';
import 'package:quiver/iterables.dart';

import 'field.dart';

/// A variation of the Field with some helper classes
/// Using model objects instead of Tuples

/// Represents a single fixed location on a [Board]
@immutable
abstract class Coordinate {
const Coordinate({required this.row, required this.col});
final int row;
final int col;

@override
bool operator ==(Object other) =>
other is Coordinate &&
other.runtimeType == runtimeType &&
other.row == row &&
other.col == col;

@override
int get hashCode => 'row:$row,col:$col'.hashCode;

@override
String toString() {
return '{ "row": $row , "col": $col}';
}
}

/// Relative coordinate usually offset from some other location
class AbsoluteCoordinate extends Coordinate {
const AbsoluteCoordinate({required super.row, required super.col});

@override
// ignore: hash_and_equals
bool operator ==(Object other) =>
other is AbsoluteCoordinate &&
other.runtimeType == runtimeType &&
other.row == row &&
other.col == col;
}

/// Relative coordinate usually offset from some other location
class OffsetCoordinate extends Coordinate {
const OffsetCoordinate({required super.row, required super.col});

@override
// ignore: hash_and_equals
bool operator ==(Object other) =>
other is OffsetCoordinate &&
other.runtimeType == runtimeType &&
other.row == row &&
other.col == col;

/// what the offset looks like to the reciever of the offset
/// Only works because no diagonals
OffsetCoordinate get invert => OffsetCoordinate(row: row * -1, col: col * -1);

/// returns a coordinate resulting from this offset applied to the parameter
Coordinate absoluteFrom(AbsoluteCoordinate relativeTo) =>
AbsoluteCoordinate(row: relativeTo.row + row, col: relativeTo.col + col);
}

/// defines a transition into or out of a square
/// Can be relative or absolute inbound
@immutable
class Transition<IT extends Coordinate, OT extends Coordinate> {
const Transition({required this.from, required this.to});
// relative or absolute originating square
final IT from;
// relative or absolute target square
final OT to;

@override
bool operator ==(Object other) =>
other is Transition<IT, OT> &&
other.runtimeType == runtimeType &&
other.from == from &&
other.to == to;

@override
int get hashCode => 'from:$from,to:$to'.hashCode;

@override
String toString() {
return '{ "from": $from , "to": $to}';
}
}

/// used for inbound relative transitions to a fixed square
/// (-1,-2) => (3,4) ===> originated from (2,2)
///
class OffsetInboundTransition extends Transition<OffsetCoordinate, Coordinate> {
const OffsetInboundTransition({required super.from, required super.to});
}

/// used for oubound transitions relative from a square
/// (3,4) => (-1,-2) ===> ended on (2,2)
///
class OffsetOutboundTransition
extends Transition<Coordinate, OffsetCoordinate> {
const OffsetOutboundTransition({required super.from, required super.to});
}

/// an entry path and the relative exit paths for that entry
/// used to define possible exits based on inbound transition
///
/// @param type used for debugging.
/// @param from the entry vector
/// @param to a list of possible exits
///
@immutable
class EntryExitsTransitionDef {
const EntryExitsTransitionDef({
required this.type,
required this.from,
required this.to,
});
// used for tracking and logging
final String type;
// the relative location of the entry into the square
final OffsetCoordinate from;
// the relative exit directions - a given entry can have multiple exiting
final List<OffsetCoordinate> to;

@override
String toString() {
return '{"from": $from , "to": $to}';
}
}

/// Holds all the entry paths for a single square and the exits for each
/// Used to map possible exits for all possible entries
class SquareTransitionDefs {
const SquareTransitionDefs({required this.symbol, required this.allDefs});
final String symbol;
final List<EntryExitsTransitionDef> allDefs;

@override
String toString() {
return '"$symbol" $allDefs';
}
}

/// Used when we want to capture a position and an int like
/// an increment or step number that it was created in
@immutable
class StepCoordinate {
const StepCoordinate({required this.step, required this.location});

final int step;
final Coordinate location;

@override
bool operator ==(Object other) =>
other is StepCoordinate &&
other.runtimeType == runtimeType &&
other.location.row == location.row &&
other.location.col == location.col &&
other.step == step;

@override
int get hashCode =>
'step:$step row:${location.row},col:${location.col}'.hashCode;

@override
String toString() {
return '{ "step": $step, "row": ${location.row} , "col": ${location.col}}';
}
}

/// this is a version of [Field] cast to work with only coordinates
///

/// A version of the Field callback that takes a [Coordinate]
typedef VoidFieldCallback = void Function(Coordinate position);

/// A helper class for easier work with 2D data.
/// 1. expects a data structure to be in row major order
/// 1. expects to be rectangular with no missin gentries
/// 1. expects to be in row major order
freemansoft marked this conversation as resolved.
Show resolved Hide resolved
///
/// The board size is immutable
///
class Board<T> {
Board({required List<List<T>> field})
// Future: Validate all rows are same length
: assert(field.isNotEmpty, 'Field must not be empty'),
assert(field[0].isNotEmpty, 'First position must not be empty'),
// creates a deep copy by value from given field to prevent
// unwanted side effects in the original board
board = List<List<T>>.generate(
field.length,
(row) => List<T>.generate(field[0].length, (col) => field[row][col]),
);

final List<List<T>> board;

int get boardWidth => board.length;
int get boardHeight => board[0].length;

/// Returns the value at the given position.
T getValueAtPosition({required Coordinate position}) {
return board[position.row][position.col];
}

/// Returns the value at the given coordinates.
T getValueAt({required int row, required int col}) =>
getValueAtPosition(position: AbsoluteCoordinate(row: row, col: col));

/// Sets the value at the given Position.
void setValueAtPosition({required Coordinate position, required T value}) {
board[position.row][position.col] = value;
}

/// Sets the value at the given coordinates.
void setValueAt({required int row, required int col, required T value}) =>
setValueAtPosition(
position: AbsoluteCoordinate(row: row, col: col),
value: value,
);

/// Returns whether the given position is inside of this field.
bool isOnboard({required Coordinate position}) {
return position.row >= 0 &&
position.col >= 0 &&
position.col < boardWidth &&
position.row < boardHeight;
}

/// Returns the whole row with given row index.
Iterable<T> getRow(int row) => board[row];

/// Returns the whole column with given column index.
Iterable<T> getColumn(int column) => board.map((row) => row[column]);

/// Returns the minimum value in this field.
T get minValue => min<T>(board.expand((element) => element))!;

/// Returns the maximum value in this field.
T get maxValue => max<T>(board.expand((element) => element))!;

/// Executes the given callback for every position on this field.
void forEach(VoidFieldCallback callback) {
for (var row = 0; row < boardHeight; row++) {
for (var col = 0; col < boardWidth; col++) {
callback(AbsoluteCoordinate(row: row, col: col));
}
}
}

/// Executes the given callback for the passed in positions.
void forPositions(
Iterable<Coordinate> positions,
VoidFieldCallback callback,
) {
for (final position in positions) {
callback(position);
}
}

/// Returns the number of occurrences of given object in this field.
int count(T searched) => board
.expand((element) => element)
.fold<int>(0, (acc, elem) => elem == searched ? acc + 1 : acc);

/// Returns all adjacent cells to the given position.
/// This does **NOT** include diagonal neighbours.
Iterable<Coordinate> adjacent(Coordinate target) {
return <Coordinate>{
AbsoluteCoordinate(row: target.row - 1, col: target.col),
AbsoluteCoordinate(row: target.row, col: target.col + 1),
AbsoluteCoordinate(row: target.row + 1, col: target.col),
AbsoluteCoordinate(row: target.row, col: target.col - 1),
}..removeWhere(
(position) {
return position.row < 0 ||
position.col < 0 ||
position.col >= boardWidth ||
position.row >= boardHeight;
},
);
}

/// Returns all positional neighbours of a point. This includes the adjacent
/// **AND** diagonal neighbours.
Iterable<Coordinate> neighbours(Coordinate target) {
return <Coordinate>{
AbsoluteCoordinate(row: target.row - 1, col: target.col),
AbsoluteCoordinate(row: target.row - 1, col: target.col + 1),
AbsoluteCoordinate(row: target.row, col: target.col + 1),
AbsoluteCoordinate(row: target.row + 1, col: target.col + 1),
AbsoluteCoordinate(row: target.row + 1, col: target.col),
AbsoluteCoordinate(row: target.row + 1, col: target.col - 1),
AbsoluteCoordinate(row: target.row, col: target.col - 1),
AbsoluteCoordinate(row: target.row - 1, col: target.col - 1),
}..removeWhere(
(position) {
return position.row < 0 ||
position.col < 0 ||
position.col >= boardWidth ||
position.row >= boardHeight;
},
);
}

/// Returns a deep copy by value of this [Board].
Board<T> copy() {
final newField = List<List<T>>.generate(
boardHeight,
(row) => List<T>.generate(boardWidth, (col) => board[row][col]),
);
return Board<T>(field: newField);
}

@override
String toString() {
final result = StringBuffer();
for (final row in board) {
for (final elem in row) {
result.write(elem.toString());
}
result.write('\n');
}
return result.toString();
}
}