ported from perforce

This commit is contained in:
2026-04-19 00:43:27 +02:00
commit 6c0c33f5d4
700 changed files with 19735 additions and 0 deletions

View File

@@ -0,0 +1,373 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.InteropServices;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
using RobotAndDonkey.Game.Utils;
namespace RobotAndDonkey.Game.Board;
public record Board(ImmutableArray<Cell> Cells, int TargetDeliveryAmount)
{
public Board(Board clone)
{
Cells = [.. clone.Cells.Select(c => new Cell(c))];
TargetDeliveryAmount = clone.TargetDeliveryAmount;
}
public int FindCellIndex(Hex hex)
{
// TODO calculate in O(1) with BoardSize
for (var i = 0; i < Cells.Length; i++)
if (Cells[i].Hex == hex)
return i;
return -1;
}
public static Board Generate(ref SRandom random, EDifficulty difficulty)
{
var cells = new List<Cell>();
var fDifficulty = 0.5f + (int)difficulty / 4.0f;
var boardSize = (int)(2 + fDifficulty * 2);
// TODO: find better order of adding cells to facilitate FindCellIndex
for (var x = -boardSize; x <= boardSize; x++)
for (var y = -boardSize; y <= boardSize; y++)
{
var hex = new Hex(x, y);
if (Hex.Distance(hex, new()) > boardSize)
continue;
var cell = new Cell(hex) { Type = ECellType.Grass };
cells.Add(cell);
}
var avatarCell = cells.Single(c => c.Hex is { X: 0, Y: 0 });
var avatar = new Avatar { Direction = (EDirection)random.Next(6) };
avatarCell.Poi = avatar;
var freeCells = cells.ToList();
freeCells.Remove(avatarCell);
for (var x = -boardSize - 1; x <= boardSize + 1; x++)
for (var y = -boardSize - 1; y <= boardSize + 1; y++)
{
var hex = new Hex(x, y);
if (Hex.Distance(hex, new()) != boardSize + 1)
continue;
var cell = new Cell(hex) { Type = ECellType.Grass };
cells.Add(cell);
}
var blockedAmount = (int)(freeCells.Count * Balancing.Instance.BlockedAmount * fDifficulty);
Splat(freeCells, freeCells.Where(c => Hex.Distance(c.Hex, new()) is 1 or 2).ToList(), ref random, blockedAmount, Balancing.Instance.BlockedSpread, c =>
{
if (!PlacementKeepsConnectivity(cells, c))
return false;
c.Type = ECellType.Blocked;
return true;
});
Sprinkle(freeCells, ref random, Balancing.Instance.DonkeySprinkle, c => c != ECellType.Blocked, c => c.Poi = new Donkey());
Sprinkle(freeCells, ref random, Balancing.Instance.ShedSprinkle, c => c != ECellType.Blocked, c => c.Poi = new Shed());
Sprinkle(freeCells, ref random, Balancing.Instance.CrateSprinkle, c => c != ECellType.Blocked, c => c.Poi = new Crate { Amount = Balancing.Instance.CrateAmount });
Sprinkle(freeCells, ref random, Balancing.Instance.TowerSprinkle, c => c != ECellType.Blocked, c => c.Poi = new Tower());
var poiCells = freeCells.Where(c =>
{
if (c.Poi != null)
return false;
for (var dir = EDirection.Right; dir <= EDirection.BottomRight; ++dir)
{
var neighbour = c.Hex.GetNeighbour(dir);
var next = cells.FindIndex(n => n.Hex == neighbour);
if (next >= 0 && cells[next].Poi != null)
return true;
}
return false;
}).ToList();
freeCells.RemoveAll(c => c.Poi != null);
var goodCells = cells.Where(c => c.Type != ECellType.Blocked).ToList();
var dryAmount = (int)(goodCells.Count * Balancing.Instance.DryAmount * fDifficulty);
var fertileAmount = (int)(goodCells.Count * Balancing.Instance.FertileAmount * (1 - fDifficulty));
var mudAmount = (int)(goodCells.Count * Balancing.Instance.MudAmount * fDifficulty);
var rockyAmount = (int)(goodCells.Count * Balancing.Instance.RockyAmount * fDifficulty);
Splat(goodCells, goodCells, ref random, dryAmount, Balancing.Instance.DrySpread, c =>
{
c.Type = ECellType.Dry;
return true;
});
Splat(goodCells, goodCells, ref random, fertileAmount, Balancing.Instance.FertileSpread, c =>
{
c.Type = ECellType.Fertile;
return true;
});
Splat(goodCells, goodCells, ref random, mudAmount, Balancing.Instance.MudSpread, c =>
{
c.Type = ECellType.Mud;
return true;
});
Splat(goodCells, goodCells, ref random, rockyAmount, Balancing.Instance.RockySpread, c =>
{
c.Type = ECellType.Rocky;
return true;
});
foreach (var cell in cells)
{
if (cell.Poi is Tower)
{
if (cell.Modifiers.All(m => m.Id != EModifierId.Unreliable))
{
freeCells.Remove(cell);
cell.AddModifier(new UnreliableCellModifier(EModifierDuration.Permanent), []);
}
var hex = cell.Hex;
for (var dir = EDirection.Right; dir <= EDirection.BottomRight; ++dir)
{
var neighbour = hex.GetNeighbour(dir);
var neighbourCell = cells.FirstOrDefault(c => c.Hex == neighbour);
if (neighbourCell == null)
continue;
if (neighbourCell.Modifiers.All(m => m.Id != EModifierId.Unreliable))
{
freeCells.Remove(neighbourCell);
poiCells.Remove(neighbourCell);
neighbourCell.AddModifier(new UnreliableCellModifier(EModifierDuration.Permanent), []);
}
}
}
else if (cell.Poi is Donkey donkey)
donkey.Direction = (EDirection)random.Next(6);
}
var corruptAmount = (int)(freeCells.Count * Balancing.Instance.CorruptedAmount * fDifficulty);
Splat(freeCells, poiCells, ref random, corruptAmount, Balancing.Instance.CorruptedSpread, c =>
{
if (!PlacementKeepsConnectivity(cells, c))
return false;
c.AddModifier(new CorruptCellModifier(EModifierDuration.Permanent), []);
return true;
});
random.Shuffle(CollectionsMarshal.AsSpan(cells));
var sheds = cells.Where(c => c.Poi is Shed).Select(c => (Shed)c.Poi!).ToArray();
var crates = cells.Where(c => c.Poi is Crate).Select(c => (Crate)c.Poi!).ToArray();
var targetDeliveryAmount = sheds.Length * Balancing.Instance.DefaultShedRequest;
var totalRequests = 0;
for (var i = 0; i < sheds.Length - 1; ++i)
{
var randomness = (random.NextSingle() - 0.5f) * Balancing.Instance.CrateShedRandomness;
var request = (int)Math.Ceiling(Balancing.Instance.DefaultShedRequest * (1 + randomness));
sheds[i].Requested = request;
totalRequests += request;
}
if (totalRequests >= targetDeliveryAmount)
targetDeliveryAmount = totalRequests + 1;
sheds[^1].Requested = targetDeliveryAmount - totalRequests;
var targetOfferAmount = (int)((1 + Balancing.Instance.CrateOfferBonus * (1 - fDifficulty)) * targetDeliveryAmount);
var defaultCrateOffer = targetOfferAmount / crates.Length;
var totalOffers = 0;
for (var i = 0; i < crates.Length - 1; ++i)
{
var randomness = (random.NextSingle() - 0.5f) * Balancing.Instance.CrateShedRandomness;
var offer = (int)Math.Max(1, Math.Floor(defaultCrateOffer * (1 + randomness)));
crates[i].Amount = offer;
totalOffers += offer;
}
crates[^1].Amount = targetOfferAmount - totalOffers;
if (!AllPoisReachable(cells))
throw new InvalidOperationException();
return new([.. cells], targetDeliveryAmount);
}
private static void Splat(List<Cell> cells, List<Cell> startCells, ref SRandom random, int needed, float spread, Func<Cell, bool> callback)
{
var converted = 0;
while (converted < needed)
{
if (startCells.Count == 0)
break;
var open = new Queue<Cell>();
open.Enqueue(startCells[random.Next(startCells.Count)]);
var closed = new HashSet<Cell>();
while (open.TryDequeue(out var cell))
{
if (!closed.Add(cell))
continue;
if (!callback(cell))
continue;
cells.Remove(cell);
startCells.Remove(cell);
converted += 1;
if (converted >= needed)
break;
var hex = cell.Hex;
for (var dir = EDirection.Right; dir <= EDirection.BottomRight; ++dir)
{
if (random.NextSingle() < spread)
continue;
var neighbour = hex.GetNeighbour(dir);
var next = cells.FindIndex(c => c.Hex == neighbour);
if (next >= 0)
open.Enqueue(cells[next]);
}
}
}
}
private static void Sprinkle(List<Cell> cells, ref SRandom random, float intensity, Predicate<ECellType> filter, Action<Cell> callback)
{
var needed = (int)(cells.Count * intensity);
var candidates = cells.Select((c, i) => (Cell: c, OriginalIndex: i)).Where(c => c.Cell.Poi == null && filter(c.Cell.Type)).ToArray();
random.Shuffle(candidates.AsSpan());
var placed = 0;
for (var i = 0; i < candidates.Length && placed < needed; ++i)
{
var (_, originalIndex) = candidates[i];
var cell = cells[originalIndex];
if (cell.Poi != null || !filter(cell.Type))
continue;
if (!PlacementKeepsConnectivity(cells, cell))
continue;
callback(cell);
placed++;
}
}
private static HashSet<Cell> GetReachableFromAvatar(List<Cell> cells)
{
var avatarCell = cells.Single(c => c.Poi is Avatar);
var visited = new HashSet<Cell>();
var queue = new Queue<Cell>();
visited.Add(avatarCell);
queue.Enqueue(avatarCell);
while (queue.TryDequeue(out var current))
{
var hex = current.Hex;
for (var dir = EDirection.Right; dir <= EDirection.BottomRight; ++dir)
{
var neighbourHex = hex.GetNeighbour(dir);
var neighbour = cells.FirstOrDefault(c => c.Hex == neighbourHex);
if (neighbour == null)
continue;
if (!IsWalkable(neighbour) || neighbour.Modifiers.Any(c => c.Id == EModifierId.Corrupt))
continue;
if (visited.Add(neighbour))
queue.Enqueue(neighbour);
}
}
return visited;
}
private static bool AllPoisReachable(List<Cell> cells)
{
var reachable = GetReachableFromAvatar(cells);
bool IsPoiAccessible(Cell poiCell)
{
if (reachable.Contains(poiCell))
return true;
var hex = poiCell.Hex;
for (var dir = EDirection.Right; dir <= EDirection.BottomRight; ++dir)
{
var neighbourHex = hex.GetNeighbour(dir);
var neighbour = cells.FirstOrDefault(c => c.Hex == neighbourHex);
if (neighbour != null && reachable.Contains(neighbour))
return true;
}
return false;
}
foreach (var poiCell in cells.Where(c => c.Poi is Donkey or Shed or Crate or Tower))
{
if (!IsPoiAccessible(poiCell))
return false;
}
return true;
}
private static bool IsWalkable(Cell cell)
{
return cell.Type != ECellType.Blocked && cell.Poi is Avatar or null;
}
private static int CountComponentSize(List<Cell> cells, Hex start, Cell? blocked = null)
{
var visited = new HashSet<Hex>();
var queue = new Queue<Hex>();
visited.Add(start);
queue.Enqueue(start);
while (queue.TryDequeue(out var current))
{
var hex = current;
for (var dir = EDirection.Right; dir <= EDirection.BottomRight; ++dir)
{
var neighbourHex = hex.GetNeighbour(dir);
var neighbour = cells.FirstOrDefault(c => c.Hex == neighbourHex);
if (neighbour == null)
continue;
if (neighbour == blocked)
continue;
if (neighbour.Type == ECellType.Blocked || neighbour.Modifiers.Any(m => m.Id == EModifierId.Corrupt))
continue;
if (visited.Add(neighbourHex))
{
if (neighbour.Poi is Avatar or null)
queue.Enqueue(neighbourHex);
}
}
}
return visited.Count;
}
private static bool PlacementKeepsConnectivity(List<Cell> cells, Cell candidate)
{
if (!IsWalkable(candidate))
return true;
var sizeWithCandidate = CountComponentSize(cells, new());
var sizeWithoutCandidate = CountComponentSize(cells, new(), candidate);
return sizeWithoutCandidate == sizeWithCandidate - 1;
}
}

