Project · Unity companions
Unity collectathon rules companion
A complete console project testing unique collection, win/timeout terminal states, input bounds and a deterministic run story.
All C# source files below are loaded together in the playground. Files written by a run live in its virtual directory.
Program.cs
using System;
using System.Globalization;
using SharpRune.UnityRun;
Console.WriteLine(RunChecks.Run() + " run checks passed");
var story = new RunState(3, 5); story.Collect(1); story.Collect(1); story.Tick(1); story.Collect(2); story.Collect(3);
Console.WriteLine("Story: " + story.Score + "/" + story.Phase + "/" + story.Remaining.ToString("0.0", CultureInfo.InvariantCulture));
Domain/RunState.cs
using System;
using System.Collections.Generic;
namespace SharpRune.UnityRun
{
public enum RunPhase { Running, Won, Lost }
public sealed class RunState
{
private readonly HashSet<int> collected = new HashSet<int>();
private readonly int target;
public int Score { get; private set; }
public double Remaining { get; private set; }
public RunPhase Phase { get; private set; }
public RunState(int target, double seconds)
{
if (target < 1 || seconds <= 0 || double.IsNaN(seconds) || double.IsInfinity(seconds)) throw new ArgumentOutOfRangeException();
this.target = target; Remaining = seconds;
}
public bool Collect(int id)
{
if (Phase != RunPhase.Running || !collected.Add(id)) return false;
Score++; if (Score >= target) Phase = RunPhase.Won; return true;
}
public void Tick(double seconds)
{
if (seconds < 0 || double.IsNaN(seconds) || double.IsInfinity(seconds)) throw new ArgumentOutOfRangeException();
if (Phase != RunPhase.Running) return;
Remaining = Math.Max(0, Remaining - seconds); if (Remaining == 0) Phase = RunPhase.Lost;
}
}
}
Checks/RunChecks.cs
using System;
using SharpRune.UnityRun;
public static class RunChecks
{
public static int Run()
{
int passed = 0;
void Check(bool condition, string name) { if (!condition) throw new InvalidOperationException(name); Console.WriteLine("PASS " + name); passed++; }
var duplicate = new RunState(3, 10); bool first = duplicate.Collect(7); bool repeated = duplicate.Collect(7);
Check(first && !repeated && duplicate.Score == 1, "duplicate contact guarded");
var win = new RunState(2, 10); win.Collect(1); win.Collect(2); bool extra = win.Collect(3); win.Tick(1);
Check(win.Phase == RunPhase.Won && !extra && win.Score == 2 && win.Remaining == 10, "win is terminal");
var loss = new RunState(3, 2); loss.Tick(2);
Check(loss.Phase == RunPhase.Lost && !loss.Collect(1) && loss.Score == 0, "timeout is terminal");
bool rejected = false; try { new RunState(3, 2).Tick(-1); } catch (ArgumentOutOfRangeException) { rejected = true; }
Check(rejected, "negative time rejected");
rejected = false; try { new RunState(3, 2).Tick(double.NaN); } catch (ArgumentOutOfRangeException) { rejected = true; }
Check(rejected, "nonfinite time rejected");
var a = new RunState(3, 2); var b = new RunState(3, 2); a.Collect(1);
Check(a.Score == 1 && b.Score == 0, "runs are independent");
rejected = false; try { new RunState(0, 2); } catch (ArgumentOutOfRangeException) { rejected = true; }
Check(rejected, "invalid setup rejected");
return passed;
}
}
Companion.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
</Project>