本文へ移動
cccskills
無料GitHub で公開

testable-game-logic

Use when starting any game task, before writing tests - find out which editors and runners exist on this machine, keep gameplay rules in engine-independent classes with an injected clock and seeded RNG, and report exactly what could not run.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md8.0 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Testable Game Logic

Overview

You run headless on the user's machine. A web game or a Bevy crate can always be built and tested; a Unity, Unreal or Godot editor may or may not be installed, licensed and at the right version. The answer is the same in every engine: gameplay rules live in plain classes that a plain runner can drive, and engine callbacks are thin adapters that feed them input, time and randomness.

Core principle: If a rule can only be tested by pressing Play, it is in the wrong class. And a test you wrote but could not run is reported as exactly that — never as a pass.

What can run here

Run these first, in one step, and keep the answers for the plan.

# Unity: the exact editor version the project pins
grep m_EditorVersion ProjectSettings/ProjectVersion.txt
ls "/Applications/Unity/Hub/Editor/" 2>/dev/null            # macOS
ls "$HOME/Unity/Hub/Editor/" 2>/dev/null                    # Linux
echo "$UNITY_PATH"
# macOS binary: /Applications/Unity/Hub/Editor/<ver>/Unity.app/Contents/MacOS/Unity
# Windows: C:\Program Files\Unity\Hub\Editor\<ver>\Editor\Unity.exe ; Linux: ~/Unity/Hub/Editor/<ver>/Editor/Unity

# Godot: binary and version must match config/features (major.minor); .NET projects need the .NET build
which godot godot4 2>/dev/null; godot --version 2>/dev/null
ls /Applications/Godot*.app/Contents/MacOS/ 2>/dev/null

# Unreal: EngineAssociation in the .uproject names the install
grep EngineAssociation *.uproject
ls "/Users/Shared/Epic Games/" 2>/dev/null                  # macOS; Windows: C:\Program Files\Epic Games\UE_5.x
# binary: <engine>/Engine/Binaries/<Mac|Win64|Linux>/UnrealEditor-Cmd

node -v; cargo --version; dotnet --version; python3 --version 2>/dev/null

Rules:

  • Use only the editor whose version matches the pin. A different Unity or Unreal version re-imports and re-serializes the project; a different Godot minor rewrites project.godot.
  • Never install or download an editor on your own initiative. Missing is a finding to report, not a problem to solve.
  • Unity batchmode needs an activated licence; a licensing error in the log is "could not run", not a test failure of yours.
  • First builds are slow (Unreal C++, Bevy, Unity import). Give run_terminal a long timeout, or run detached to /tmp/tt-<task key>/<step>.log and read the log.

The layering

