Skip to content

Instantly share code, notes, and snippets.

@stephenkirk
Last active June 16, 2025 09:48
Show Gist options
  • Select an option

  • Save stephenkirk/82d841ff959234d1f743a082b98381e5 to your computer and use it in GitHub Desktop.

Select an option

Save stephenkirk/82d841ff959234d1f743a082b98381e5 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.ComponentModel.DataAnnotations;
namespace Balatro.Identity.Generation.Services
{
public interface IWordListProvider
{
IEnumerable<string> GetAdjectives();
IEnumerable<string> GetNouns();
}
public interface IRandomNumberGenerator
{
int Next(int maxValue);
}
public interface IIdentifierGenerationService
{
Task<string> GenerateIdentifierAsync(string typePrefix, CancellationToken cancellationToken = default);
}
public interface IIdentifierValidationService
{
ValidationResult ValidateTypePrefix(string typePrefix);
}
public class IdentifierGenerationOptions
{
public const string SectionName = "IdentifierGeneration";
[Required]
[MinLength(1)]
public string[] Adjectives { get; set; } = Array.Empty<string>();
[Required]
[MinLength(1)]
public string[] Nouns { get; set; } = Array.Empty<string>();
[Range(1, 100)]
public int MaxTypePrefixLength { get; set; } = 20;
public bool EnableTelemetry { get; set; } = true;
}
public class CryptographicRandomNumberGenerator : IRandomNumberGenerator
{
private readonly Random _random;
public CryptographicRandomNumberGenerator()
{
_random = new Random(Guid.NewGuid().GetHashCode());
}
public int Next(int maxValue) => _random.Next(maxValue);
}
public class ConfigurationBasedWordListProvider : IWordListProvider
{
private readonly IdentifierGenerationOptions _options;
public ConfigurationBasedWordListProvider(IOptions<IdentifierGenerationOptions> options)
{
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
}
public IEnumerable<string> GetAdjectives() => _options.Adjectives;
public IEnumerable<string> GetNouns() => _options.Nouns;
}
public class IdentifierValidationService : IIdentifierValidationService
{
private readonly IdentifierGenerationOptions _options;
private readonly ILogger<IdentifierValidationService> _logger;
public IdentifierValidationService(
IOptions<IdentifierGenerationOptions> options,
ILogger<IdentifierValidationService> logger)
{
_options = options.Value;
_logger = logger;
}
public ValidationResult ValidateTypePrefix(string typePrefix)
{
if (string.IsNullOrWhiteSpace(typePrefix))
{
_logger.LogWarning("Type prefix validation failed: null or whitespace");
return new ValidationResult("Type prefix cannot be null or whitespace");
}
if (typePrefix.Length > _options.MaxTypePrefixLength)
{
_logger.LogWarning("Type prefix validation failed: exceeds maximum length of {MaxLength}",
_options.MaxTypePrefixLength);
return new ValidationResult($"Type prefix cannot exceed {_options.MaxTypePrefixLength} characters");
}
if (typePrefix.Any(c => !char.IsLetterOrDigit(c) && c != '-' && c != '_'))
{
_logger.LogWarning("Type prefix validation failed: contains invalid characters");
return new ValidationResult("Type prefix can only contain letters, digits, hyphens, and underscores");
}
return ValidationResult.Success!;
}
}
public class BalatroIdentifierGenerationService : IIdentifierGenerationService
{
private readonly IWordListProvider _wordListProvider;
private readonly IRandomNumberGenerator _randomNumberGenerator;
private readonly IIdentifierValidationService _validationService;
private readonly ILogger<BalatroIdentifierGenerationService> _logger;
private readonly IdentifierGenerationOptions _options;
public BalatroIdentifierGenerationService(
IWordListProvider wordListProvider,
IRandomNumberGenerator randomNumberGenerator,
IIdentifierValidationService validationService,
ILogger<BalatroIdentifierGenerationService> logger,
IOptions<IdentifierGenerationOptions> options)
{
_wordListProvider = wordListProvider ?? throw new ArgumentNullException(nameof(wordListProvider));
_randomNumberGenerator = randomNumberGenerator ?? throw new ArgumentNullException(nameof(randomNumberGenerator));
_validationService = validationService ?? throw new ArgumentNullException(nameof(validationService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
}
public async Task<string> GenerateIdentifierAsync(string typePrefix, CancellationToken cancellationToken = default)
{
using var activity = _options.EnableTelemetry ?
System.Diagnostics.Activity.Current?.Source.StartActivity("GenerateIdentifier") : null;
_logger.LogDebug("Beginning identifier generation for type prefix: {TypePrefix}", typePrefix);
var validationResult = _validationService.ValidateTypePrefix(typePrefix);
if (validationResult != ValidationResult.Success)
{
_logger.LogError("Identifier generation failed validation: {ValidationError}", validationResult.ErrorMessage);
throw new ArgumentException(validationResult.ErrorMessage, nameof(typePrefix));
}
var adjectives = _wordListProvider.GetAdjectives().ToArray();
var nouns = _wordListProvider.GetNouns().ToArray();
if (!adjectives.Any() || !nouns.Any())
{
_logger.LogError("Word lists are empty - cannot generate identifier");
throw new InvalidOperationException("Word lists cannot be empty");
}
await Task.Delay(1, cancellationToken);
var selectedAdjective = adjectives[_randomNumberGenerator.Next(adjectives.Length)];
var selectedNoun = nouns[_randomNumberGenerator.Next(nouns.Length)];
var generatedId = $"{typePrefix}-{selectedAdjective}-{selectedNoun}";
_logger.LogInformation("Successfully generated identifier: {GeneratedId}", generatedId);
activity?.SetTag("identifier.type_prefix", typePrefix);
activity?.SetTag("identifier.generated", generatedId);
return generatedId;
}
}
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddBalatroIdentifierGeneration(
this IServiceCollection services,
Action<IdentifierGenerationOptions>? configureOptions = null)
{
services.AddOptions<IdentifierGenerationOptions>()
.Configure(options =>
{
// add more stuff here as needed
options.Adjectives = new[]
{
"uncommon", "rare", "foil", "checkered", "orbital", "handy", "small", "big", "psychic", "verdant",
"violet", "crimson", "cerulean", "foolish", "grim", "wasteful" // ...
};
options.Nouns = new[]
{
"serpent", "pillar", "needle", "head", "hierophant", "hermit", "strength",
"temperance", "aura", "wraith", "sigil", "ouija", "ectoplasm", "immolate",
"ankh", "cryptid", "pluto", "uranus", "venus", "saturn", "jupiter", "earth",
"neptune", "ball", "globe", "telescope", "observatory", "money", "antimatter",
"trick", "illusion", "hieroglyph", "petroglyph", "retcon", "brush", "palette"
};
configureOptions?.Invoke(options);
})
.ValidateDataAnnotations();
services.AddSingleton<IRandomNumberGenerator, CryptographicRandomNumberGenerator>();
services.AddSingleton<IWordListProvider, ConfigurationBasedWordListProvider>();
services.AddScoped<IIdentifierValidationService, IdentifierValidationService>();
services.AddScoped<IIdentifierGenerationService, BalatroIdentifierGenerationService>();
return services;
}
}
}
// usage example:
// services.AddBalatroIdentifierGeneration();
// var idService = serviceProvider.GetRequiredService<IIdentifierGenerationService>();
// var id = await idService.GenerateIdentifierAsync("user");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment