using System; using System.Collections.Generic; using System.Text; namespace RobotAndDonkey.Game.Utils; public static class SeedString { /// /// Encode an int seed to a short, human-friendly string. /// /// Any 32-bit signed integer. /// Append 1 char to detect typos. /// Insert '-' every 4 chars for readability. 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(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(); } /// /// Try to parse a seed string back to the original int. /// /// Case-insensitive; hyphens/spaces/underscores ignored. O=0, I/L=1 accepted. /// Decoded seed. /// If true, only accept strings with a valid trailing checksum. 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; } /// /// Parse or throw (optionally requiring checksum). /// 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 BuildMap() { var d = new Dictionary(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 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 CharToVal = BuildMap(); }