OneAbove.Arena.GameContract 4.4.0

dotnet add package OneAbove.Arena.GameContract --version 4.4.0
                    
NuGet\Install-Package OneAbove.Arena.GameContract -Version 4.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="OneAbove.Arena.GameContract" Version="4.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OneAbove.Arena.GameContract" Version="4.4.0" />
                    
Directory.Packages.props
<PackageReference Include="OneAbove.Arena.GameContract" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add OneAbove.Arena.GameContract --version 4.4.0
                    
#r "nuget: OneAbove.Arena.GameContract, 4.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package OneAbove.Arena.GameContract@4.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=OneAbove.Arena.GameContract&version=4.4.0
                    
Install as a Cake Addin
#tool nuget:?package=OneAbove.Arena.GameContract&version=4.4.0
                    
Install as a Cake Tool

OneAbove.Arena.GameContract

The game contract of the One-Above Arena. Whoever builds a game for the arena needs this package — and nothing else from the platform.

The three sentences everything follows from

  1. The server decides. Your rule module says what is allowed. The renderer in the browser never does.
  2. Your rule module is pure. Same input, same output — today and in two years. No DateTime.Now, no Random.Shared, no file, no network. Randomness solely from the seed in the state.
  3. Every seat sees only its own view. What GetView(state, seat) does not output does not exist for that player. That is a security boundary, not a display question.

The smallest thing a game is

public sealed class MyGame : IGameEngine<MyState, MyMove, MyView>
{
    public GameDescriptor Descriptor { get; } = new()
    {
        Id = GameId.Parse("my-game"),
        Version = new GameVersion(1, 0, 0),
        DisplayName = LocalizedText.De("Mein Spiel"),
        Spectators = true,
        OpenInformation = true,
        Chrome = GameChrome.Platform,
        Offerings =
        [
            new GameOffering
            {
                Key = "online",
                Name = LocalizedText.De("Online"),
                Reach = OfferingReach.Remote,
                SeatCounts = [2],
                AllowedSeatKinds = SeatKinds.Human,
                Clocks = [ArenaClock.PerTurn.Minutes5],
                OnTimeout = TimeoutPolicy.Resign,
                Result = new ResultShape { Kind = ResultKind.Ranking },
            },
        ],
    };

    public MyState CreateInitialState(MatchSetup setup) => …;
    public IReadOnlyList<Seat> GetSeatsToAct(MyState s) => …;
    public IReadOnlyList<LegalMove<MyMove>> GetLegalMoves(MyState s, Seat seat) => …;
    public MoveResult<MyState> ApplyMove(MyState s, Seat seat, MyMove move) => …;
    public GameView<MyView> GetView(MyState s, Seat? seat) => …;  // seat == null ⇒ spectator
    public MatchOutcome? GetOutcome(MyState s) => …;
    public LegalMove<MyMove>? GetForcedMove(MyState s, Seat seat) => …;
}

What a game declares is per offering, not per game: seat counts, clocks, options, what happens on a timeout. „2 or 4 seats, not 3" is a set, and two numbers could not express it.

What your rule module does NOT do

  • Check whether the caller may have the seat or is to act. The platform does that before it calls in. Your module believes the seat it is told.
  • Throw because a client sent nonsense. Every conceivable move gets an answer — accepted or MoveResult.Rejected. An exception is a system error, not a rejected move.
  • Reference anything other than this package. No access to a database, clock, network or another game.

Pitfalls that cost time

Computed properties do not belong in the state. A property like IsOver => … is serialised along and devalues every recorded match as soon as somebody changes its derivation. [JsonIgnore] on every derived property.

Dictionary order, double arithmetic, LINQ without a stable sort. The three places at which a replay falls apart on you in a year's time. For money and points use decimal, not double.

GetSeatsToAct returns a list. In turn-based games it is exactly one seat long. Several seats mean simultaneous moves — hidden bids, programming phases.

Seat colours, title image and manual (contract 4.4)

Colours

An offering may say which colour belongs to which seat — SeatColours[i] belongs to seat i. It is optional: Minesweeper has one seat, and a game that colours by piece rather than by seat should leave it empty. Empty means „not my concept", not „incomplete".

SeatColours =
[
    new SeatColour { Key = "gelb", Name = LocalizedText.De("Gelb"), Hex = "#e8c547" },
    new SeatColour { Key = "rot",  Name = LocalizedText.De("Rot"),  Hex = "#c2452d" },
]

⚠️ The colour also reaches your renderer (ArenaSeat.colour, arena-client 3.2.0), and that is the reason the field exists at all rather than living in your CSS: without it there would be two copies of the same hex value — one in the rule module, one in the renderer — and the arena would claim a colour your board could contradict. A wrong statement is worse than none. Whoever draws a seat colour reads it from there.

What is refused, and every line of it fail-closed, because the declaration comes out of a foreign repository and the surface writes the value as an inline style:

Rule Why
Hex exactly #rrggbb, lower case Upper case would make #E8C547 and #e8c547 two colours for the duplicate check and one for the eye
Contrast ≥ 3:1 against both of the arena's reference grounds Measured: real palettes sit at 4.7–7.8. #017E97 — out of the arena's own colour list — reaches 2.02 and would be an invisible dot
Pairwise distance ≥ 0.08 (Oklab) Measured: real pairs 0.138 and 0.255; two near-identical yellows 0.008. Twelve colours of which two look alike are eleven
Key unique, Hex unique Two seats in the same colour is the reported bug, word for word, back again
Exactly SeatCounts.Max() of them, or none ⚠️ SeatCounts is a set. Three colours leave three seats colourless at six, and „the order is the mapping" then fails silently

💡 The colour's name never travels in a match view. It is resolved once per lobby load in the asker's language; the seat DTOs carry key and hex only. A LocalizedText in a push has no recipient to be resolved against.

Media

Optional, next to your renderer and your golden matches:

media/
  cover/<locale>.webp      title image, square (1024² recommended)  ≤ 400 KB
  icon/<locale>.webp       icon, square (256² recommended)          ≤  64 KB
  manual/<locale>.json     a ManualDocument                         ≤  64 KB
  manual/img/<name>.webp   the manual's pictures                    ≤ 200 KB

4 MB in total, at most 20 manual pictures. .webp, .png, .jpg, .jpegno SVG: an <img src="…svg"> runs no script today, but that exception holds only for as long as nobody turns it into an inline SVG, and that is a promise about future code.

⚠️ The languages are derived from the file names, not declared. The truth is which files you shipped. A declared language set would be a second truth about your own directory — and a declared language tag would be a foreign string the platform builds a path out of.

⚠️ The measurements are a recommendation, the byte caps are caps. Checking the dimensions would need an image decoder over foreign bytes, and that is an attack surface of its own for a statement that protects nobody.

A manual is a ManualDocument with a Format version and exactly two chapter levels — as a type, not as a check: ManualSubchapter has no Chapters, so your compiler refuses a third level. Blocks: text (with strong/em runs), list, table, image, callout. An image's Src is checked against what you actually shipped.

⚠️ Refused, not filtered. An unknown block kind, a control character, a bidi override, a Src outside your inventory: the whole bundle is refused with a sentence. A manual that silently swallows a block looks complete and is not — and you would pay a version number to find out.

GameEngineContract runs the very same check your admission will run, so a refused manual costs you a test run and not a v tag.

Fullscreen (contract 4.2)

Chrome = GameChrome.Either lets the player take the whole screen (Game always does). Then FullscreenSidebar says what the arena keeps drawing beside your board: Open — its side bar with players, log, chat and rules, you draw only the board; Collapsed (the default) — only the stub in the corner, you draw your own surface and ask for the rest over the bridge (you, log, chat; undo(), resign(), say(), open()). Keep the top-right corner free (chrome.reserved), and use no <form> — the iframe is sandboxed without allow-forms.

Taking back (contract 4.1)

Undo is a move, not a deletion: the platform appends the rolled-back state as a further move and keeps the one that was taken back. A game opts in per offering (Undo = UndoAvailability.OptionalOff, an optional UndoBudgetPerTurn) and implements IUndoableGame<TState>. Whether the table chose it arrives in MatchSetup.UndoEnabled — and that is the point: a game may shape its turns differently then. Connect Four, with undo on, makes a drop tentative (MoveResult.AcceptedWithinTurn) and adds a confirming move; only between the two is there something to take back. A refusal from Undo is the normal answer, not a fault.

What belongs with it

Package What for
OneAbove.Arena.GameContract.Testing The check your module has to pass
OneAbove.Arena.GameHost.Dev The test table: dotnet run, two browser windows
@one-above/arena-client (npm) The bridge between renderer and platform
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on OneAbove.Arena.GameContract:

Package Downloads
OneAbove.Arena.GameContract.Testing

The One-Above Arena's contract suite: purity, determinism, totality and leak checks for every rule module — plus the mechanics for golden matches.

OneAbove.Arena.GameHost.Dev

The One-Above Arena's test table: a rule module and its renderer, playable with `dotnet run` — without a database, without a sign-in, without the platform.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.4.0 0 9/18/2026
4.4.0-rc.1 0 9/18/2026
4.3.0 53 9/17/2026
4.3.0-rc.2 27 9/17/2026
4.3.0-rc.1 33 9/17/2026
4.2.0 62 9/17/2026
4.2.0-rc.1 37 9/17/2026
4.1.1 64 9/17/2026
4.1.1-rc.1 40 9/17/2026
4.1.0 59 9/17/2026
4.1.0-rc.3 38 9/17/2026
4.1.0-rc.2 37 9/17/2026
4.1.0-rc.1 35 9/17/2026
4.0.0 81 9/16/2026
4.0.0-rc.5 80 9/16/2026
4.0.0-rc.4 76 9/15/2026
4.0.0-rc.3 47 9/15/2026
4.0.0-rc.2 50 9/15/2026
4.0.0-rc.1 51 9/15/2026
3.0.1 99 9/14/2026
Loading failed