initialize repo

This commit is contained in:
2025-08-08 15:36:09 +02:00
parent d6a2c37a5f
commit cabf13d164
92 changed files with 2160 additions and 2 deletions

133
Lib/Civilization.Core/.gitignore vendored Normal file
View File

@@ -0,0 +1,133 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
x64/
[Bb]in/
[Oo]bj/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.log
*.svclog
*.scc
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# Click-Once directory
publish/
# Publish Web Output
*.Publish.xml
*.pubxml
*.azurePubxml
# NuGet Packages Directory
## TODO: If you have NuGet Package Restore enabled, uncomment the next line
packages/
## TODO: If the tool you use requires repositories.config, also uncomment the next line
!packages/repositories.config
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
![Ss]tyle[Cc]op.targets
~$*
*~
*.dbmdl
*.[Pp]ublish.xml
*.publishsettings
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
App_Data/*.mdf
App_Data/*.ldf
# =========================
# Windows detritus
# =========================
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Mac desktop service store files
.DS_Store
_NCrunch*

View File

@@ -0,0 +1,26 @@
using Civilization.Core.Interfaces;
namespace Civilization.Core.Actions;
public class ActionQueue
{
private readonly Queue<IGameAction> _pending = new();
private readonly Stack<ExecutedAction> _history = new();
public void Enqueue(IGameAction action)
{
_pending.Enqueue(action);
}
public void ExecuteAll(GameActionContext context)
{
while (_pending.Count > 0)
{
var action = _pending.Dequeue();
if (!action.CanExecute(context)) continue;
action.Execute(context);
_history.Push(new ExecutedAction(action, context));
}
}
}

View File

@@ -0,0 +1,15 @@
using Civilization.Core.Interfaces;
namespace Civilization.Core.Actions;
public class ExecutedAction
{
public IGameAction Action { get; }
public GameActionContext ContextSnapshot { get; }
public ExecutedAction(IGameAction action, GameActionContext snapshot)
{
Action = action;
ContextSnapshot = snapshot;
}
}

View File

@@ -0,0 +1,39 @@
using Civilization.Core.Interfaces;
namespace Civilization.Core.Actions;
public class ExpandTerritoryAction(Guid cityId, Vec2I targetTile) : IGameAction
{
public Guid CityId { get; } = cityId;
public Vec2I TargetTile { get; } = targetTile;
public bool CanExecute(GameActionContext context)
{
var city = context.State.FindCity(CityId);
if (city == null || city.ActionPoints < 1) return false;
if (!context.Map.Grid.IsValidPosition(TargetTile)) return false;
var tile = context.Map.GetTile(TargetTile);
if (tile is not { OwnerId: null }) return false;
return city.Territory.Any(t =>
{
var neighbors = context.Map.GetNeighbors(t);
return neighbors.Contains(tile);
});
}
public void Execute(GameActionContext context)
{
var city = context.State.FindCity(CityId)!;
city.ClaimTile(context.Map, TargetTile);
city.SpendActionPoint();
}
public void Undo(GameActionContext context)
{
}
}

View File

@@ -0,0 +1,19 @@
using Civilization.Core.Game;
namespace Civilization.Core.Actions;
public class GameActionContext
{
public GameMap Map { get; }
public List<Player> Players { get; }
public GameState State { get; }
public GameActionContext(GameState gameState)
{
State = gameState;
Map = gameState.Map;
Players = gameState.Players;
}
public Player CurrentPlayer => State.CurrentPlayer;
}

View File

@@ -0,0 +1,31 @@
using Civilization.Core.Interfaces;
namespace Civilization.Core.Actions;
public class MoveUnitAction(Guid unitId, Vec2I targetPosition) : IGameAction
{
public Guid UnitId { get; } = unitId;
public Vec2I TargetPosition { get; } = targetPosition;
public bool CanExecute(GameActionContext context)
{
var unit = context.State.FindUnit(UnitId);
if (unit == null || unit.OwnerId != context.CurrentPlayer.Id) return false;
return context.Map.Grid.IsValidPosition(TargetPosition) && unit.CanMoveTo(TargetPosition, context.Map);
}
public void Execute(GameActionContext context)
{
var unit = context.State.FindUnit(UnitId);
if (unit == null) return;
unit.Position = TargetPosition;
unit.ActionPoints -= 1;
}
public void Undo(GameActionContext context)
{
}
}

View File

@@ -0,0 +1,45 @@
using Civilization.Core.Game;
using Civilization.Core.Interfaces;
using Civilization.Core.Units;
namespace Civilization.Core.Actions;
public class SettleCityAction(Guid unitId) : IGameAction
{
private const int CityNearbyRange = 4;
public Guid UnitId { get; } = unitId;
public bool CanExecute(GameActionContext context)
{
var unit = context.State.FindUnit(UnitId);
if (unit == null || !unit.HasTag(UnitTag.Settle)) return false;
if (unit.OwnerId != context.CurrentPlayer.Id) return false;
if (unit.ActionPoints < 1) return false;
var tile = context.Map.GetTile(unit.Position);
if (tile is not { OwnerId: null }) return false;
// Later we could also check if there is city nearby
return true;
}
public void Execute(GameActionContext context)
{
var unit = context.State.FindUnit(UnitId)!;
var position = unit.Position;
context.State.RemoveUnit(UnitId);
var city = new City(unit.OwnerId, position);
city.ClaimTile(context.Map, position);
context.State.AddCity(city);
}
public void Undo(GameActionContext context)
{
}
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,14 @@
namespace Civilization.Core;
public readonly struct ColorRGBA(byte r, byte g, byte b, byte a = 255)
{
public byte R { get; } = r;
public byte G { get; } = g;
public byte B { get; } = b;
public byte A { get; } = a;
public string ToHex()
{
return $"#{R:X2}{G:X2}{B:X2}{A:X2}";
}
}

View File

@@ -0,0 +1,71 @@
using Civilization.Core.Actions;
using Civilization.Core.Interfaces;
namespace Civilization.Core.Game;
public class City : IOnTurnListener, IEntity
{
public Guid Id { get; } = Guid.NewGuid();
public int OwnerId { get; }
public string Name { get; set; }
public Vec2I Position { get; }
public HashSet<Vec2I> Territory { get; } = [];
public int MaxActionPoints { get; private set; } = 1;
public int ActionPoints { get; set; }
public City(int ownerId, Vec2I position, string name = "New City")
{
OwnerId = ownerId;
Position = position;
Name = name;
Territory.Add(position);
ActionPoints = MaxActionPoints;
}
public void ExpandTo(Vec2I newTile) => Territory.Add(newTile);
public void ResetActionPoints() => ActionPoints = MaxActionPoints;
public bool CanProduce() => ActionPoints > 0;
public void SpendActionPoint()
{
if (ActionPoints > 0) ActionPoints--;
}
public void ClaimTile(GameMap map, Vec2I tilePos)
{
var tile = map.GetTile(tilePos);
if (tile == null) return;
tile.OwnerId = OwnerId;
Territory.Add(tilePos);
}
public void OnTurnStart(GameState state)
{
ResetActionPoints();
var map = state.Map;
var neighbors = Territory.SelectMany(pos => map.Grid.GetNeighbors(pos))
.Distinct()
.Where(p => map.GetTile(p)?.OwnerId == null)
.ToList();
if (neighbors.Count > 0)
{
var rng = new Random();
var choice = neighbors[rng.Next(neighbors.Count)];
state.ActionQueue.Enqueue(new ExpandTerritoryAction(Id, choice));
}
}
public void OnTurnEnd(GameState state)
{
}
}

View File

@@ -0,0 +1,56 @@
using Civilization.Core.Actions;
using Civilization.Core.Interfaces;
using Civilization.Core.Units;
namespace Civilization.Core.Game;
public class GameState(GameMap map, List<Player> players)
{
private readonly Dictionary<Guid, Unit> _units = new();
private readonly Dictionary<Guid, City> _cities = new();
public GameMap Map { get; } = map;
public List<Player> Players { get; } = players;
public TurnManager TurnManager { get; } = new(players);
public ActionQueue ActionQueue { get; } = new();
public IEnumerable<Unit> Units => _units.Values;
public Player CurrentPlayer => TurnManager.CurrentPlayer;
public IEnumerable<City> Cities => _cities.Values;
public void NextTurn()
{
foreach (var listener in GetAllTurnListeners(CurrentPlayer.Id)) listener.OnTurnEnd(this);
TurnManager.AdvanceTurn();
foreach (var listener in GetAllTurnListeners(CurrentPlayer.Id)) listener.OnTurnStart(this);
ActionQueue.ExecuteAll(new GameActionContext(this));
}
public void AddUnit(Unit unit) => _units.Add(unit.Id, unit);
public Unit? FindUnit(Guid id) => _units.GetValueOrDefault(id);
public IEnumerable<Unit> GetUnitsForPlayer(int playerId) => _units.Values.Where(u => u.OwnerId == playerId);
public void RemoveUnit(Guid id) {
// Later, let's check if player can remove this unit (e.g. his turn, unit is not in combat, etc.)
_units.Remove(id);
}
public void AddCity(City city) => _cities.Add(city.Id, city);
public City? FindCity(Guid id) => _cities.GetValueOrDefault(id);
public IEnumerable<City> GetCitiesForPlayer(int playerId)
=> _cities.Values.Where(c => c.OwnerId == playerId);
private IEnumerable<IOnTurnListener> GetAllTurnListeners(int playerId)
{
foreach (var unit in GetUnitsForPlayer(playerId)) yield return unit;
foreach (var city in GetCitiesForPlayer(playerId)) yield return city;
}
}

View File

@@ -0,0 +1,11 @@
namespace Civilization.Core.Game;
public class Player(int id, string name, ColorRGBA color, bool isHuman = true)
{
public int Id { get; } = id;
public string Name { get; } = name;
public ColorRGBA Color { get; } = color;
public bool IsHuman { get; } = isHuman;
}

View File

@@ -0,0 +1,24 @@
namespace Civilization.Core.Game;
public class TurnManager
{
private readonly List<Player> _players = [];
private int _currentIndex = 0;
public int TurnNumber { get; private set; } = 1;
public Player CurrentPlayer => _players[_currentIndex];
public TurnManager(IEnumerable<Player> players)
{
_players = players.ToList();
}
public void AdvanceTurn()
{
_currentIndex++;
if (_currentIndex < _players.Count) return;
_currentIndex = 0;
TurnNumber++;
}
}

View File

@@ -0,0 +1,33 @@
using Civilization.Core.Interfaces;
namespace Civilization.Core;
public class GameMap
{
private readonly Dictionary<Vec2I, Tile.Tile> _tiles = new();
public ITileGrid Grid { get; private set; }
public GameMap(ITileGrid grid)
{
Grid = grid;
for (var x = 0; x < ((IGridSize)grid).Width; x++)
for (var y = 0; y < ((IGridSize)grid).Height; y++)
{
var pos = new Vec2I(x, y);
_tiles[pos] = new Tile.Tile(pos);
}
}
public IEnumerable<Tile.Tile> GetTiles() => _tiles.Values;
public IEnumerable<Tile.Tile> GetNeighbors(Vec2I position)
{
return Grid.GetNeighbors(position).Select(GetTile).Where(t => t != null)!;
}
public Tile.Tile? GetTile(Vec2I position) => _tiles.GetValueOrDefault(position);
}

View File

@@ -0,0 +1,46 @@
using Civilization.Core.Interfaces;
namespace Civilization.Core.Grid;
public class SquareGrid : ITileGrid, IGridSize
{
public int Width { get; }
public int Height { get; }
private static readonly Vec2I[] NeighborOffsets =
{
new Vec2I(-1, 0), // Left
new Vec2I(1, 0), // Right
new Vec2I(0, -1), // Up
new Vec2I(0, 1) // Down
};
public SquareGrid(int width, int height)
{
Width = width;
Height = height;
}
public IEnumerable<Vec2I> GetNeighbors(Vec2I tilePosition)
{
foreach (var offset in NeighborOffsets)
{
var neighbor = tilePosition + offset;
if (IsValidPosition(neighbor))
yield return neighbor;
}
}
public Vec2I ClampPosition(Vec2I position)
{
var x = Math.Clamp(position.X, 0, Width - 1);
var y = Math.Clamp(position.Y, 0, Height - 1);
return new Vec2I(x, y);
}
public bool IsValidPosition(Vec2I position)
{
return position.X >= 0 && position.X < Width && position.Y >= 0 && position.Y < Height;
}
}

View File

@@ -0,0 +1,7 @@
namespace Civilization.Core.Interfaces;
public interface IEntity
{
Guid Id { get; }
int OwnerId { get; }
}

View File

@@ -0,0 +1,10 @@
using Civilization.Core.Actions;
namespace Civilization.Core.Interfaces;
public interface IGameAction
{
bool CanExecute(GameActionContext context);
void Execute(GameActionContext context);
void Undo(GameActionContext context);
}

View File

@@ -0,0 +1,7 @@
namespace Civilization.Core.Interfaces;
public interface IGridSize
{
public int Width { get; }
public int Height { get; }
}

View File

@@ -0,0 +1,6 @@
namespace Civilization.Core.Interfaces;
public interface ILogger
{
void Log(string message);
}

View File

@@ -0,0 +1,9 @@
using Civilization.Core.Game;
namespace Civilization.Core.Interfaces;
public interface IOnTurnListener
{
void OnTurnStart(GameState state);
void OnTurnEnd(GameState state);
}

View File

@@ -0,0 +1,10 @@
namespace Civilization.Core.Interfaces;
public interface ITileGrid
{
IEnumerable<Vec2I> GetNeighbors(Vec2I tilePosition);
Vec2I ClampPosition(Vec2I position);
bool IsValidPosition(Vec2I position);
}

View File

@@ -0,0 +1,16 @@
namespace Civilization.Core.Tile;
public class Tile
{
public Vec2I Position { get; }
public TileType Type { get; set; } = TileType.Plain;
public int? OwnerId { get; set; } = null;
public Tile(Vec2I position)
{
Position = position;
}
}

View File

@@ -0,0 +1,16 @@
namespace Civilization.Core.Tile;
public enum TileType
{
Plain = 0,
Forest = 1,
Mountain = 2,
Water = 3,
Desert = 4,
Grassland = 5,
Hills = 6,
Ocean = 7,
Tundra = 8,
Snow = 9,
Swamp = 10,
}

View File

@@ -0,0 +1,56 @@
using Civilization.Core.Game;
using Civilization.Core.Interfaces;
namespace Civilization.Core.Units;
public class Unit : IOnTurnListener, IEntity
{
public Guid Id { get; } = Guid.NewGuid();
public int OwnerId { get; }
public UnitType Type { get; }
public UnitData Data { get; }
public Vec2I Position { get; set; }
public int MaxActionPoints { get; }
public int ActionPoints { get; set; }
public Unit(int ownerId, UnitType type, Vec2I position)
{
Id = Guid.NewGuid();
OwnerId = ownerId;
Type = type;
Position = position;
Data = UnitDataRegistry.Get(Type);
MaxActionPoints = Data.MaxActionPoints;
ActionPoints = MaxActionPoints;
}
public void ResetActionPoints()
{
ActionPoints = MaxActionPoints;
}
public bool CanMoveTo(Vec2I destination, GameMap map)
{
if (ActionPoints <= 0) return false;
if (!map.Grid.IsValidPosition(destination)) return false;
var distance = Math.Abs(Position.X - destination.X) + Math.Abs(Position.Y - destination.Y);
return distance <= Data.MoveRange;
}
public bool HasTag(UnitTag tag) => Data.HasTag(tag);
public void OnTurnStart(GameState state)
{
ResetActionPoints();
}
public void OnTurnEnd(GameState state)
{
}
}

View File

@@ -0,0 +1,11 @@
namespace Civilization.Core.Units;
public class UnitData
{
public string Name { get; init; }
public int MaxActionPoints { get; init; }
public int MoveRange { get; init; } = 1;
public HashSet<UnitTag> Tags { get; init; } = [];
public bool HasTag(UnitTag tag) => Tags.Contains(tag);
}

View File

@@ -0,0 +1,17 @@
namespace Civilization.Core.Units;
public class UnitDataRegistry
{
private static readonly Dictionary<UnitType, UnitData> _registry = new()
{
[UnitType.Settler] = new UnitData
{
Name = "Settler",
MaxActionPoints = 1,
MoveRange = 1,
Tags = new() { UnitTag.Settle, }
}
};
public static UnitData Get(UnitType unitType) => _registry[unitType];
}

View File

@@ -0,0 +1,6 @@
namespace Civilization.Core.Units;
public enum UnitTag
{
Settle,
}

View File

@@ -0,0 +1,6 @@
namespace Civilization.Core.Units;
public enum UnitType
{
Settler,
}

View File

@@ -0,0 +1,15 @@
namespace Civilization.Core;
public readonly struct Vec2I(int x, int y) : IEquatable<Vec2I>
{
public int X { get; } = x;
public int Y { get; } = y;
public static Vec2I operator +(Vec2I a, Vec2I b) => new(a.X + b.X, a.Y + b.Y);
public static Vec2I operator -(Vec2I a, Vec2I b) => new(a.X - b.X, a.Y - b.Y);
public override string ToString() => $"({X}, {Y})";
public bool Equals(Vec2I other) => X == other.X && Y == other.Y;
public override bool Equals(object? obj) => obj is Vec2I other && Equals(other);
public override int GetHashCode() => HashCode.Combine(X, Y);
}