< Summary

Information
Class: Safarimacik.Model.Board
Assembly: Safarimacik
File(s): /builds/szofttech-ab-2025/group-03/safarimacik/model/Board.cs
Line coverage
96%
Covered lines: 177
Uncovered lines: 7
Coverable lines: 184
Total lines: 324
Line coverage: 96.1%
Branch coverage
88%
Covered branches: 122
Total branches: 138
Branch coverage: 88.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Item(...)100%11100%
.ctor(...)100%11100%
.ctor(...)93.75%646498.84%
GetLength(...)100%11100%
ChangeTile(...)100%88100%
FindPath(...)73.33%313091.67%
FindPathR(...)100%22100%
FindJeepPath(...)100%1212100%
ShortenFloat(...)75%4486.67%
GetValidNeighbours(...)83.33%191887.5%

File(s)

/builds/szofttech-ab-2025/group-03/safarimacik/model/Board.cs

#LineLine coverage
 1using Godot;
 2
 3
 4namespace Safarimacik.Model;
 5
 6
 7/// <summary>
 8/// Represents a board in the game.
 9/// </summary>
 10/// A 2D grid of tiles, where each tile can be a different type of terrain. Map generation is done when the board is cre
 11public class Board {
 12  private Tile[,] _board;
 13  private IRandomGenerator _rng;
 14
 115  public Tile this[int x, int y] { get { return _board[x, y]; } }
 16
 17  /// <summary>
 18  /// Creates a new board with the X and Y dimensions.
 19  /// </summary>
 20  /// <param name="x">X (horizontal) size of the board.</param>
 21  /// <param name="y">Y (vertical) size of the board.</param>
 22  /// <exception cref="ArgumentOutOfRangeException">The X and Y parameters must be positive.</exception>
 23  /// <exception cref="ArgumentException">The X and Y parameters bust be in a 16, and 9 ratio respectively.</exception>
 24  /// <remarks>
 25  /// The board is generated using Perlin noise to create a natural-looking terrain. The following invariants are used:
 26  /// - The leftmost tile is a Gateway tile with the direction set to false.
 27  /// - The rightmost tile is a Gateway tile with the direction set to true.
 28  /// - The Gateways are connected by a path of Grass tiles, so the player can connect them with a road.
 29  /// - If the board is large enough, at least one river is generated.
 30  /// </remarks>
 131  public Board(int x, int y) : this(x, y, new RandomGenerator()) { }
 32
 33  /// <summary>
 34  /// Creates a new board with the X and Y dimensions.
 35  /// </summary>
 36  /// <param name="x">X (horizontal) size of the board.</param>
 37  /// <param name="y">Y (vertical) size of the board.</param>
 38  /// <exception cref="ArgumentOutOfRangeException">The X and Y parameters must be positive.</exception>
 39  /// <exception cref="ArgumentException">The X and Y parameters bust be in a 16, and 9 ratio respectively.</exception>
 40  /// <remarks>
 41  /// The board is generated using Perlin noise to create a natural-looking terrain. The following invariants are used:
 42  /// - The leftmost tile is a Gateway tile with the direction set to false.
 43  /// - The rightmost tile is a Gateway tile with the direction set to true.
 44  /// - The Gateways are connected by a path of Grass tiles, so the player can connect them with a road.
 45  /// - If the board is large enough, at least one river is generated.
 46  /// </remarks>
 147  public Board(int x, int y, IRandomGenerator random) {
 148    if (x <= 0) throw new ArgumentOutOfRangeException(nameof(x), x, "Board sizes must be positive!");
 149    if (y <= 0) throw new ArgumentOutOfRangeException(nameof(y), y, "Board sizes must be positive!");
 150    if ((float)x / y != 16f / 9) {
 151      throw new ArgumentException("Board size ratio has to be 16:9!");
 52    }
 153    _rng = random;
 54
 155    int maxGenIterations = 20;
 156    do {
 157      maxGenIterations--;
 158      if (maxGenIterations <= 0) {
 059        throw new Exception("Failed to generate a board with a path between the Gateways!");
 60      }
 61
 162      _board = new Tile[x, y];
 63
 164      FastNoiseLite waterNoise = new() {
 165        NoiseType = FastNoiseLite.NoiseTypeEnum.Perlin,
 166        Frequency = 0.05f,
 167        Seed = _rng.RandI(0, 1000),
 168        FractalType = FastNoiseLite.FractalTypeEnum.Fbm,
 169        FractalOctaves = 2,
 170        FractalLacunarity = 2.0f,
 171        FractalGain = 0,
 172      };
 73
 174      FastNoiseLite hillNoise = new() {
 175        NoiseType = FastNoiseLite.NoiseTypeEnum.Perlin,
 176        Frequency = 0.09f,
 177        Seed = _rng.RandI(0, 1000),
 178        FractalType = FastNoiseLite.FractalTypeEnum.Fbm,
 179        FractalOctaves = 2,
 180        FractalLacunarity = 1.0f,
 181        FractalGain = 0,
 182      };
 83
 84      // Define thresholds for different terrain types. A larger threshold makes the tiles rarer.
 185      float waterThreshold = 0.55f;
 186      float hillThreshold = 0.62f;
 87
 188      for (int i = 0; i < x; i++) {
 189        for (int j = 0; j < y; j++) {
 190          float waterNoiseValue = waterNoise.GetNoise2D(i, j);
 191          float hillNoiseValue = hillNoise.GetNoise2D(i, j);
 92
 93          // If the water noise value is above the water threshold, create a Water tile.
 194          if (waterNoiseValue > waterThreshold) {
 195            _board[i, j] = new Water();
 196          } else if (hillNoiseValue > hillThreshold) {
 97            // If the hill noise value is above the hill threshold, first check if the tile is near water.
 198            bool isNearWater = false;
 199            for (int nx = i - 1; nx <= i + 1; nx++) {
 1100              for (int ny = j - 1; ny <= j + 1; ny++) {
 1101                if (nx >= 0 && nx < x && ny >= 0 && ny < y && _board[nx, ny] is Water) {
 1102                  isNearWater = true;
 1103                  break;
 104                }
 1105              }
 1106              if (isNearWater) break;
 1107            }
 108            // If the tile is not near water, create a Hill tile.
 1109            if (!isNearWater) _board[i, j] = new Hill();
 1110          }
 111          // If no tile has been placed in this location, create a Grass tile.
 1112          if (_board[i, j] == null) {
 1113            _board[i, j] = new Grass();
 1114          }
 1115        }
 1116      }
 117
 118      // Create 1-4 rivers
 1119      for (int n = 0; n < _rng.RandI(1, 4); n++) {
 1120        int startX = 0, startY = 0, endX = 0, endY = 0, maxIter = 10;
 121        // Find a random starting point for the river, that is not on a water tile.
 1122        do {
 1123          maxIter--;
 1124          if (maxIter <= 0) break;
 1125          startX = _rng.RandI(2, x - 3);
 1126          startY = _rng.RandI(2, y - 3);
 1127        } while (_board[startX, startY].GetType() == typeof(Water));
 1128        if (maxIter <= 0) continue;
 1129        maxIter = 10;
 130
 131        // Find a random ending point for the river, that is not on a water tile and is at least 10 tiles away, but no f
 1132        do {
 1133          maxIter--;
 1134          if (maxIter <= 0) break;
 1135          endX = _rng.RandI(2, x - 3);
 1136          endY = _rng.RandI(2, y - 3);
 1137        } while (Math.Abs(startX - endX) < 10 || Math.Abs(startY - endY) < 10 || _board[endX, endY].GetType() == typeof(
 1138        if (maxIter <= 0) continue;
 139
 140        // Find a path between the starting and ending points, but only through Grass tiles. Place Water tiles along the
 1141        var river = FindPath(new Vector2I(startX, startY), new Vector2I(endX, endY), false, typeof(Grass));
 1142        if (river.Count > 0) {
 1143          for (int i = 1; i < river.Count - 1; i++) {
 1144            _board[river[i].X, river[i].Y] = new Water();
 1145          }
 1146        }
 1147      }
 148
 1149      _board[0, y / 2] = new Gateway(false);
 1150      _board[x - 1, y / 2] = new Gateway(true);
 1151    } while (
 1152      // If no path is found between the Gateways, try the generation again.
 1153      FindPath(new Vector2I(0, y / 2), new Vector2I(x - 1, y / 2), false, typeof(Grass), typeof(Gateway)).Count == 0
 1154    );
 1155  }
 156
 1157  public int GetLength(int dimension) {
 1158    return _board.GetLength(dimension);
 1159  }
 160
 161  /// <summary>
 162  /// Changes a tile on the board at the given coordinates.
 163  /// </summary>
 164  /// <param name="x">X (horizontal) coordinate of the tile.</param>
 165  /// <param name="y">Y (horizontal) coordinate of the tile.</param>
 166  /// <param name="newTile">Tile to replace the old one with.</param>
 167  /// <exception cref="ArgumentOutOfRangeException">The X and Y parameters must be within the board's dimensions.</excep
 1168  public void ChangeTile(int x, int y, Tile newTile) {
 1169    if (x < 0 || x >= _board.GetLength(0)) {
 1170      throw new ArgumentOutOfRangeException(nameof(x), x, "Must be between 0 and horizontal size - 1!");
 171    }
 1172    if (y < 0 || y >= _board.GetLength(1)) {
 1173      throw new ArgumentOutOfRangeException(nameof(y), y, "Must be between 0 and vertical size - 1!");
 174    }
 1175    _board[x, y] = newTile;
 1176  }
 177
 178  /// <summary>
 179  /// Finds a path between two points on the board using the A* algorithm.
 180  /// </summary>
 181  /// <param name="start">The coordinates of the point where we start the pathfinding.</param>
 182  /// <param name="end">The destination coordinates where we want a route.</param>
 183  /// <param name="weighted">Whether the pathfinding should take into account the travel speed of the different tiles. A
 184  /// <param name="tileTypes">The whitelisted tiles that we can use to pathfind. All tiles are allowed if this parameter
 185  /// <returns>A list of the coordinates on the grid used to get to the destination, where first element is the start po
 186  /// <exception cref="ArgumentOutOfRangeException">The start and end coordinates must be within the board's dimensions.
 1187  public List<Vector2I> FindPath(Vector2I start, Vector2I end, bool weighted, params Type[] tileTypes) {
 1188    if (start.X < 0 || start.Y < 0 || start.X >= _board.GetLength(0) || start.Y >= _board.GetLength(1)) {
 0189      throw new ArgumentOutOfRangeException(nameof(start), start, "Start coordinates must be within the board's dimensio
 190    }
 1191    if (end.X < 0 || end.Y < 0 || end.X >= _board.GetLength(0) || end.Y >= _board.GetLength(1)) {
 0192      throw new ArgumentOutOfRangeException(nameof(end), end, "End coordinates must be within the board's dimensions!");
 193    }
 1194    if (start == end) return [start];
 195
 1196    AStarGrid2D astarGrid = new() {
 1197      Region = new Rect2I(0, 0, _board.GetLength(0), _board.GetLength(1)),
 1198      DiagonalMode = weighted ? AStarGrid2D.DiagonalModeEnum.Always : AStarGrid2D.DiagonalModeEnum.Never,
 1199    };
 1200    astarGrid.Update();
 201
 1202    for (int i = 0; i < _board.GetLength(0); i++) {
 1203      for (int j = 0; j < _board.GetLength(1); j++) {
 1204        if (tileTypes.Length > 0 && !tileTypes.Contains(_board[i, j].GetType())) {
 1205          astarGrid.SetPointSolid(new Vector2I(i, j), true);
 1206        }
 1207        if (weighted) {
 1208          astarGrid.SetPointWeightScale(new Vector2I(i, j), 1 / _board[i, j].Speed);
 1209        }
 1210      }
 1211    }
 212
 1213    List<Vector2I> path = [.. astarGrid.GetIdPath(start, end)];
 1214    return path;
 1215  }
 216
 217  /// <summary>
 218  /// Finds a path between two points on the board using the FindPath method, used for animal pathfinding.
 219  /// </summary>
 220  /// <param name="start">The coordinates of the point where we start the pathfinding.</param>
 221  /// <param name="end">The destination coordinates where we want a route.</param>
 222  /// <param name="weighted">Whether the pathfinding should take into account the travel speed of the different tiles. A
 223  /// <returns></returns>
 1224  public List<Vector2> FindPathR(Vector2I start, Vector2I end, bool weighted) {
 1225    List<Vector2I> path = FindPath(start, end, weighted, typeof(Grass), typeof(Road), typeof(Hill), typeof(Water));
 1226    List<Vector2> ret = [];
 227    Vector2 updated;
 228
 229
 1230    for (int i = 0; i < path.Count; i++) {
 1231      updated = path[i] * 16 + new Vector2(_rng.Rand(0, 15), _rng.Rand(0, 15));
 1232      ret.Add(updated.Clamp(new Vector2(0, 0), new Vector2(GetLength(0) - 1, GetLength(1) - 1) * 16));
 1233    }
 234
 1235    return ret;
 1236  }
 237
 238  /// <summary>
 239  /// Finds a random path of Roads from start to end using DFS algorithm.
 240  /// </summary>
 241  /// <param name="start">The entrance Gateway of the board.</param>
 242  /// <param name="end">The exit Gateway of the board.</param>
 243  /// <param name="shortest">Whether the pathfinding should return the shortest path instead of a random one.</param>
 244  /// <returns>A path as a List of Vector2 points or null of there was none.</returns>
 1245  public List<Vector2>? FindJeepPath(Vector2I start, Vector2I end, bool shortest = false) {
 1246    if (shortest) {
 1247      return ShortenFloat(FindPath(start, end, false, typeof(Road), typeof(Gateway)));
 248    }
 249
 1250    Stack<List<Vector2I>> paths = new();
 1251    HashSet<Vector2I> visited = [];
 252
 1253    paths.Push([start]);
 1254    visited.Add(start);
 255
 1256    while (paths.Count > 0) {
 1257      List<Vector2I> path = paths.Pop();
 1258      Vector2I current = path.Last();
 259
 1260      if (current == end) {
 1261        return ShortenFloat(path);
 262      }
 263
 1264      List<Vector2I> neighbours = [.. GetValidNeighbours(current).Where(n => !visited.Contains(n))];
 265
 1266      neighbours = [.. neighbours.OrderBy(_ => _rng.Rand(1, 100))];
 267
 1268      foreach (Vector2I neighbour in neighbours) {
 1269        visited.Add(neighbour);
 1270        List<Vector2I> newPath = [.. path, neighbour];
 1271        paths.Push(newPath);
 1272      }
 1273    }
 1274    return null;
 1275  }
 276
 277  /// <summary>
 278  /// Converts the points to global position from board indexes, shortens the path to leave only corner points in.
 279  /// </summary>
 280  /// <param name="origin">Original path with board indexes and middle points.</param>
 281  /// <returns>List of significant points of the path</returns>
 1282  private List<Vector2> ShortenFloat(List<Vector2I> origin) {
 1283    List<Vector2> shortened = [origin[0] * 16];
 284
 1285    for (int i = 1; i < origin.Count - 1; i++) {
 1286      Vector2 prev = origin[i - 1];
 1287      Vector2 curr = origin[i];
 1288      Vector2 next = origin[i + 1];
 289
 1290      Vector2 dir1 = prev - curr;
 1291      Vector2 dir2 = curr - next;
 292
 1293      if (dir1 != dir2) {
 0294        shortened.Add(curr * 16);
 0295      }
 1296    }
 297
 1298    shortened.Add(origin.Last() * 16);
 299
 1300    return shortened;
 1301  }
 302
 303  /// <summary>
 304  /// Finds the neighbours of a cell on the board
 305  /// </summary>
 306  /// <param name="position">Cell whose neighbours are searched</param>
 307  /// <returns>List of neighbouring cells</returns>
 1308  private List<Vector2I> GetValidNeighbours(Vector2I position) {
 1309    List<Vector2I> neighbours = [];
 1310    for (int i = -1; i <= 1; i += 2) {
 1311      if (position.X + i >= 0 && position.X + i < _board.GetLength(0)) {
 1312        if (_board[position.X + i, position.Y] is Road || _board[position.X + i, position.Y] is Gateway) {
 1313          neighbours.Add(new Vector2I(position.X + i, position.Y));
 1314        }
 1315      }
 1316      if (position.Y + i >= 0 && position.Y + i < _board.GetLength(1)) {
 1317        if (_board[position.X, position.Y + i] is Road || _board[position.X, position.Y + i] is Gateway) {
 0318          neighbours.Add(new Vector2I(position.X, position.Y + i));
 0319        }
 1320      }
 1321    }
 1322    return neighbours;
 1323  }
 324}