View File

@@ -0,0 +1,25 @@
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
using RobotAndDonkey.Game.Utils;
namespace RobotAndDonkey.Game.Board;
public record Cell(Hex Hex) : Entity
{
public Cell(Cell clone)
: base(clone)
{
Hex = clone.Hex;
Type = clone.Type;
Poi = clone.Poi?.DeepClone();
}
public override string ToString()
{
return $"Hex={Hex}, {Type}, Poi={Poi}" + base.ToString();
}
public Poi? Poi { get; set; }
public ECellType Type { get; set; }
}

View File

@@ -0,0 +1,11 @@
namespace RobotAndDonkey.Game.Board;
public enum ECellType
{
Grass,
Dry,
Fertile,
Mud,
Blocked,
Rocky
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace RobotAndDonkey.Game.Cards;
public class AvailableCards
{
static AvailableCards()
{
Instance = new();
var cardTypes = Instance.GetType().Assembly.GetTypes().Where(t => !t.IsAbstract && t.BaseType?.BaseType == typeof(Card) || t.BaseType?.BaseType?.BaseType == typeof(Card));
foreach (var type in cardTypes)
{
var card = (Card)Activator.CreateInstance(type)!;
Instance.Cards.Add(card.Id, card);
}
}
public static AvailableCards Instance { get; }
public Dictionary<ECard, Card> Cards { get; } = [];
public Card Create(ECard card)
{
return Cards[card].DeepClone();
}
}

View File

@@ -0,0 +1,43 @@
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
namespace RobotAndDonkey.Game.Cards;
public record Card(ECard Id, ECardType CardType, ERarity Rarity, int ShopCost, int BasePlayCost) : Entity
{
public Card DeepClone()
{
var result = (Card)Activator.CreateInstance(GetType())!;
result.PlayCost = PlayCost;
result.CardId = CardId;
foreach (var modifier in Modifiers)
result.AddModifier(modifier.DeepClone(), []);
return result;
}
public virtual void CreateIntents(Cell? avatarCell, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
if (PlayCost > 0)
intents.Add(new CardCostIntent(this, PlayCost));
}
public int PlayCost { get; set; } = BasePlayCost;
public virtual string Name => Id.ToString();
public virtual string ToolTip => Name;
public sealed override string ToString()
{
return $"{Id}" + base.ToString();
}
public int OccupiedSpace => Modifiers.Any(m => m.Id == EModifierId.Efficient) ? 0 : 1;
public Guid CardId { get; private set; } = Guid.NewGuid();
public virtual Modifier[] TooltipModifiers => [];
}

View File

@@ -0,0 +1,42 @@
namespace RobotAndDonkey.Game.Cards;
public enum ECard
{
// Glitches
Bitflip, // corrupt one instruction, permanent
ShortCircuit, // unreliable one instruction, permanent
Slipstream, // race condition one instruction, permanent
Latency, // throttle one instruction, permanent
Rain, // 1 in X grass cells become mud, dry grass becomes grass, permanent
Drought, // fertile becomes grass, 1 in X grass cells become dry, #Temporary
Pest, // delivery malus, temporary
Gravity, // carry malus, temporary
HeatWave, // energy malus, temporary
//SandStorm, // JAM, preview malus, temporary
SolarFlare, // corrupt everything, temporary
LightningStorm, // unreliable everything, temporary
MeteorStorm, // race condition everything, temporary
WindStorm, // throttle everything, temporary
// Patches
Move = 0x1000,
TurnLeft,
TurnRight,
Interact,
NoOp,
Potentiate, // adds effective
Optimize, // adds optimized
Streamline, // adds efficient
Persist, // adds persistent
Remember, // adds memory
Reason, // adds hand size
DetoxiumPrime, // Converts #Forward mud into grass or grass into fertile
FlyingDisk, // Move #Forward into 1 blocked cell
AluminumHat, // instructions are #Immune to the effects of unreliable, #ShortTerm
EMField, // instructions are #Immune to the effects of corrupt #ShortTerm
AtomicClock, // instructions are #Immune to the effects of race condition #ShortTerm
Jump, // move #Forward 2 cells, ignoring blocking
Repeat, // copy #Next
Rest, // restores energy
Stabilize // nullifies the effect of any modifiers of #Next
}

View File

@@ -0,0 +1,7 @@
namespace RobotAndDonkey.Game.Cards;
public enum ECardType
{
Glitch,
Patch,
}

View File

@@ -0,0 +1,10 @@
namespace RobotAndDonkey.Game.Cards;
public enum ERarity
{
Common,
Magic,
Uncommon,
Rare,
Legendary
}

View File

@@ -0,0 +1,3 @@
namespace RobotAndDonkey.Game.Cards.Glitches;
public record Bitflip() : ModifyInstructionPermanently(EModifierId.Corrupt, ECard.Bitflip);

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
namespace RobotAndDonkey.Game.Cards.Glitches;
public record Drought() : GlitchCard(ECard.Drought)
{
public override void CreateIntents(Cell? avatarCell, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
var cells = coreLoop.Board.Cells;
foreach (var cell in cells)
{
if (cell.Type == ECellType.Fertile)
{
intents.Add(new ModifyCell(cell, EModifierId.Drought, EModifierDuration.Temporary));
}
else if (cell.Type == ECellType.Grass)
{
if (coreLoop.Random.NextSingle() < Balancing.Instance.DroughtTransformProbability)
intents.Add(new ModifyCell(cell, EModifierId.Drought, EModifierDuration.Temporary));
}
}
}
public override string ToolTip => "Fertility in the land is drastically reduced.";
public override Modifier[] TooltipModifiers => [new DroughtModifier(EModifierDuration.Temporary)];
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Cards.Glitches;
public record GlitchCard(ECard Id) : Card(Id, ECardType.Glitch, ERarity.Common, 0, 0)
{
public override void CreateIntents(Cell? avatarCell, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
base.CreateIntents(avatarCell, coreLoop, requestId, intents, results);
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
namespace RobotAndDonkey.Game.Cards.Glitches;
public record Gravity() : GlitchCard(ECard.Gravity)
{
public override void CreateIntents(Cell? avatarCell, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new ModifyRobot(EModifierId.Gravity, EModifierDuration.Temporary));
}
public override string ToolTip => "Carry capacity temporarily reduced.";
public override Modifier[] TooltipModifiers => [new GravityModifier(EModifierDuration.Temporary)];
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
namespace RobotAndDonkey.Game.Cards.Glitches;
public record HeatWave() : GlitchCard(ECard.HeatWave)
{
public override void CreateIntents(Cell? avatarCell, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new ModifyRobot(EModifierId.HeatWave, EModifierDuration.Temporary));
}
public override string ToolTip => "Energy consumption temporarily increased.";
public override Modifier[] TooltipModifiers => [new HeatWaveModifier(EModifierDuration.Temporary)];
}

View File

@@ -0,0 +1,7 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Execution.Results;
namespace RobotAndDonkey.Game.Cards.Glitches;
public record Latency() : ModifyInstructionPermanently(EModifierId.Throttled, ECard.Latency);

View File

@@ -0,0 +1,3 @@
namespace RobotAndDonkey.Game.Cards.Glitches;
public record LightningStorm() : ModifyInstructionsTemporarily(EModifierId.Unreliable, ECard.LightningStorm);

View File

@@ -0,0 +1,3 @@
namespace RobotAndDonkey.Game.Cards.Glitches;
public record MeteorStorm() : ModifyInstructionsTemporarily(EModifierId.RaceCondition, ECard.MeteorStorm);

View File

@@ -0,0 +1,21 @@
using RobotAndDonkey.Game;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.Cards.Glitches;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
public abstract record ModifyInstructionPermanently(EModifierId Modifier, ECard Id) : GlitchCard(Id)
{
public override void CreateIntents(Cell? avatarCell, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
var victim = coreLoop.PatchDeck[coreLoop.Random.Next(coreLoop.PatchDeck.Count)];
intents.Add(new ModifyCard(victim, Modifier, EModifierDuration.Permanent, ECardLocation.Deck));
}
public override string ToolTip => $"Adds {Modifier} to one instruction, permanently.";
public override Modifier[] TooltipModifiers => [ModifyCard.Create(Modifier, EModifierDuration.Permanent)];
}

View File

@@ -0,0 +1,20 @@
using RobotAndDonkey.Game;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.Cards.Glitches;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
public abstract record ModifyInstructionsTemporarily(EModifierId Modifier, ECard Id) : GlitchCard(Id)
{
public override void CreateIntents(Cell? avatarCell, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.AddRange(coreLoop.PatchDeck.Select(victim => new ModifyCard(victim, Modifier, EModifierDuration.Temporary, ECardLocation.Deck)));
}
public override string ToolTip => $"Adds {Modifier} to all instructions, temporarily.";
public override Modifier[] TooltipModifiers => [ModifyCard.Create(Modifier, EModifierDuration.Temporary)];
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
namespace RobotAndDonkey.Game.Cards.Glitches;
public record Pest() : GlitchCard(ECard.Pest)
{
public override void CreateIntents(Cell? avatarCell, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new ModifyRobot(EModifierId.Pest, EModifierDuration.Temporary));
}
public override string ToolTip => $"Deliveries temporarily reduced by {Balancing.Instance.PestDeliveryMultiplier * 100:N0}%.";
public override Modifier[] TooltipModifiers => [new PestModifier(EModifierDuration.Temporary)];
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
namespace RobotAndDonkey.Game.Cards.Glitches;
public record Rain() : GlitchCard(ECard.Rain)
{
public override void CreateIntents(Cell? avatarCell, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
var cells = coreLoop.Board.Cells;
foreach (var cell in cells)
{
if (cell.Type == ECellType.Rocky)
{
intents.Add(new ModifyCell(cell, EModifierId.Rain, EModifierDuration.Temporary));
}
else if (cell.Type == ECellType.Grass)
{
if (coreLoop.Random.NextSingle() < Balancing.Instance.RainTransformProbability)
intents.Add(new ModifyCell(cell, EModifierId.Rain, EModifierDuration.Temporary));
}
}
}
public override string ToolTip => "Increases fertility throughout the map.";
public override Modifier[] TooltipModifiers => [new RainModifier(EModifierDuration.Temporary)];
}

View File

@@ -0,0 +1,3 @@
namespace RobotAndDonkey.Game.Cards.Glitches;
public record ShortCircuit() : ModifyInstructionPermanently(EModifierId.Unreliable, ECard.ShortCircuit);

View File

@@ -0,0 +1,3 @@
namespace RobotAndDonkey.Game.Cards.Glitches;
public record Slipstream() : ModifyInstructionPermanently(EModifierId.RaceCondition, ECard.Slipstream);

View File

@@ -0,0 +1,3 @@
namespace RobotAndDonkey.Game.Cards.Glitches;
public record SolarFlare() : ModifyInstructionsTemporarily(EModifierId.Corrupt, ECard.SolarFlare);

View File

@@ -0,0 +1,3 @@
namespace RobotAndDonkey.Game.Cards.Glitches;
public record WindStorm() : ModifyInstructionsTemporarily(EModifierId.Throttled, ECard.WindStorm);

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record AluminumHat() : PatchCard(ECard.AluminumHat, Balancing.Instance.AluminumHatCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Uncommon)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
var tape = coreLoop.GetTapeCards();
var index = tape.IndexOf(this);
if (index < 0)
{
results.Add(new InvalidInstructionResult(requestId, EInvalidReason.NotFound, this, avatarCell));
return;
}
for (int i = index + 1; i < tape.Count; ++i)
{
intents.Add(new ImmunizeCard(this, tape[i], EModifierId.Unreliable, EModifierDuration.ShortTerm));
}
}
public override string ToolTip => "Instructions on tape become #Immune to the effects of unreliable";
public override Modifier[] TooltipModifiers => [new UnreliableCardModifier(EModifierDuration.Temporary)];
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record AtomicClock() : PatchCard(ECard.AtomicClock, Balancing.Instance.AtomicClockCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Uncommon)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
var tape = coreLoop.GetTapeCards();
var index = tape.IndexOf(this);
if (index < 0)
{
results.Add(new InvalidInstructionResult(requestId, EInvalidReason.NotFound, this, avatarCell));
return;
}
for (int i = index + 1; i < tape.Count; ++i)
{
intents.Add(new ImmunizeCard(this, tape[i], EModifierId.RaceCondition, EModifierDuration.ShortTerm));
}
}
public override string ToolTip => "Instructions on tape become #Immune to the effects of race condition";
public override Modifier[] TooltipModifiers => [new RaceConditionModifier(EModifierDuration.Temporary)];
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record DetoxiumPrime() : PatchCard(ECard.DetoxiumPrime, Balancing.Instance.DetoxiumPrimeCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Uncommon)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new Intents.DetoxiumPrime(avatarCell, this, avatar.Direction));
}
public override string Name => "Detoxium Prime";
public override string ToolTip => "Heals the cell in front";
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record EMField() : PatchCard(ECard.EMField, Balancing.Instance.EMFieldCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Uncommon)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
var tape = coreLoop.GetTapeCards();
var index = tape.IndexOf(this);
if (index < 0)
{
results.Add(new InvalidInstructionResult(requestId, EInvalidReason.NotFound, this, avatarCell));
return;
}
for (int i = index + 1; i < tape.Count; ++i)
{
intents.Add(new ImmunizeCard(this, tape[i], EModifierId.Corrupt, EModifierDuration.Temporary));
}
}
public override string ToolTip => "Instructions on tape become #Immune to the effects of corrupt";
public override Modifier[] TooltipModifiers => [new CorruptCardModifier(EModifierDuration.Temporary)];
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record FlyingDisk() : PatchCard(ECard.FlyingDisk, Balancing.Instance.FlyingDiskCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Uncommon)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new Intents.Move(avatarCell, this, avatar.Direction, true, 1));
}
public override string ToolTip => "Fly forward 1 cell, ignoring blocked areas";
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record Interact() : PatchCard(ECard.Interact, Balancing.Instance.InteractCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Common)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new Intents.Interact(avatarCell, this, avatar.Direction));
}
public override string ToolTip => "Interact with the forward cell";
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record Jump() : PatchCard(ECard.Jump, Balancing.Instance.JumpCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Legendary)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new Intents.Move(avatarCell, this, avatar.Direction, true, 2));
}
public override string ToolTip => "Move forward 2 cells, ignoring obstacles";
}

View File

@@ -0,0 +1,35 @@
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public abstract record ModifyCardBase(ECard Id, int ShopCost, int PlayCost, ERarity Rarity, EModifierId Modifier) : PatchCard(Id, ShopCost, PlayCost, Rarity)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
var tape = coreLoop.GetTapeCards();
var tapeIndex = tape.IndexOf(this);
if (tapeIndex < 0)
results.Add(new InvalidInstructionResult(requestId, EInvalidReason.NotFound, this, null));
else
{
var corrupt = Modifiers.Any(m => m is CorruptModifierBase { DebuffSources.Count: 0 });
for (var i = tapeIndex + 1; i < tape.Count; ++i)
{
var victim = tape[i];
if (corrupt)
intents.Add(new ImmunizeCard(this, victim, Modifier, EModifierDuration.Temporary, false));
else
intents.Add(new ModifyCard(victim, Modifier, EModifierDuration.Temporary, ECardLocation.Tape));
}
}
}
public override string ToolTip => $"All following instructions in tape become {Modifier} temporarily";
public override Modifier[] TooltipModifiers => [ModifyCard.Create(Modifier, EModifierDuration.Temporary)];
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record Move() : PatchCard(ECard.Move, Balancing.Instance.MoveCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Common)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new Intents.Move(avatarCell, this, avatar.Direction, false, 1));
}
public override string ToolTip => "Move one cell forward if it's free";
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record NoOp() : PatchCard(ECard.NoOp, Balancing.Instance.InteractCost, 0, ERarity.Common)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new Intents.Rest(avatarCell, Balancing.Instance.CardNoOpEnergyReplenish));
}
public override string ToolTip => $"Take a break, do nothing. Restore {Balancing.Instance.CardNoOpEnergyReplenish} energy.";
}

View File

@@ -0,0 +1,5 @@
using RobotAndDonkey.Game.Data;
namespace RobotAndDonkey.Game.Cards.Patches;
public record Optimize() : ModifyCardBase(ECard.Optimize, Balancing.Instance.OptimizeCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Magic, EModifierId.Optimized);

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record PatchCard(ECard Id, int ShopCost, int PlayCost, ERarity Rarity) : Card(Id, ECardType.Patch, Rarity, ShopCost, PlayCost)
{
public override void CreateIntents(Cell? avatarCell, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
if (avatarCell?.Poi is not Avatar avatar)
return;
base.CreateIntents(avatarCell, coreLoop, requestId, intents, results);
CreateIntents(avatarCell, avatar, coreLoop, requestId, intents, results);
}
protected virtual void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
}
}

View File

@@ -0,0 +1,5 @@
using RobotAndDonkey.Game.Data;
namespace RobotAndDonkey.Game.Cards.Patches;
public record Persist() : ModifyCardBase(ECard.Persist, Balancing.Instance.PersistCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Magic, EModifierId.Persistent);

View File

@@ -0,0 +1,5 @@
using RobotAndDonkey.Game.Data;
namespace RobotAndDonkey.Game.Cards.Patches;
public record Potentiate() : ModifyCardBase(ECard.Potentiate, Balancing.Instance.PotentiateCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Magic, EModifierId.Effective);

View File

@@ -0,0 +1,18 @@
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record Reason() : PatchCard(ECard.Reason, Balancing.Instance.ReasonCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Rare)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new ModifyCurrency(new(0, 0, 0, 0, 0, HandSize: 1), true));
}
public override string ToolTip => "Adds hand size, permanently.";
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record Remember() : PatchCard(ECard.Remember, Balancing.Instance.RememberCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Rare)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new ModifyCurrency(new(0, 0, 0, 0, TapeLength: 1, 0), true));
}
public override string ToolTip => "Increases tape length, permanently.";
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
using RobotAndDonkey.Game.Utils;
namespace RobotAndDonkey.Game.Cards.Patches;
public record Repeat() : PatchCard(ECard.Repeat, Balancing.Instance.RepeatCost, 0, ERarity.Legendary)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
var tape = coreLoop.GetTapeCards();
var index = tape.IndexOf(this);
if (index < 0)
{
results.Add(new InvalidInstructionResult(requestId, EInvalidReason.NotFound, this, avatarCell));
return;
}
var targetIndex = CardExtensions.NextIndexConsideringCorruption(index, coreLoop);
if (targetIndex < 0 || targetIndex >= tape.Count)
{
results.Add(new InvalidInstructionResult(requestId, EInvalidReason.OutOfBounds, this, avatarCell));
return;
}
var targetCard = tape[targetIndex];
if (!m_RepeatedCards.Add(targetCard))
{
results.Add(new InvalidInstructionResult(requestId, EInvalidReason.AlreadyExecuted, this, avatarCell));
return;
}
targetCard.CreateIntents(avatarCell, coreLoop, requestId, intents, results);
m_RepeatedCards.Remove(targetCard);
}
public override string ToolTip => "Behaves exactly like the next instruction.";
private readonly HashSet<Card> m_RepeatedCards = [];
}

View File

@@ -0,0 +1,18 @@
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record Rest() : PatchCard(ECard.Rest, Balancing.Instance.RestCost, 0, ERarity.Legendary)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new Intents.Rest(avatarCell, Balancing.Instance.CardRestEnergyReplenish));
}
public override string ToolTip => $"Reboot the system and restore {Balancing.Instance.CardRestEnergyReplenish} energy.";
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record Stabilize() : PatchCard(ECard.Stabilize, Balancing.Instance.StabilizeCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Legendary)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
var tape = coreLoop.GetTapeCards();
var index = tape.IndexOf(this);
if (index < 0)
{
results.Add(new InvalidInstructionResult(requestId, EInvalidReason.NotFound, this, avatarCell));
return;
}
var targetIndex = index + 1;
if (targetIndex >= tape.Count)
{
results.Add(new InvalidInstructionResult(requestId, EInvalidReason.OutOfBounds, this, avatarCell));
return;
}
intents.Add(new ImmunizeCard(this, tape[targetIndex], EModifierId._Invalid, EModifierDuration.ShortTerm));
}
public override string ToolTip => "Temporarily nullifies all modifiers of the next card.";
}

View File

@@ -0,0 +1,5 @@
using RobotAndDonkey.Game.Data;
namespace RobotAndDonkey.Game.Cards.Patches;
public record Streamline() : ModifyCardBase(ECard.Streamline, Balancing.Instance.StreamlineCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Magic, EModifierId.Efficient);

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record TurnLeft() : PatchCard(ECard.TurnLeft, Balancing.Instance.TurnLeftCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Common)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new Turn(avatarCell, 1));
}
public override string ToolTip => "Change the orientation by 60 degrees counter-clockwise";
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Cards.Patches;
public record TurnRight() : PatchCard(ECard.TurnRight, Balancing.Instance.TurnRightCost, Balancing.Instance.CardPlayEnergyCost, ERarity.Common)
{
protected override void CreateIntents(Cell avatarCell, Avatar avatar, CoreLoop coreLoop, Guid requestId, List<Intent> intents, List<Result> results)
{
intents.Add(new Turn(avatarCell, -1));
}
public override string ToolTip => "Change the orientation by 60 degrees clockwise";
}

View File

@@ -0,0 +1,122 @@
namespace RobotAndDonkey.Game.Data;
public class Balancing
{
public static Balancing Instance { get; } = new();
// Robots
public int RobotEasyEnergy => 50;
public int RobotMediumEnergy => 40;
public int RobotHardEnergy => 30;
public int EasyMaxCarry => 10;
public int MediumMaxCarry => 8;
public int HardMaxCarry => 6;
public int EasyHandSize => 9;
public int MediumHandSize => 8;
public int HardHandSize => 7;
public int EasyTapeLength => 6;
public int MediumTapeLength => 5;
public int HardTapeLength => 4;
public int CourierEnergyReplenishOnDelivery => 5;
public int RangerFertileRestEnergyDelta => 1;
public int AnalysisHandSizeDelta => 1;
public int AnalysisEnergyDelta => -1;
// Gameplay
public int DiscardEnergyCost => 1;
public int LegendaryWeight => 1;
public int RareWeight => 3;
public int UncommonWeight => 12;
public int MagicWeight => 36;
public int CommonWeight => 108;
public int GetDeferGlitchEnergyCost(int deferCount)
{
deferCount += 1;
return deferCount * (deferCount + 1) / 2;
}
// Shop
public float ShopBuffChance => 0.25f;
public float ShopDebuffChance => 0.25f;
public int GambleBoosterSize => 6;
public int GambleEnergyCost => 5;
public int BufferOverflowEnergyCost => 10;
public int ShopSize => 5;
public int GetRerollEnergyCost(int rerollCount)
{
return rerollCount + 1;
}
// Modifiers
public float EffectiveChance => 0.25f;
public float UnreliableChance => 0.25f;
public float RaceConditionChance => 0.25f;
public int GravityMaxCarryPenalty => 2;
public int GravityExtraMoveCost => 1;
// Board
public int DefaultShedRequest => 10;
public float CrateShedRandomness => 0.5f;
public float CrateOfferBonus => 1.0f;
public int CrateAmount => 10;
public float DryAmount => 0.25f;
public float DrySpread => 0.75f;
public float FertileAmount => 0.5f;
public float FertileSpread => 0.75f;
public float MudAmount => 0.375f;
public float MudSpread => 0.5f;
public float BlockedAmount => 0.25f;
public float BlockedSpread => 0.5f;
public float CorruptedAmount => 0.375f;
public float CorruptedSpread => 0.5f;
public float RockyAmount => 0.2f;
public float RockySpread => 0.9f;
public float DonkeySprinkle => 0.05f;
public float ShedSprinkle => 0.1f;
public float CrateSprinkle => 0.2f;
public float TowerSprinkle => 0.05f;
public float RainTransformProbability => 0.5f;
public float DroughtTransformProbability => 0.5f;
// Cards
public int CardPlayEnergyCost => 1;
public int CardNoOpEnergyReplenish => 5;
public int CardRestEnergyReplenish => 20;
public int DonkeyMaxCarryBonus => 10;
public int HeatWaveEnergyPenalty => 10;
public float PestDeliveryMultiplier => 0.5f;
public int MoveCost => 1;
public int TurnLeftCost => 1;
public int TurnRightCost => 1;
public int InteractCost => 1;
public int PotentiateCost => 3;
public int OptimizeCost => 3;
public int StreamlineCost => 3;
public int PersistCost => 3;
public int RememberCost => 3;
public int ReasonCost => 3;
public int DetoxiumPrimeCost => 5;
public int FlyingDiskCost => 5;
public int AluminumHatCost => 5;
public int EMFieldCost => 5;
public int AtomicClockCost => 5;
public int JumpCost => 8;
public int RepeatCost => 8;
public int RestCost => 8;
public int StabilizeCost => 8;
public int MudMoveEnergyCost => 1;
public int FertileRestEnergyReplenish => 1;
public int DryRestEnergyMalus => 1;
public int DryInteractEnergyMalus => 1;
public int EndOfProgramEnergyReplenish => 10;
}

View File

@@ -0,0 +1,11 @@
namespace RobotAndDonkey.Game;
public enum EDirection
{
Right = 0,
TopRight = 1,
TopLeft = 2,
Left = 3,
BottomLeft = 4,
BottomRight = 5
}

View File

@@ -0,0 +1,8 @@
namespace RobotAndDonkey.Game;
public enum EModifierDuration
{
ShortTerm,
Temporary,
Permanent
}

View File

@@ -0,0 +1,24 @@
namespace RobotAndDonkey.Game;
public enum EModifierId
{
_Invalid = -1,
Corrupt,
Unreliable,
RaceCondition,
Throttled,
Effective,
Optimized,
Efficient,
Persistent,
Analytic,
Rain,
Drought,
Pest,
Gravity,
HeatWave,
//SandStorm, // JAM
CourierOverspill,
RangerFertileRest,
GlobalImmunity,
}

View File

@@ -0,0 +1,8 @@
namespace RobotAndDonkey.Game;
public enum EModifierKind
{
Card,
Robot,
Cell
}

View File

@@ -0,0 +1,16 @@
namespace RobotAndDonkey.Game;
public enum EDifficulty
{
Easy,
Medium,
Hard
}
public enum ERobotType
{
Vintage = 0,
Courier,
Analyst,
Ranger,
}

View File

@@ -0,0 +1,20 @@
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record AcceptCardCommand(Guid RequestId) : Command(RequestId, typeof(DrawGlitchRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
intents.Add(new AcceptGlitch(coreLoop.GlitchDeck[coreLoop.NextGlitch]));
intents.Add(new NextGlitch());
intents.Add(new NextPhase(ERunPhase.Improve));
}
public override string ToString()
{
return "Accept card command";
}
}

View File

