ported from perforce
This commit is contained in:
40
RobotAndDonkey.Game/Utils/CardExtensions.cs
Normal file
40
RobotAndDonkey.Game/Utils/CardExtensions.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using RobotAndDonkey.Game.Cards;
|
||||
using RobotAndDonkey.Game.GameState;
|
||||
using RobotAndDonkey.Game.Modifiers;
|
||||
|
||||
namespace RobotAndDonkey.Game.Utils;
|
||||
|
||||
public static class CardExtensions
|
||||
{
|
||||
public static int NextIndexConsideringCorruption(int cardIndex, CoreLoop coreLoop)
|
||||
{
|
||||
var tape = coreLoop.GetTapeCards();
|
||||
if (cardIndex < 0 || cardIndex >= tape.Count)
|
||||
return -1;
|
||||
|
||||
var card = tape[cardIndex];
|
||||
var isCorrupted = card.Modifiers.Any(m => m is CorruptModifierBase && m.DebuffSources.Count == 0);
|
||||
cardIndex = isCorrupted ? cardIndex - 1 : cardIndex + 1;
|
||||
if (cardIndex < 0 || cardIndex >= tape.Count)
|
||||
return -1;
|
||||
|
||||
return cardIndex;
|
||||
}
|
||||
|
||||
public static List<Card> SortForHand(IEnumerable<Card> cards)
|
||||
{
|
||||
return cards.Order(Comparer<Card>.Create((a, b) =>
|
||||
{
|
||||
var cmp = a.Rarity.CompareTo(b.Rarity);
|
||||
if (cmp == 0)
|
||||
cmp = a.Id.CompareTo(b.Id);
|
||||
if (cmp == 0)
|
||||
cmp = a.Modifiers.Count.CompareTo(b.Modifiers.Count);
|
||||
if (cmp == 0)
|
||||
cmp = a.CardId.CompareTo(b.CardId);
|
||||
return cmp;
|
||||
})).ToList();
|
||||
}
|
||||
}
|
||||
25
RobotAndDonkey.Game/Utils/DirectionExtensions.cs
Normal file
25
RobotAndDonkey.Game/Utils/DirectionExtensions.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RobotAndDonkey.Game.Utils;
|
||||
|
||||
|
||||
public static class DirectionExtensions
|
||||
{
|
||||
public static EDirection Opposite(this EDirection d) => Modify(d, 3);
|
||||
public static EDirection RotateLeft(this EDirection d) => Modify(d, 1);
|
||||
public static EDirection RotateRight(this EDirection d) => Modify(d, 5);
|
||||
|
||||
public static EDirection Modify(EDirection direction, int delta)
|
||||
{
|
||||
var i = (int)direction;
|
||||
i += delta;
|
||||
i %= 6;
|
||||
if (i < 0)
|
||||
i += 6;
|
||||
return (EDirection)i;
|
||||
}
|
||||
}
|
||||
24
RobotAndDonkey.Game/Utils/Disposable.cs
Normal file
24
RobotAndDonkey.Game/Utils/Disposable.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace RobotAndDonkey.Game.Utils;
|
||||
|
||||
public class Disposable : IDisposable
|
||||
{
|
||||
public static Disposable Create(Action action)
|
||||
{
|
||||
return new(action);
|
||||
}
|
||||
|
||||
private Disposable(Action action)
|
||||
{
|
||||
m_Action = action;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_Action?.Invoke();
|
||||
m_Action = null;
|
||||
}
|
||||
|
||||
private Action? m_Action;
|
||||
}
|
||||
79
RobotAndDonkey.Game/Utils/Hex.cs
Normal file
79
RobotAndDonkey.Game/Utils/Hex.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace RobotAndDonkey.Game.Utils;
|
||||
|
||||
public record struct Hex(int X, int Y)
|
||||
{
|
||||
public static int Distance(Hex a, Hex b)
|
||||
{
|
||||
var (acx, acy, acz) = OffsetToCube(a);
|
||||
var (bcx, bcy, bcz) = OffsetToCube(b);
|
||||
return Math.Max(Math.Max(Math.Abs(acx - bcx), Math.Abs(acy - bcy)), Math.Abs(acz - bcz));
|
||||
|
||||
static (int, int, int) OffsetToCube(Hex hex)
|
||||
{
|
||||
var x = hex.X - (hex.Y - (hex.Y & 1)) / 2;
|
||||
var z = hex.Y;
|
||||
var y = -x - z;
|
||||
return (x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
public static Hex FromWorld(Vector2 coords)
|
||||
{
|
||||
var cx = coords.X * MathF.Sqrt(3) / 3 - coords.Y / 3;
|
||||
var cz = coords.Y * 2 / 3;
|
||||
var cy = -cx - cz;
|
||||
var rx = (int)MathF.Round(cx);
|
||||
var ry = (int)MathF.Round(cy);
|
||||
var rz = (int)MathF.Round(cz);
|
||||
|
||||
var xDiff = MathF.Abs(rx - cx);
|
||||
var yDiff = MathF.Abs(ry - cy);
|
||||
var zDiff = MathF.Abs(rz - cz);
|
||||
|
||||
if (xDiff > yDiff && xDiff > zDiff)
|
||||
rx = -ry - rz;
|
||||
else if (yDiff > zDiff)
|
||||
ry = -rx - rz;
|
||||
else
|
||||
rz = -rx - ry;
|
||||
|
||||
var col = rx + (rz - (rz & 1)) / 2;
|
||||
var row = rz;
|
||||
return new(col, row);
|
||||
}
|
||||
|
||||
public static Hex FromPixel(Vector2 coords, float size)
|
||||
{
|
||||
return FromWorld(new(coords.X / size, coords.Y / size));
|
||||
}
|
||||
|
||||
public Hex GetNeighbour(EDirection direction)
|
||||
{
|
||||
var parity = Y & 1;
|
||||
var dir = offsetDirections[parity, (int)direction];
|
||||
return new(X + dir.X, Y + dir.Y);
|
||||
}
|
||||
|
||||
public Vector2 ToWorld()
|
||||
{
|
||||
var x = MathF.Sqrt(3) * (X + 0.5f * (Y & 1));
|
||||
var y = 3.0f / 2 * Y;
|
||||
return new(x, y);
|
||||
}
|
||||
|
||||
public Vector2 ToPixel(float size)
|
||||
{
|
||||
var w = ToWorld();
|
||||
return new(w.X * size, w.Y * size);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"({X}, {Y})";
|
||||
}
|
||||
|
||||
private static readonly Hex[,] offsetDirections = { { new(+1, 0), new(0, -1), new(-1, -1), new(-1, 0), new(-1, +1), new(0, +1) }, { new(+1, 0), new(+1, -1), new(0, -1), new(-1, 0), new(0, +1), new(+1, +1) } };
|
||||
}
|
||||
100
RobotAndDonkey.Game/Utils/SRandom.cs
Normal file
100
RobotAndDonkey.Game/Utils/SRandom.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
|
||||
namespace RobotAndDonkey.Game.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Implements a XorShift* PRNG, with 64 bits of internal state.
|
||||
/// See http://en.wikipedia.org/wiki/Xorshift
|
||||
/// </summary>
|
||||
public struct SRandom
|
||||
{
|
||||
public SRandom(ulong seed)
|
||||
{
|
||||
if (seed == 0)
|
||||
throw new InvalidOperationException("Seed needs to be bigger than zero.");
|
||||
|
||||
Seed = seed;
|
||||
}
|
||||
|
||||
public void Shuffle<T>(Span<T> span)
|
||||
{
|
||||
var n = span.Length;
|
||||
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
var r = i + Next(n - i);
|
||||
(span[r], span[i]) = (span[i], span[r]);
|
||||
}
|
||||
}
|
||||
|
||||
public ulong Next()
|
||||
{
|
||||
Seed ^= Seed >> 12;
|
||||
Seed ^= Seed << 25;
|
||||
Seed ^= Seed >> 27;
|
||||
return Seed * 2685821657736338717UL; // multiplier taken from wikipedia article on XorShift PRNGs
|
||||
}
|
||||
|
||||
public ulong Next(ulong upperLimit)
|
||||
{
|
||||
if (upperLimit == 0)
|
||||
return 0;
|
||||
|
||||
return Next() % upperLimit;
|
||||
}
|
||||
|
||||
public uint Next(uint upperLimit)
|
||||
{
|
||||
if (upperLimit == 0)
|
||||
return 0;
|
||||
|
||||
return (uint)(Next() % upperLimit);
|
||||
}
|
||||
|
||||
public uint Next(uint lowerLimit, uint upperLimit)
|
||||
{
|
||||
if (upperLimit == 0)
|
||||
return 0;
|
||||
|
||||
if (lowerLimit == upperLimit)
|
||||
return lowerLimit;
|
||||
|
||||
return lowerLimit + (uint)(Next() % (upperLimit - lowerLimit));
|
||||
}
|
||||
|
||||
public int Next(int upperLimit)
|
||||
{
|
||||
if (upperLimit == 0)
|
||||
return 0;
|
||||
|
||||
return (int)(Next() % (uint)(upperLimit & 0x7fffffff));
|
||||
}
|
||||
|
||||
public int Next(int lowerLimit, int upperLimit)
|
||||
{
|
||||
if (upperLimit == 0)
|
||||
return 0;
|
||||
|
||||
if (lowerLimit == upperLimit)
|
||||
return lowerLimit;
|
||||
|
||||
return lowerLimit + (int)(Next() % (uint)((upperLimit - lowerLimit) & 0x7fffffff));
|
||||
}
|
||||
|
||||
public double NextDouble()
|
||||
{
|
||||
return Next(int.MaxValue) * (1.0 / int.MaxValue);
|
||||
}
|
||||
|
||||
public float NextSingle()
|
||||
{
|
||||
return (float)NextDouble();
|
||||
}
|
||||
|
||||
public readonly override string ToString()
|
||||
{
|
||||
return $"0x{Seed:X}";
|
||||
}
|
||||
|
||||
public ulong Seed { get; internal set; }
|
||||
}
|
||||
159
RobotAndDonkey.Game/Utils/SeedString.cs
Normal file
159
RobotAndDonkey.Game/Utils/SeedString.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace RobotAndDonkey.Game.Utils;
|
||||
|
||||
public static class SeedString
|
||||
{
|
||||
/// <summary>
|
||||
/// Encode an int seed to a short, human-friendly string.
|
||||
/// </summary>
|
||||
/// <param name="seed">Any 32-bit signed integer.</param>
|
||||
/// <param name="includeChecksum">Append 1 char to detect typos.</param>
|
||||
/// <param name="group">Insert '-' every 4 chars for readability.</param>
|
||||
public static string ToString(int seed, bool includeChecksum = true, bool group = false)
|
||||
{
|
||||
var u = unchecked((uint)seed);
|
||||
|
||||
// Convert to base-32 (most-significant digit first)
|
||||
if (u == 0)
|
||||
return includeChecksum ? "0" + CheckChar(0) : "0";
|
||||
|
||||
var digits = new List<char>(8);
|
||||
while (u > 0)
|
||||
{
|
||||
var v = (int)(u & 31);
|
||||
digits.Add(Alphabet[v]);
|
||||
u >>= 5;
|
||||
}
|
||||
|
||||
digits.Reverse();
|
||||
|
||||
var sb = new StringBuilder(digits.Count + (includeChecksum ? 1 : 0) + (group ? digits.Count / 4 : 0));
|
||||
for (var i = 0; i < digits.Count; i++)
|
||||
{
|
||||
if (group && i > 0 && i % 4 == 0)
|
||||
sb.Append('-');
|
||||
sb.Append(digits[i]);
|
||||
}
|
||||
|
||||
if (includeChecksum)
|
||||
sb.Append(CheckChar(unchecked((uint)seed)));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to parse a seed string back to the original int.
|
||||
/// </summary>
|
||||
/// <param name="code">Case-insensitive; hyphens/spaces/underscores ignored. O=0, I/L=1 accepted.</param>
|
||||
/// <param name="seed">Decoded seed.</param>
|
||||
/// <param name="requireChecksum">If true, only accept strings with a valid trailing checksum.</param>
|
||||
public static bool TryParse(string code, out int seed, bool requireChecksum = false)
|
||||
{
|
||||
seed = 0;
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
return false;
|
||||
|
||||
var t = Normalize(code);
|
||||
if (t.Length == 0)
|
||||
return false;
|
||||
|
||||
// If checksum is required (or present), prefer validating it:
|
||||
if (t.Length >= 2)
|
||||
{
|
||||
if (TryParsePayload(t.AsSpan(0, t.Length - 1), out var u) && TryMap(t[^1], out var chk) && chk == Check5(u))
|
||||
{
|
||||
seed = unchecked((int)u);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (requireChecksum)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fallback: treat entire string as payload without checksum.
|
||||
if (TryParsePayload(t.AsSpan(), out var uv))
|
||||
{
|
||||
seed = unchecked((int)uv);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse or throw (optionally requiring checksum).
|
||||
/// </summary>
|
||||
public static int Parse(string code, bool requireChecksum = false)
|
||||
{
|
||||
return TryParse(code, out var seed, requireChecksum) ? seed : throw new FormatException("Invalid seed string.");
|
||||
}
|
||||
|
||||
// ---------- publics ----------
|
||||
|
||||
private static Dictionary<char, int> BuildMap()
|
||||
{
|
||||
var d = new Dictionary<char, int>(64);
|
||||
for (var i = 0; i < Alphabet.Length; i++)
|
||||
d[Alphabet[i]] = i;
|
||||
|
||||
// Forgiving mappings
|
||||
d['O'] = d['0']; // O -> 0
|
||||
d['I'] = d['1']; // I -> 1
|
||||
d['L'] = d['1']; // L -> 1
|
||||
return d;
|
||||
}
|
||||
|
||||
private static string Normalize(string s)
|
||||
{
|
||||
var sb = new StringBuilder(s.Length);
|
||||
foreach (var ch in s)
|
||||
{
|
||||
if (ch == '-' || ch == '_' || char.IsWhiteSpace(ch))
|
||||
continue;
|
||||
sb.Append(char.ToUpperInvariant(ch));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static bool TryParsePayload(ReadOnlySpan<char> span, out uint u)
|
||||
{
|
||||
u = 0;
|
||||
if (span.Length == 0)
|
||||
return false;
|
||||
if (span.Length > 7)
|
||||
return false; // 7 base32 digits cover full 32-bit range (5 bits per digit)
|
||||
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
{
|
||||
if (!TryMap(span[i], out var v))
|
||||
return false;
|
||||
u = (u << 5) | (uint)v;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryMap(char c, out int v)
|
||||
{
|
||||
return CharToVal.TryGetValue(char.ToUpperInvariant(c), out v);
|
||||
}
|
||||
|
||||
// 5-bit checksum: multiplicative hash then take top 5 bits.
|
||||
private static int Check5(uint u)
|
||||
{
|
||||
return (int)((u * 0x9E3779B1u) >> 27);
|
||||
}
|
||||
|
||||
private static char CheckChar(uint u)
|
||||
{
|
||||
return Alphabet[Check5(u)];
|
||||
}
|
||||
|
||||
// Crockford Base32 (no I, L, O, U)
|
||||
private const string Alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
||||
private static readonly Dictionary<char, int> CharToVal = BuildMap();
|
||||
}
|
||||
Reference in New Issue
Block a user