Skip to content

Commit

Permalink
add tile builder, expand grid
Browse files Browse the repository at this point in the history
  • Loading branch information
roy-t committed Apr 16, 2024
1 parent 6a1e040 commit 9699d8f
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 76 deletions.
5 changes: 5 additions & 0 deletions LibGame.Tests/Tiles/TileUtilitiesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace LibGame.Tests.Tiles;
public static class TileUtilitiesTests
{

}
130 changes: 129 additions & 1 deletion LibGame/Collections/Grid.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using LibGame.Mathematics;
using System.Diagnostics;
using LibGame.Mathematics;
using LibGame.Tiles;

namespace LibGame.Collections;

Expand All @@ -10,12 +12,133 @@ public interface IReadOnlyGrid<T>
public int Columns { get; }
public int Rows { get; }
public int Count { get; }

public (int column, int row) GetNeighbourIndex(int column, int row, TileSide side)
{
var (nc, nr) = side switch
{
TileSide.North => (column + 0, row - 1),
TileSide.East => (column + 1, row + 0),
TileSide.South => (column + 0, row + 1),
TileSide.West => (column - 1, row + 0),
_ => throw new ArgumentOutOfRangeException(nameof(side))
};

if (nc < 0 || nc >= this.Columns)
{
throw new ArgumentOutOfRangeException(nameof(side));
}

if (nr < 0 || nr >= this.Rows)
{
throw new ArgumentOutOfRangeException(nameof(side));
}

return (nc, nr);
}

public IReadOnlyGrid<T> Slice(int columnOffset, int columnSpan, int rowOffset, int rowSpan);

public IReadOnlyGrid<T> SliceAtMost(int columnOffset, int columnSpan, int rowOffset, int rowSpan)
{
columnSpan = Math.Min(this.Columns - columnOffset, columnSpan);
rowSpan = Math.Min(this.Rows - rowOffset, rowSpan);

return this.Slice(columnOffset, columnSpan, rowOffset, rowSpan);
}
}

public readonly struct ReadOnlyGridSlice<T> : IReadOnlyGrid<T>
{
private readonly int ColumnOffset;
private readonly int ColumnSpan;
private readonly int RowOffset;
private readonly int RowSpan;

private readonly T[] tiles;
private readonly int stride;

internal ReadOnlyGridSlice(T[] tiles, int stride, int columnOffset, int columnSpan, int rowOffset, int rowSpan)
{
Debug.Assert(stride > 0);
Debug.Assert(stride <= tiles.Length);

Debug.Assert(columnOffset >= 0);
Debug.Assert(columnSpan > 0);

Debug.Assert(rowOffset >= 0);
Debug.Assert(rowSpan > 0);

this.tiles = tiles;
this.stride = stride;

this.ColumnOffset = columnOffset;
this.ColumnSpan = columnSpan;
this.RowOffset = rowOffset;
this.RowSpan = rowSpan;
}

public T this[int index]
{
get
{
var (c, r) = Indexes.ToTwoDimensional(index, this.ColumnSpan);
return this[c, r];
}
}

public T this[int column, int row]
{
get
{
var c = column + this.ColumnOffset;
var r = row + this.RowOffset;

if (c < this.ColumnOffset || c >= this.ColumnOffset + this.ColumnSpan)
{
throw new ArgumentOutOfRangeException(nameof(column));
}

if (r < this.RowOffset || r >= this.RowOffset + this.RowSpan)
{
throw new ArgumentOutOfRangeException(nameof(row));
}

return this.tiles[Indexes.ToOneDimensional(c, r, this.stride)];
}
}

public int Columns => this.ColumnSpan;
public int Rows => this.RowSpan;
public int Count => this.ColumnSpan * this.RowSpan;

public IReadOnlyGrid<T> Slice(int columnOffset, int columnSpan, int rowOffset, int rowSpan)
{
if (columnSpan > this.ColumnSpan)
{
throw new ArgumentOutOfRangeException(nameof(columnSpan));
}

if (rowSpan > this.RowSpan)
{
throw new ArgumentOutOfRangeException(nameof(rowSpan));
}

return new ReadOnlyGridSlice<T>(this.tiles, this.stride,
this.ColumnOffset + columnOffset,
columnSpan,
this.RowOffset + rowOffset,
rowSpan);
}
}