@@ -0,0 +1,31 @@
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record BuyCardsCommand(Guid RequestId, int[] Cards) : Command(RequestId, typeof(ImproveRequest), typeof(GambleRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
var gamble = coreLoop.RunPhase == ERunPhase.Gamble;
foreach (var index in Cards)
{
if (index < 0)
continue;
if (gamble && index < coreLoop.BoosterPack.Count)
intents.Add(new BuyPatch(coreLoop.BoosterPack[index], true));
else if (!gamble && index < coreLoop.Shop.Count)
intents.Add(new BuyPatch(coreLoop.Shop[index], false));
}
if (coreLoop.RunPhase == ERunPhase.Gamble)
intents.Add(new NextPhase(ERunPhase.Improve));
}
public override string ToString()
{
return $"Buy cards command {string.Join(", ", Cards.Select(c => c.ToString()))}";
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
namespace RobotAndDonkey.Game.Execution.Commands;
public abstract record Command(Guid RequestId, params Type[] RequestTypes)
{
protected abstract void CreateIntents(CoreLoop coreLoop, List<Intent> intents);
public List<Result> Preview(CoreLoop coreLoop)
{
var mockCoreLoop = new CoreLoop(coreLoop) { IsPreview = true };
return Execute(mockCoreLoop, false, false);
}
public List<Result> Execute(CoreLoop coreLoop, bool force, bool verbose)
{
var results = new List<Result>();
var intents = new List<Intent>();
CreateIntents(coreLoop, intents);
var modifiers = GetModifierStack(coreLoop);
modifiers.Execute(RequestId, coreLoop, intents, results, force, verbose);
return results;
}
private static ModifierStack GetModifierStack(CoreLoop coreLoop)
{
var modifiers = new ModifierStack();
modifiers.Push(coreLoop.Robot);
foreach (var cell in coreLoop.Board.Cells)
{
if (cell.Poi is Avatar)
{
modifiers.Push(cell);
}
}
return modifiers;
}
public bool IsValid(CoreLoop coreLoop, out EInvalidReason reason)
{
var results = Preview(coreLoop);
if (results.Count == 0)
{
reason = EInvalidReason.Invariant;
return false;
}
if (results is [InvalidInstructionResult])
{
reason = EInvalidReason.Invalid;
return false;
}
reason = (EInvalidReason)(-1);
return true;
}
public int EstimateEnergyCost(CoreLoop coreLoop)
{
var results = Preview(coreLoop);
if (results.Count == 0)
return 0;
var newEnergy = 0;
foreach (var result in results)
{
if (result is CurrencyResult currencyResult)
newEnergy = currencyResult.NewCurrency.Energy;
}
return coreLoop.Currency.Energy - newEnergy;
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record DeferCardCommand(Guid RequestId) : Command(RequestId, typeof(DrawGlitchRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
intents.Add(new DeferGlitch(Balancing.Instance.GetDeferGlitchEnergyCost(coreLoop.DeferGlitchCount)));
intents.Add(new NextGlitch());
intents.Add(new NextPhase(ERunPhase.Improve));
}
public override string ToString()
{
return $"Defer card command";
}
}

View File

@@ -0,0 +1,22 @@
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record DestroyCardCommand(Guid RequestId, int HandIndex) : Command(RequestId, typeof(BufferOverflowRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
var handCards = coreLoop.GetHandCards();
if (HandIndex < 0 || HandIndex >= handCards.Count)
return;
intents.Add(new DestroyCard(handCards[HandIndex]));
}
public override string ToString()
{
return $"Destroy card command {HandIndex}";
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Linq;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record DiscardCommand(Guid RequestId, Guid[] CardIds) : Command(RequestId, typeof(ExecuteProgramRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
var cost = CardIds.Length * Balancing.Instance.DiscardEnergyCost;
intents.Add(new Discard(CardIds, cost));
}
public override string ToString()
{
return $"Discard cards command {string.Join(", ", CardIds.Select(id => id.ToString()))}";
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Linq;
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record MoveCardsCommand(Guid RequestId, Guid[] OrderedCards, Guid[] TapeCardIds) : Command(RequestId, typeof(ExecuteProgramRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
intents.Add(new MoveCards(OrderedCards, TapeCardIds));
}
public override string ToString()
{
return $"Move cards command: order={string.Join(", ", OrderedCards.Select(c => c.ToString()))}; tape={string.Join(", ", TapeCardIds.Select(c => c.ToString()))}";
}
}

View File

@@ -0,0 +1,19 @@
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record NextAssignmentCommand(Guid RequestId) : Command(RequestId, typeof(ScoringRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
coreLoop.ResetShop();
intents.Add(new NextPhase(ERunPhase.DrawGlitch));
}
public override string ToString()
{
return $"Next assignment command";
}
}

View File

@@ -0,0 +1,19 @@
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record PreviewProgramCommand(Guid RequestId) : Command(RequestId, typeof(ImproveRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
intents.Add(new NextPhase(ERunPhase.ExecuteProgram));
intents.Add(new EnterPreview());
}
public override string ToString()
{
return "Preview program command";
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record RerollCommand(Guid RequestId) : Command(RequestId, typeof(ImproveRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
intents.Add(new Reroll(Balancing.Instance.GetRerollEnergyCost(coreLoop.RerollCount)));
}
public override string ToString()
{
return "Reroll command";
}
}

View File

@@ -0,0 +1,18 @@
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record RunProgramCommand(Guid RequestId) : Command(RequestId, typeof(ExecuteProgramRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
intents.Add(new RunProgram());
}
public override string ToString()
{
return "Run program command";
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record StartBufferOverflowCommand(Guid RequestId) : Command(RequestId, typeof(ImproveRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
intents.Add(new EnterBufferOverflow(Balancing.Instance.BufferOverflowEnergyCost));
}
public override string ToString()
{
return "Start buffer overflow command";
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record StartGamblingCommand(Guid RequestId) : Command(RequestId, typeof(ImproveRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
intents.Add(new EnterGamble(Balancing.Instance.GambleEnergyCost));
}
public override string ToString()
{
return "Start gambling command";
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record StopBufferOverflowCommand(Guid RequestId) : Command(RequestId, typeof(BufferOverflowRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
var handCards = coreLoop.GetHandCards();
coreLoop.PatchDeck.AddRange(handCards);
foreach (var handCard in handCards)
{
coreLoop.RemoveProgramCard(handCard);
}
coreLoop.ClearTapeSelection();
intents.Add(new NextPhase(ERunPhase.Improve));
}
public override string ToString()
{
return "Stop buffer overflow command";
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
namespace RobotAndDonkey.Game.Execution.Commands;
public sealed record StopGamblingCommand(Guid RequestId) : Command(RequestId, typeof(GambleRequest))
{
protected override void CreateIntents(CoreLoop coreLoop, List<Intent> intents)
{
intents.Add(new NextPhase(ERunPhase.Improve));
}
public override string ToString()
{
return "Stop gambling command";
}
}

View File

@@ -0,0 +1,13 @@
namespace RobotAndDonkey.Game.Execution;
public enum ERunPhase
{
Init,
ExecuteProgram,
Scoring,
DrawGlitch,
Improve,
Gamble,
BufferOverflow,
_Count
}

View File

@@ -0,0 +1,139 @@
using RobotAndDonkey.Game.Execution.Commands;
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Intents;
using System;
using System.Collections.Immutable;
using System.Threading;
namespace RobotAndDonkey.Game.Execution;
public sealed record GameEvent(long Sequence, DateTimeOffset Time, object Payload);
public sealed record NextStepIssued(Request Step);
public sealed record StepApplied(ImmutableArray<Result> Results);
public sealed record StepRejected(Guid RequestId, string Reason);
public sealed record StateChanged;
public sealed record GameOver(string Reason);
public sealed class GameRuntime
{
public event EventHandler<GameEvent>? Published;
public void Start(CoreLoop coreLoop)
{
lock (m_Gate)
{
m_CoreLoop = coreLoop;
Publish(new StateChanged());
IssueNextStepLocked();
}
}
public void Submit(Command command, CancellationToken ct = default)
{
lock (m_Gate)
{
if (m_CoreLoop == null)
{
Publish(new StepRejected(command.RequestId, "No core loop started."));
return;
}
if (m_ExpectedRequest is null)
{
Publish(new StepRejected(command.RequestId, "No step expected (game over or not started)."));
return;
}
if (m_ExpectedRequest.RequestId != command.RequestId)
{
Publish(new StepRejected(command.RequestId, "Command does not match the expected step."));
return;
}
if (!m_ExpectedRequest.IsCommandCompatible(command))
{
Publish(new StepRejected(command.RequestId, "Command not compatible with the current step."));
return;
}
var results = command.Execute(m_CoreLoop, false, false);
Publish(new StepApplied([..results]));
Publish(new StateChanged());
IssueNextStepLocked();
}
}
private void IssueNextStepLocked()
{
m_ExpectedRequest = NextRequest();
if (m_ExpectedRequest is null)
Publish(new GameOver("No more steps."));
else
Publish(new NextStepIssued(m_ExpectedRequest));
}
private Request? NextRequest()
{
var coreLoop = m_CoreLoop!;
switch (coreLoop.RunPhase)
{
case ERunPhase.Init:
{
const ERunPhase firstPhase = ERunPhase.ExecuteProgram;
coreLoop.ResetShop();
new EnterPreview().Run(Guid.Empty, coreLoop, [], []);
// DEBUG
// const ERunPhase firstPhase = ERunPhase.DrawGlitch;
coreLoop.RunPhase = firstPhase;
goto case firstPhase;
}
case ERunPhase.DrawGlitch:
{
return DrawGlitchRequest.Create(coreLoop);
}
case ERunPhase.Improve:
{
return ImproveRequest.Create(coreLoop);
}
case ERunPhase.Gamble:
{
return GambleRequest.Create(coreLoop);
}
case ERunPhase.BufferOverflow:
{
return BufferOverflowRequest.Create(coreLoop);
}
case ERunPhase.ExecuteProgram:
{
return ExecuteProgramRequest.Create(coreLoop);
}
case ERunPhase.Scoring:
{
return ScoringRequest.Create(coreLoop);
}
}
return null;
}
private void Publish(object payload)
{
Published?.Invoke(this, new(Interlocked.Increment(ref m_Sequence), DateTimeOffset.UtcNow, payload));
}
private CoreLoop? m_CoreLoop;
private readonly Lock m_Gate = new();
private Request? m_ExpectedRequest;
private long m_Sequence;
}

View File

@@ -0,0 +1,18 @@
using RobotAndDonkey.Game.GameState;
namespace RobotAndDonkey.Game.Execution.Requests;
public sealed record BufferOverflowRequest(Guid RequestId) : Request(RequestId)
{
public static BufferOverflowRequest Create(CoreLoop coreLoop)
{
coreLoop.ShuffleDeck();
coreLoop.DrawHand();
return new(Guid.NewGuid());
}
public override string ToString()
{
return "Buffer overflow request";
}
}

View File

@@ -0,0 +1,18 @@
using System;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.GameState;
namespace RobotAndDonkey.Game.Execution.Requests;
public sealed record DrawGlitchRequest(Guid RequestId, Card Card) : Request(RequestId)
{
public static DrawGlitchRequest Create(CoreLoop coreLoop)
{
return new(Guid.NewGuid(), coreLoop.GlitchDeck[coreLoop.NextGlitch]);
}
public override string ToString()
{
return $"Draw glitch request: {Card}";
}
}

View File

@@ -0,0 +1,18 @@
using System;
using RobotAndDonkey.Game.Execution.Commands;
using RobotAndDonkey.Game.GameState;
namespace RobotAndDonkey.Game.Execution.Requests;
public sealed record ExecuteProgramRequest(Guid RequestId) : Request(RequestId)
{
public static ExecuteProgramRequest Create(CoreLoop coreLoop)
{
return new(Guid.NewGuid());
}
public override string ToString()
{
return "Execute program request";
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Immutable;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.GameState;
namespace RobotAndDonkey.Game.Execution.Requests;
public sealed record GambleRequest(Guid RequestId) : Request(RequestId)
{
public static GambleRequest Create(CoreLoop coreLoop)
{
return new(Guid.NewGuid());
}
public override string ToString()
{
return "Gamble request";
}
}

View File

@@ -0,0 +1,18 @@
using System;
using RobotAndDonkey.Game.Execution.Commands;
using RobotAndDonkey.Game.GameState;
namespace RobotAndDonkey.Game.Execution.Requests;
public sealed record ImproveRequest(Guid RequestId) : Request(RequestId)
{
public static ImproveRequest Create(CoreLoop coreLoop)
{
return new(Guid.NewGuid());
}
public override string ToString()
{
return "Improve request";
}
}

View File

@@ -0,0 +1,22 @@
using System;
using RobotAndDonkey.Game.Execution.Commands;
namespace RobotAndDonkey.Game.Execution.Requests;
public abstract record Request(Guid RequestId)
{
public bool IsCommandCompatible(Command command)
{
foreach (var requestType in command.RequestTypes)
{
if (requestType == GetType())
return true;
}
return false;
}
public static readonly EmptyRequest s_Empty = new();
}
public sealed record EmptyRequest() : Request(Guid.Empty);

View File

@@ -0,0 +1,18 @@
using System;
using RobotAndDonkey.Game.Execution.Commands;
using RobotAndDonkey.Game.GameState;
namespace RobotAndDonkey.Game.Execution.Requests;
public sealed record ScoringRequest(Guid RequestId) : Request(RequestId)
{
public static ScoringRequest Create(CoreLoop coreLoop)
{
return new(Guid.NewGuid());
}
public override string ToString()
{
return "Scoring request";
}
}

View File

@@ -0,0 +1,11 @@
using RobotAndDonkey.Game.Utils;
namespace RobotAndDonkey.Game.Execution.Results;
public record CellTypeResult(Guid RequestId, Board.Board Board, Hex Hex) : Result(RequestId)
{
public override string ToString()
{
return $"Cell type result: {Hex}";
}
}

View File

@@ -0,0 +1,13 @@
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.GameState;
using System;
namespace RobotAndDonkey.Game.Execution.Results;
public record CurrencyResult(Guid RequestId, Currency NewCurrency) : Result(RequestId)
{
public override string ToString()
{
return $"Currency result: {NewCurrency}";
}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace RobotAndDonkey.Game.Execution.Results;
public record DeferGlitchCountResult(Guid RequestId, int NewDeferGlitchCount) : Result(RequestId)
{
public override string ToString()
{
return $"Defer glitch count result: {NewDeferGlitchCount}";
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Cards;
namespace RobotAndDonkey.Game.Execution.Results;
public record DiscardResult(Guid RequestId, IReadOnlyList<Card> Discard) : Result(RequestId)
{
public override string ToString()
{
return $"Discard result: {string.Join(", ", Discard.Select(c => c.ToString()))}";
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Cards;
namespace RobotAndDonkey.Game.Execution.Results;
public record HandResult(Guid RequestId, IReadOnlyList<Card> Hand) : Result(RequestId)
{
public override string ToString()
{
return $"Hand result: {string.Join(", ", Hand.Select(c => c.ToString()))}";
}
}

View File

@@ -0,0 +1,27 @@
using System;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Cards;
namespace RobotAndDonkey.Game.Execution.Results;
public enum EInvalidReason
{
NotFound,
Invariant,
Blocked,
OutOfBounds,
NoEnergy,
NoTarget,
NoAmount,
AlreadyExecuted,
Invalid,
NoSpace
}
public record InvalidInstructionResult(Guid RequestId, EInvalidReason Reason, Card? Card, Cell? Cell) : Result(RequestId)
{
public override string ToString()
{
return $"Invalid instruction result: {Reason}, {Card}, {Cell}";
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using RobotAndDonkey.Game.Cards;
namespace RobotAndDonkey.Game.Execution.Results;
public record DeckResult(Guid RequestId, IReadOnlyList<Card> Deck) : Result(RequestId)
{
public override string ToString()
{
return $"Deck result: {string.Join(", ", Deck.Select(c => c.ToString()))}";
}
}

View File

@@ -0,0 +1,22 @@
using System;
using RobotAndDonkey.Game.Cards;
namespace RobotAndDonkey.Game.Execution.Results;
public enum ECardLocation
{
Tape,
Hand,
Deck,
Discard,
Robot,
Board
}
public record ModifyCardResult(Guid RequestId, Card Card, EModifierId Modifier, ECardLocation Location) : Result(RequestId)
{
public override string ToString()
{
return $"Modify card result in {Location}: {Card} - {Modifier}";
}
}

View File

@@ -0,0 +1,11 @@
using RobotAndDonkey.Game.Utils;
namespace RobotAndDonkey.Game.Execution.Results;
public record ModifyCellResult(Guid RequestId, Board.Board Board, EModifierId Modifier, Hex Hex) : Result(RequestId)
{
public override string ToString()
{
return $"Modify Cell result: {Hex} {Modifier}";
}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace RobotAndDonkey.Game.Execution.Results;
public record ModifyRobotResult(Guid RequestId) : Result(RequestId)
{
public override string ToString()
{
return "Modify robot result";
}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace RobotAndDonkey.Game.Execution.Results;
public record NextGlitchResult(Guid RequestId, int NewNextGlitch) : Result(RequestId)
{
public override string ToString()
{
return $"Next glitch result: {NewNextGlitch}";
}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace RobotAndDonkey.Game.Execution.Results;
public record NoMoreBufferOverflowResult(Guid RequestId) : Result(RequestId)
{
public override string ToString()
{
return $"No more buffer overflow result";
}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace RobotAndDonkey.Game.Execution.Results;
public record NoMoreGamblingResult(Guid RequestId) : Result(RequestId)
{
public override string ToString()
{
return "No more gambling result";
}
}

View File

@@ -0,0 +1,12 @@
using RobotAndDonkey.Game.Pois;
using RobotAndDonkey.Game.Utils;
namespace RobotAndDonkey.Game.Execution.Results;
public record PoiResult(Guid RequestId, Board.Board Board, Poi? Poi, Hex Hex) : Result(RequestId)
{
public override string ToString()
{
return $"Poi result: {Hex}";
}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace RobotAndDonkey.Game.Execution.Results;
public record ProgramResult(Guid RequestId, int NewProgram) : Result(RequestId)
{
public override string ToString()
{
return $"Program result: {NewProgram}";
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using RobotAndDonkey.Game.Cards;
namespace RobotAndDonkey.Game.Execution.Results;
public record ProgramRowResult(Guid RequestId, IReadOnlyList<Card> OrderedCards, IReadOnlyCollection<Guid> TapeCardIds) : Result(RequestId)
{
public override string ToString()
{
return $"Program row result: {string.Join(", ", OrderedCards.Select(c => c.ToString()))}; tape={string.Join(", ", TapeCardIds.Select(id => id.ToString()))}";
}
}

View File

@@ -0,0 +1,5 @@
using System;
namespace RobotAndDonkey.Game.Execution.Results;
public abstract record Result(Guid RequestId);

View File

@@ -0,0 +1,12 @@
using System;
using RobotAndDonkey.Game.Cards;
namespace RobotAndDonkey.Game.Execution.Results;
public record RunCardResult(Guid RequestId, Card? Card) : Result(RequestId)
{
public override string ToString()
{
return $"Run card result: {Card}";
}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace RobotAndDonkey.Game.Execution.Results;
public record RunPhaseResult(Guid RequestId, ERunPhase NewRunPhase) : Result(RequestId)
{
public override string ToString()
{
return $"Run phase result: {NewRunPhase}";
}
}

View File

@@ -0,0 +1,11 @@
using RobotAndDonkey.Game.Cards;
namespace RobotAndDonkey.Game.Execution.Results;
public record ShopResult(Guid RequestId, Card[] Shop) : Result(RequestId)
{
public override string ToString()
{
return $"Shop result: {string.Join(", ", Shop.Select(c => c.ToString()))}";
}
}

View File

@@ -0,0 +1,11 @@
using RobotAndDonkey.Game.Cards;
namespace RobotAndDonkey.Game.Execution.Results;
public record TapeResult(Guid RequestId, IReadOnlyList<Card> Tape) : Result(RequestId)
{
public override string ToString()
{
return $"Tape result: {string.Join(", ", Tape.Select(c => c.ToString()))}";
}
}

View File

@@ -0,0 +1,348 @@
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.Cards.Glitches;
using RobotAndDonkey.Game.Cards.Patches;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution;
using RobotAndDonkey.Game.Intents;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Robots;
using RobotAndDonkey.Game.Utils;
namespace RobotAndDonkey.Game.GameState;
public record CoreLoop(SRandom random, ImmutableArray<Card> GlitchDeck, Board.Board Board, Robot Robot)
{
public static ImmutableArray<Card> CreatePatchDeck(ref SRandom random, EDifficulty difficulty, int amount, bool includeModifiers)
{
int GetRarityWeight(ERarity rarity)
{
return rarity switch
{
ERarity.Common => Balancing.Instance.CommonWeight,
ERarity.Magic => Balancing.Instance.MagicWeight,
ERarity.Uncommon => Balancing.Instance.UncommonWeight,
ERarity.Rare => Balancing.Instance.RareWeight,
ERarity.Legendary => Balancing.Instance.LegendaryWeight,
_ => 0
};
}
var allCards = new List<PatchCard>();
foreach (var type in s_PatchCardTypes.Values)
allCards.Add((PatchCard)Activator.CreateInstance(type)!);
var byRarity = allCards.GroupBy(c => c.Rarity).ToDictionary(g => g.Key, g => g.ToList());
var result = new List<Card>(Math.Max(0, amount));
ERarity PickRarity(ref SRandom random)
{
var candidates = byRarity.Where(kvp => kvp.Value.Count > 0).Select(kvp => (kvp.Key, Value: GetRarityWeight(kvp.Key))).ToArray();
if (candidates.Length == 0)
return ERarity.Common;
var total = candidates.Sum(kv => kv.Value);
var roll = random.Next(total + 1);
var accum = 0;
foreach (var (rarity, weight) in candidates)
{
accum += weight;
if (roll <= accum)
return rarity;
}
return candidates[^1].Key;
}
while (result.Count < amount)
{
if (!byRarity.Values.Any(list => list.Count > 0))
break;
var rarity = PickRarity(ref random);
var bucket = byRarity[rarity];
var idx = random.Next(bucket.Count);
var chosen = bucket[idx];
result.Add(chosen.DeepClone());
bucket.RemoveAt(idx);
}
if (!includeModifiers)
return [..result];
var fDifficulty = 0.5f + (int)difficulty / 4.0f;
var buffChance = Balancing.Instance.ShopBuffChance * (1 - fDifficulty);
var debuffChance = Balancing.Instance.ShopDebuffChance * fDifficulty;
for (int i = 0; i < result.Count; ++i)
{
var buff = random.NextSingle() <= buffChance;
var debuff = random.NextSingle() <= debuffChance;
if (debuff)
{
result[i].AddModifier(ModifyCard.Create(random.Next(4) switch {
0 => EModifierId.Corrupt,
1 => EModifierId.Unreliable,
2 => EModifierId.RaceCondition,
3 => EModifierId.Throttled}, EModifierDuration.Permanent), []);
}
if (buff)
{
result[i].AddModifier(ModifyCard.Create(random.Next(4) switch {
0 => EModifierId.Effective,
1 => EModifierId.Optimized,
2 => EModifierId.Efficient,
3 => EModifierId.Persistent}, EModifierDuration.Permanent), []);
}
}
return [..result];
}
public CoreLoop(SRandom random, MatchParameters parameters) : this(random, CreateGlitchDeck(ref random), RobotAndDonkey.Game.Board.Board.Generate(ref random, parameters.Difficulty), CreateRobot(parameters))
{
Difficulty = parameters.Difficulty;
m_Random = random;
Currency = Robot.Currency;
PatchDeck = Robot.Deck.Select(c => (Card)Activator.CreateInstance(s_PatchCardTypes[c])!).ToList();
ResetShop();
}
public CoreLoop(CoreLoop clone)
{
GlitchDeck = [..clone.GlitchDeck.Select(c => c.DeepClone())];
Board = new(clone.Board);
Robot = clone.Robot.DeepClone();
NextGlitch = clone.NextGlitch;
DeferGlitchCount = clone.DeferGlitchCount;
RunPhase = clone.RunPhase;
ProgramCount = clone.ProgramCount;
RerollCount = clone.RerollCount;
EnergyWasted = clone.EnergyWasted;
ProgramsExecuted = clone.ProgramsExecuted;
InstructionsUsed = clone.InstructionsUsed;
Overspill = clone.Overspill;
PathLength = clone.PathLength;
Random = clone.Random;
Currency = clone.Currency;
CanGamble = clone.CanGamble;
CanBufferOverflow = clone.CanBufferOverflow;
HasDonkey = clone.HasDonkey;
Difficulty = clone.Difficulty;
CellsVisited = clone.CellsVisited.ToHashSet();
Shop = clone.Shop.Select(c => c.DeepClone()).ToList();
BoosterPack = clone.BoosterPack.Select(c => c.DeepClone()).ToList();
Discard = clone.Discard.Select(c => c.DeepClone()).ToList();
ProgramRow = clone.ProgramRow.Select(c => c.DeepClone()).ToList();
m_TapeCardIds = clone.m_TapeCardIds.ToHashSet();
PatchDeck = clone.PatchDeck.Select(c => c.DeepClone()).ToList();
}
private static ImmutableArray<Card> CreateGlitchDeck(ref SRandom random)
{
var allCards = new List<GlitchCard>();
foreach (var type in s_GlitchCardTypes.Values)
allCards.Add((GlitchCard)Activator.CreateInstance(type)!);
random.Shuffle(CollectionsMarshal.AsSpan(allCards));
return [..allCards];
}
private static Robot CreateRobot(MatchParameters parameters)
{
return parameters.RobotType switch
{
ERobotType.Vintage => new Vintage(parameters),
ERobotType.Analyst => new Analyst(parameters),
_ => throw new NotImplementedException()
};
}
public void ResetShop()
{
PatchDeck.AddRange(ProgramRow);
PatchDeck.AddRange(Discard);
ProgramRow.Clear();
m_TapeCardIds.Clear();
Discard.Clear();
ShuffleDeck();
RerollPatchDeck();
ProgramCount = 0;
RerollCount = 0;
CanGamble = true;
CanBufferOverflow = true;
}
public void RerollPatchDeck()
{
Shop.Clear();
Shop.AddRange(CreatePatchDeck(ref m_Random, Difficulty, Balancing.Instance.ShopSize, false).ToList());
RerollCount += 1;
}
public void DrawHand()
{
var handCount = GetHandCards().Count;
var toDraw = Math.Min(PatchDeck.Count, Math.Max(0, Currency.HandSize - handCount));
var drawnCards = PatchDeck.Take(toDraw).ToList();
ProgramRow.AddRange(drawnCards);
PatchDeck.RemoveRange(0, toDraw);
}
public List<Card> GetHandCards()
{
return ProgramRow.Where(card => !m_TapeCardIds.Contains(card.CardId)).ToList();
}
public List<Card> GetTapeCards()
{
return ProgramRow.Where(card => m_TapeCardIds.Contains(card.CardId)).ToList();
}
public bool IsTapeCard(Card card)
{
return m_TapeCardIds.Contains(card.CardId);
}
public void SetProgramRow(IReadOnlyList<Card> orderedCards, IReadOnlyCollection<Guid> tapeCardIds)
{
ProgramRow.Clear();
ProgramRow.AddRange(orderedCards);
m_TapeCardIds.Clear();
if (tapeCardIds.Count == 0)
return;
var validIds = ProgramRow.Select(card => card.CardId).ToHashSet();
foreach (var cardId in tapeCardIds)
{
if (validIds.Contains(cardId))
m_TapeCardIds.Add(cardId);
}
}
public void ClearTapeSelection()
{
m_TapeCardIds.Clear();
}
public void RemoveProgramCard(Card card)
{
m_TapeCardIds.Remove(card.CardId);
ProgramRow.Remove(card);
}
public void InsertProgramCard(int index, Card card, bool select)
{
ProgramRow.Insert(index, card);
if (select)
m_TapeCardIds.Add(card.CardId);
else
m_TapeCardIds.Remove(card.CardId);
}
public void AddProgramCard(Card card, bool select)
{
ProgramRow.Add(card);
if (select)
m_TapeCardIds.Add(card.CardId);
else
m_TapeCardIds.Remove(card.CardId);
}
public int GetProgramIndexForTapeIndex(int tapeIndex)
{
if (tapeIndex < 0)
return -1;
var current = -1;
for (var i = 0; i < ProgramRow.Count; i++)
{
if (!m_TapeCardIds.Contains(ProgramRow[i].CardId))
continue;
current += 1;
if (current == tapeIndex)
return i;
}
return -1;
}
public bool SwapTapeCards(int tapeIndexA, int tapeIndexB)
{
var indexA = GetProgramIndexForTapeIndex(tapeIndexA);
var indexB = GetProgramIndexForTapeIndex(tapeIndexB);
if (indexA < 0 || indexB < 0 || indexA == indexB)
return false;
(ProgramRow[indexA], ProgramRow[indexB]) = (ProgramRow[indexB], ProgramRow[indexA]);
return true;
}
public void ShuffleDeck()
{
m_Random.Shuffle(CollectionsMarshal.AsSpan(PatchDeck));
}
public int NextGlitch { get; set; }
public int DeferGlitchCount { get; set; }
public ERunPhase RunPhase { get; set; }
public int ProgramCount { get; set; }
public int RerollCount { get; set; }
public int EnergyWasted { get; set; }
public int ProgramsExecuted { get; set; }
public int InstructionsUsed { get; set; }
public int Overspill { get; set; }
public int PathLength { get; set; }
public ref SRandom Random => ref m_Random;
public ref Currency Currency => ref Robot.Currency;
public bool CanGamble { get; set; } = true;
public bool CanBufferOverflow { get; set; } = true;
public bool HasDonkey { get; set; }
public HashSet<Hex> CellsVisited { get; } = [];
public List<Card> Shop { get; } = [];
public List<Card> BoosterPack { get; set; } = [];
public List<Card> Discard { get; } = [];
public List<Card> ProgramRow { get; } = [];
public IReadOnlyList<Card> Hand => GetHandCards();
public List<Card> PatchDeck { get; } = [];
public IReadOnlyList<Card> Tape => GetTapeCards();
public IReadOnlyCollection<Guid> TapeCardIds => m_TapeCardIds;
public bool IsPreview { get; init; }
public EDifficulty Difficulty { get; }
private SRandom m_Random = random;
private readonly HashSet<Guid> m_TapeCardIds = [];
private static readonly Dictionary<ECard, Type> s_PatchCardTypes = typeof(CoreLoop).Assembly.GetTypes().Where(t => !t.IsAbstract && t.BaseType == typeof(PatchCard) || t.BaseType?.BaseType == typeof(PatchCard)).ToDictionary(t => ((PatchCard)Activator.CreateInstance(t)!).Id);
private static readonly Dictionary<ECard, Type> s_GlitchCardTypes = typeof(CoreLoop).Assembly.GetTypes().Where(t => !t.IsAbstract && t.BaseType == typeof(GlitchCard) || t.BaseType?.BaseType == typeof(GlitchCard)).ToDictionary(t => ((GlitchCard)Activator.CreateInstance(t)!).Id);
}

View File

@@ -0,0 +1,3 @@
namespace RobotAndDonkey.Game.GameState;
public record struct Currency(int Energy, int MaxCarry, int Carry, int Delivery, int TapeLength, int HandSize);

View File

@@ -0,0 +1,14 @@
using RobotAndDonkey.Game.Robots;
using RobotAndDonkey.Game.Utils;
namespace RobotAndDonkey.Game.GameState;
public record GameState(CoreLoop CoreLoop, MetaGame MetaGame)
{
public static GameState CreateNew(MatchParameters parameters)
{
var random = new SRandom((ulong)parameters.Seed);
var coreLoop = new CoreLoop(random, parameters);
return new(coreLoop, new([]));
}
}

Some files were not shown because too many files have changed in this diff Show More