Rules (plain C# / GDScript RefCounted / C++ struct / TS module / Rust fn)   ← unit tests, any runner
   ↑ called by
Engine adapter (MonoBehaviour / Node / Actor component / Phaser scene / Bevy system)  ← engine tests
   ↑ wired in
Scenes, prefabs, Blueprints, data assets                                     ← looked at (game-visual-self-review)

The adapter reads input and deltaTime, calls the rule, and applies the result to the engine (transform, animation, UI event). It contains no if about game rules.

Worked example (Unity, C#)

// ❌ rule welded to the engine: untestable without Play mode, reads global time
public class Dash : MonoBehaviour {
    float _lastDash;
    void Update() {
        if (Input.GetKeyDown(KeyCode.Space) && Time.time - _lastDash > 1.5f) {
            _lastDash = Time.time;
            GetComponent<Rigidbody>().AddForce(transform.forward * 20, ForceMode.Impulse);
        }
    }
}

// ✅ rule in a plain class (assembly with noEngineReferences), time passed in
public sealed class Cooldown {
    readonly float _duration; float _remaining;
    public Cooldown(float duration) => _duration = duration;
    public bool IsReady => _remaining <= 0f;
    public void Tick(float dt) => _remaining = MathF.Max(0f, _remaining - dt);
    public bool TryConsume() { if (!IsReady) return false; _remaining = _duration; return true; }
}

[Test] public void TryConsume_WhileCoolingDown_ReturnsFalse() {
    var c = new Cooldown(1.5f);
    Assert.That(c.TryConsume(), Is.True);
    c.Tick(1.0f);
    Assert.That(c.TryConsume(), Is.False);
    c.Tick(0.5f);
    Assert.That(c.TryConsume(), Is.True);
}

The adapter DashAbility : MonoBehaviour caches its Rigidbody in Awake, ticks the cooldown in Update with Time.deltaTime, and applies the impulse in FixedUpdate when an input flag is set — three lines with no rule in them.

When the editor is missing

EngineRuns without the editorHow
UnityRules in an asmdef with "noEngineReferences": trueA throwaway net8.0 NUnit project in /tmp/tt-<task key>/ that compiles those sources: <Compile Include="<repo>/Assets/Scripts/Core/**/*.cs" />, then dotnet test. Commit the EditMode test in the repo either way.
Godot (C#)Plain C# classes with no Godot namespaceA class-library project referenced by an xUnit/NUnit project, dotnet test
Godot (GDScript)Nothing — GDScript needs the Godot binaryWrite the gdUnit4/GUT test anyway and report it as not run
UnrealNothing — even pure C++ in a module needs the engine buildWrite the Automation Spec anyway and report it as not run
Web, Bevy, MonoGame, pygameEverythingVitest, cargo test, dotnet test, pytest

If the repository already has such a plain-runner project (a Tests.Core.csproj, a logic/ crate), use it instead of a throwaway one.

Time, randomness and input are parameters

  • Time: rules take dt (or an IClock), never read Time.time, OS.get_ticks_msec(), performance.now() or Instant::now() themselves.
  • Randomness: one seeded generator passed in — new System.Random(seed), RandomNumberGenerator with .seed, a mulberry32(seed) in TS, ChaCha8Rng::seed_from_u64(seed) in Rust. A test fixes the seed; the game seeds from the clock or the save.
  • Input: rules receive intents (MoveIntent(x, y), Jump), not key codes. Rebinding then never touches a rule.

Tests that games specifically need

  • Two-delta test for anything time-based: run 1 s as 60×(1/60) and as 30×(1/30); assert the same result within tolerance (frame-rate-independence).
  • Determinism test where replays, lockstep or rollback exist: same seed + same input log → same state hash after N steps.
  • Table-driven transition test for every state machine (game-state-machines-and-ai).
  • Data invariant test over every shipped data asset: no zero-weight drop table, no negative damage, every referenced ID exists (data-driven-gameplay-design).
  • Regression test for every bug, watched failing before the fix.
  • Performance test with a threshold that fails when exceeded, for a hot path you changed (game-performance-budget).

Naming follows the repository; without a convention: NUnit Method_Scenario_Expected, gdUnit4/GUT test_<scenario>_<expected>, Unreal Project.System.Scenario, Vitest/cargo test a sentence describing the behaviour.

Reporting what could not run

One line per command in the closing message, verbatim:

NOT RUN: Unity 6000.3.2f1 not installed (Hub has 6000.0.40f1 only) — EditMode tests in Assets/Tests/EditMode/CooldownTests.cs written, not executed.
RAN: dotnet test /tmp/tt-T-42/core-tests → 12 passed, 0 failed (Core sources compiled without UnityEngine).

Common Mistakes

  • Testing a rule through a MonoBehaviour, Node or Actor because "that's where the code was".
  • Reading global time or Random inside a rule, then fighting flaky tests.
  • Running the tests in a mismatched editor version and committing the re-import churn.
  • Calling a run "verified" when the headless editor exited on a licence error.

Red Flags

  • A rule class that imports UnityEngine, Godot, Phaser or bevy::prelude without needing it.
  • A closing message with no RAN:/NOT RUN: accounting on an engine project.
  • A test that only passes at one frame rate.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月10日 更新

Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月10日 更新

Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月10日 更新

Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月10日 更新

How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月10日 更新

Use when deciding whether a request needs an analiz task before implementation - the conditions that require the architect's analysis versus going straight to implementation

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月10日 更新

makifbaysal のスキルをすべて見る

このスキルの問題を報告する