public sealed class Grid<T> : IReadOnlyGrid<T>
{
private readonly T[] Tiles;

public Grid(int columns, int rows)
: this(new T[columns * rows], columns, rows) { }

public Grid(T[] tiles, int columns, int rows)
{
this.Tiles = tiles;
Expand Down Expand Up @@ -43,6 +166,11 @@ public IReadOnlyGrid<T> AsReadOnly()
{
return this;
}

public IReadOnlyGrid<T> Slice(int columnOffset, int columnSpan, int rowOffset, int rowSpan)
{
return new ReadOnlyGridSlice<T>(this.Tiles, this.Columns, columnOffset, columnSpan, rowOffset, rowSpan);
}
}


132 changes: 132 additions & 0 deletions LibGame/Tiles/TileBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using LibGame.Collections;
using LibGame.Mathematics;

namespace LibGame.Tiles;
public static class TileBuilder
{
public static Grid<Tile> FromHeightMap(IReadOnlyGrid<byte> heightMap)
{
var count = heightMap.Count;
var columns = heightMap.Columns;
var rows = heightMap.Rows;
var tiles = new Grid<Tile>(columns, rows);

// First tile
tiles[0, 0] = new Tile(heightMap[0, 0]);

// First row
for (var i = 1; i < columns; i++)
{
var previous = tiles[i - 1, 0];
var neighbourhood = heightMap.SliceAtMost(i - 1, 3, 0, 2);
tiles[i, 0] = FitFirstRow(previous, neighbourhood, heightMap[i, 0]);
}

// First column
for (var i = 1; i < rows; i++)
{
var previous = tiles[0, i - 1];
var neighbourhood = heightMap.SliceAtMost(0, 2, i - 1, 3);
tiles[0, i] = FitFirstColumn(previous, neighbourhood, heightMap[0, i]);
}

// Fill
for (var i = 0; i < count; i++)
{
var (c, r) = Indexes.ToTwoDimensional(i, columns);
if (c > 0 && r > 0)
{
// TODO: why do we need this cast as tiles implements this interface!
var nwNeighbours = ((IReadOnlyGrid<Tile>)tiles).SliceAtMost(c - 1, 3, r - 1, 3);
var seNeighbours = heightMap.SliceAtMost(c - 1, 3, r - 1, 3);

tiles[i] = Fit(nwNeighbours, seNeighbours, heightMap[c, r]);
}
}

return tiles;
}

private static Tile FitFirstRow(Tile tile, IReadOnlyGrid<byte> neighbourhood, byte baseHeight)
{
var east = neighbourhood.Columns > 2 && neighbourhood.Rows > 0 ? neighbourhood[2, 0] : baseHeight;
var southEast = neighbourhood.Columns > 2 && neighbourhood.Rows > 1 ? neighbourhood[2, 1] : baseHeight;
var south = neighbourhood.Columns > 1 && neighbourhood.Rows > 1 ? neighbourhood[1, 1] : baseHeight;
var southWest = neighbourhood.Columns > 0 && neighbourhood.Rows > 1 ? neighbourhood[0, 1] : baseHeight;

var hne = Fit(baseHeight, east);
var hse = Fit(baseHeight, east, southEast, south);
var hsw = Fit(baseHeight, tile.GetHeight(TileCorner.SE), southWest, south);
var hnw = Fit(baseHeight, tile.GetHeight(TileCorner.NE));

return new Tile(hne, hse, hsw, hnw, baseHeight);
}

private static Tile FitFirstColumn(Tile tile, IReadOnlyGrid<byte> neighbourhood, byte baseHeight)
{
var northEast = neighbourhood.Columns > 1 && neighbourhood.Rows > 0 ? neighbourhood[1, 0] : baseHeight;
var east = neighbourhood.Columns > 1 && neighbourhood.Rows > 1 ? neighbourhood[1, 1] : baseHeight;
var southEast = neighbourhood.Columns > 1 && neighbourhood.Rows > 2 ? neighbourhood[1, 2] : baseHeight;
var south = neighbourhood.Columns > 0 && neighbourhood.Rows > 2 ? neighbourhood[0, 2] : baseHeight;

var hne = Fit(baseHeight, tile.GetHeight(TileCorner.SE), northEast, east);
var hse = Fit(baseHeight, east, southEast, south);
var hsw = Fit(baseHeight, south);
var hnw = Fit(baseHeight, tile.GetHeight(TileCorner.SW));

return new Tile(hne, hse, hsw, hnw, baseHeight);
}

private static Tile Fit(IReadOnlyGrid<Tile> nwNeighbours, IReadOnlyGrid<byte> seNeighbours, byte baseHeight)
{
var baseTile = new Tile(baseHeight);

var nw = nwNeighbours.Columns > 0 && nwNeighbours.Rows > 0 ? nwNeighbours[0, 0] : baseTile;
var n = nwNeighbours.Columns > 1 && nwNeighbours.Rows > 0 ? nwNeighbours[1, 0] : baseTile;
var ne = nwNeighbours.Columns > 2 && nwNeighbours.Rows > 0 ? nwNeighbours[2, 0] : baseTile;
var w = nwNeighbours.Columns > 0 && nwNeighbours.Rows > 1 ? nwNeighbours[0, 1] : baseTile;

var heightEast = seNeighbours.Columns > 2 && seNeighbours.Rows > 1 ? seNeighbours[2, 1] : baseHeight;
var heightSouthWest = seNeighbours.Columns > 0 && seNeighbours.Rows > 2 ? seNeighbours[0, 2] : baseHeight;
var heightSouth = seNeighbours.Columns > 1 && seNeighbours.Rows > 2 ? seNeighbours[1, 2] : baseHeight;
var heightSouthEast = seNeighbours.Columns > 2 && seNeighbours.Rows > 2 ? seNeighbours[2, 2] : baseHeight;

var hne = Fit(baseHeight, n.GetHeight(TileCorner.SE), ne.GetHeight(TileCorner.SW), heightEast);
var hse = Fit(baseHeight, heightEast, heightSouthEast, heightSouth);
var hsw = Fit(baseHeight, w.GetHeight(TileCorner.SE), heightSouthWest, heightSouth);
var hnw = Fit(baseHeight, nw.GetHeight(TileCorner.SE), n.GetHeight(TileCorner.SW), w.GetHeight(TileCorner.NE));

return new Tile(hne, hse, hsw, hnw, baseHeight);
}

private static CornerType Fit(byte baseHeight, params byte[] options)
{
var result = baseHeight;
for (var i = 0; i < options.Length; i++)
{
var height = options[i];
if (IsWithin(height, baseHeight - 1, baseHeight + 1))
{
result = height;
break;
}
}

if (result > baseHeight)
{
return CornerType.Raised;
}

if (result < baseHeight)
{
return CornerType.Lowered;
}

return CornerType.Level;
}

private static bool IsWithin(int value, int min, int max)
{
return value <= max || value >= min;
}
}
Loading

0 comments on commit 9699d8f

Please sign in to comment.