| name | game-cycle |
| description | EGM game cycle initiation, detection, and completion. Use when starting a game, detecting game cycle state, choosing between SelfPlay and StartGame button, understanding game state flow, or waiting for game idle. Covers: GameCycleActive, InActivePlayMode, SelfPlayFacade vs ButtonsFacade StartGame, game state flow diagram, bet placement, reel spin, outcome evaluation. stages: test-implementation, test-validation |
Game Cycle
When to Use
- Starting a game cycle (spin)
- Detecting whether a game cycle is active or complete
- Choosing between
SelfPlayFacade and the StartGame button
- Understanding the EGM game state flow
- Waiting for the game to return to idle after a spin
Game State Flow
Idle / Attract → Credit Insertion → [Denom Selection Popup] → Active Play Mode → Game Cycle → [Feature Handling] → [Win Counting] → Active Play Mode → ...
Brackets indicate conditional states that only occur under specific circumstances.
Game Cycle Phases
- Bet placement — credits deducted based on selected bet level
- Reel spin — reels animate
- Outcome evaluation — win/loss determined
- Feature triggers — bonus, free games, re-spins may activate
- Win counting — winning amount counted up on display
- Return to idle —
GameCycleActive becomes false, game enters Active Play Mode
Initiating a Game Cycle
Option A: StartGame Button (Use When Bet Level Matters)
await buttonsFacade.EmulateButtonPressByNameAsync(
ButtonNames.GameplayButtons.StartGame);
Use this when the test needs to play at a specific bet level or denomination. The button respects the currently selected CustomBet setting.
Option B: SelfPlay (Use When Bet Level Doesn't Matter)
await selfPlayFacade.StartSingleGameAsync();
WARNING: SelfPlayFacade.StartSingleGameAsync() uses the SelfPlay UTP module's internal automation, which overrides the currently selected bet level with its own configured wager. Any preceding CustomBet button press is ignored.
Decision Rule
| Scenario | Use |
|---|
| Playing at a specific bet level | ButtonNames.GameplayButtons.StartGame via ButtonsFacade |
| Playing at a specific denomination | ButtonNames.GameplayButtons.StartGame via ButtonsFacade |
| Iterating bet/denom combinations | ButtonNames.GameplayButtons.StartGame via ButtonsFacade |
| Simple smoke test, bet doesn't matter | SelfPlayFacade.StartSingleGameAsync() |
| Attract mode exit test | SelfPlayFacade.StartSingleGameAsync() |
CRITICAL: Two-Phase Game Cycle (Start → Complete)
A game cycle is a two-phase transition: GameCycleActive goes false → true (game started) then true → false (game completed). Both phases MUST be verified.
Anti-Pattern Rule
A StartGame press without verification that GameCycleActive transitioned to true is testing nothing.
The while (GameCycleActive) completion loop exits immediately if the game never started — silently skipping the entire game. This is the #1 cause of tests that appear to pass while playing zero games. State verification for game initiation must never be removed to resolve test failures during automated or manual fix iterations. If the game fails to start, fix the preconditions, timing, or retry logic — do not strip the initiation check.
Detecting Game Cycle State
- Active:
StatusDatabasePaths.GameStatus.GameCycleActive is true
- Complete/Idle:
GameCycleActive is false
- In Active Play Mode:
StatusDatabasePaths.GameStatus.InActivePlayMode is true
var isActive = await gameStatusProvider.IsGameCycleActiveAsync();
var isIdle = await gameFlowStatusProvider.IsInBaseGameAsync();
Phase 1: Verify Game Initiated (Press → Poll → Re-Press)
After pressing StartGame, poll until GameCycleActive becomes true. If the press was swallowed (popup, animation, UI transition), re-press and retry. This mirrors the press → poll → re-press pattern used for bet and denom selection.
Canonical Code Template
private const int GameStartPollIntervalMs = 200;
private const int GameStartTimeoutMs = 5_000;
private const int GameStartMaxRetries = 3;
private async Task StartAndVerifyGameCycleAsync()
{
for (var attempt = 1; attempt <= GameStartMaxRetries; attempt++)
{
await buttonsFacade.EmulateButtonPressByNameAsync(
ButtonNames.GameplayButtons.StartGame);
var started = await WaitForConditionAsync(
async () => await gameStatusProvider.IsGameCycleActiveAsync(),
timeoutMs: GameStartTimeoutMs,
pollIntervalMs: GameStartPollIntervalMs);
if (started)
{
return;
}
await TestContext.Out.WriteLineAsync(
$"[Retry] StartGame attempt {attempt}: "
+ "GameCycleActive did not become true. Re-pressing...");
}
Assert.Fail(
$"Game cycle did not start after {GameStartMaxRetries} "
+ "StartGame presses. GameCycleActive remained false.");
}
Key Rules
- Poll
GameCycleActive in a bounded loop — do NOT use a fixed delay after the press
- If the poll times out, re-press the button (the first press was swallowed) — do NOT skip or weaken the check
- After exhausting retries, fail explicitly — do not silently continue to the next iteration
- Log every retry attempt via
TestContext.Out.WriteLineAsync for diagnostics
Phase 2: Wait for Game Completion
Once GameCycleActive is confirmed true, poll until it becomes false while servicing active features on each iteration:
private const int GameCompletionPollIntervalMs = 500;
private const int GameCompletionTimeoutMs = 120_000;
private async Task WaitForGameCompletionAsync()
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken);
cts.CancelAfter(GameCompletionTimeoutMs);
try
{
while (await gameStatusProvider.IsGameCycleActiveAsync())
{
await gameFeatureController.HandleAllActiveFeaturesAsync(CancellationToken);
await Task.Delay(GameCompletionPollIntervalMs, cts.Token);
}
await gameFeatureController.HandleAllActiveFeaturesAsync(CancellationToken);
}
catch (OperationCanceledException)
{
Assert.Fail($"Game cycle did not complete within {GameCompletionTimeoutMs}ms");
}
}
Complete Game Cycle Pattern (Both Phases)
The correct sequence for playing a single game is always:
await StartAndVerifyGameCycleAsync();
await gameSpeedController.IncreaseSpeedAsync();
await WaitForGameCompletionAsync();
await gameSpeedController.DecreaseSpeedAsync();
NEVER replace this with a bare StartGame press followed directly by WaitForGameCompletionAsync() — that skips Phase 1 verification.
## Preconditions for Starting a Game
- `InActivePlayMode` must be `true`
- No popup is blocking (e.g., denom selection popup must be dismissed)
- Credits must be sufficient for the current bet level
- No other game cycle is already active
## Common Pitfalls
| Pitfall | Consequence | Fix |
|---|---|---|
| `while (GameCycleActive)` without verifying it became `true` first | Loop exits immediately if game never started — zero games played | Use `StartAndVerifyGameCycleAsync()` (Phase 1) before the completion loop |
| Bare `StartGame` press + fixed delay instead of polling | Race condition — delay may be too short or too long | Poll `GameCycleActive` with bounded timeout |
| Removing the game-initiation check to fix a test failure | Test passes but plays no games — false positive | Fix preconditions/timing, never remove the verification |
| Using `SelfPlayFacade` when iterating bet/denom combos | SelfPlay overrides the selected bet level | Use `StartGame` button via `ButtonsFacade` |