{"record":{"id":"a9f34705e6f1840f","repo":"dotnet/orleans","slug":"can-t-join-game-once-its-over","errorCode":null,"errorMessage":"Can't join game once its over","messagePattern":"Can't join game once its over","errorType":"validation","errorClass":"ApplicationException","httpStatus":null,"severity":"error","filePath":"samples/TicTacToe/Grains/GameGrain.cs","lineNumber":50,"sourceCode":"    {\n        // make sure newly formed game is in correct state\n        _playerIds = new List<Guid>();\n        _moves = new List<GameMove>();\n        _indexNextPlayerToMove = -1;  // safety default - is set when game begins to 0 or 1\n        _board = new int[3, 3] { { -1, -1, -1 }, { -1, -1, -1 }, { -1, -1, -1 } };  // -1 is empty\n\n        _gameState = GameState.AwaitingPlayers;\n        _winnerId = Guid.Empty;\n        _loserId = Guid.Empty;\n\n        return base.OnActivateAsync(token);\n    }\n\n    // add a player into a game\n    public Task<GameState> AddPlayerToGame(Guid player)\n    {\n        // check if its ok to join this game\n        if (_gameState is GameState.Finished) throw new ApplicationException(\"Can't join game once its over\");\n        if (_gameState is GameState.InPlay) throw new ApplicationException(\"Can't join game once its in play\");\n\n        // add player\n        _playerIds.Add(player);\n\n        // check if the game is ready to play\n        if (_gameState is GameState.AwaitingPlayers && _playerIds.Count is 2)\n        {\n            // a new game is starting\n            _gameState = GameState.InPlay;\n            _indexNextPlayerToMove = Random.Shared.Next(0, 1);  // random as to who has the first move\n        }\n\n        // let user know if game is ready or not\n        return Task.FromResult(_gameState);\n    }\n\n    // make a move during the game","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/dotnet/orleans/blob/fca799fa70ecb6ad975224271703ca43221f58de/samples/TicTacToe/Grains/GameGrain.cs#L32-L68","documentation":"Thrown by TicTacToe GameGrain.AddPlayerToGame when the game's state is already Finished — you cannot add a player to a completed game. The grain is a state machine (AwaitingPlayers -> InPlay -> Finished) and this guards an illegal transition.","triggerScenarios":"A client calls AddPlayerToGame(playerId) on a game grain whose _gameState is GameState.Finished (a winner has been decided). Also thrown implicitly via the sibling check for InPlay with a different message.","commonSituations":"Joining a game id that already completed; a stale UI showing an old game as joinable; reusing a finished game grain id instead of creating a new one.","solutions":["Create or join a fresh game (new grain key) instead of reusing a Finished one.","Reset the grain (the OnActivateAsync path sets AwaitingPlayers) — but only a new activation/deactivation cycle does that, so prefer a new game id.","Have the client check the returned GameState before attempting to join."],"exampleFix":"// before\nvar state = await game.AddPlayerToGame(playerId); // throws if Finished\n\n// after: verify state, or start a new game\nvar state = await game.GetState();\nif (state == GameState.Finished) game = GrainFactory.GetGrain<IGameGrain>(Guid.NewGuid());\nawait game.AddPlayerToGame(playerId);","handlingStrategy":"validation","validationCode":"// Check the game state before joining\nvar state = await game.GetState();\nif (state == GameState.Finished || state == GameState.InPlay)\n    return; // do not call AddPlayerToGame","typeGuard":"null","tryCatchPattern":"try { await game.AddPlayerToGame(playerId); }\ncatch (ApplicationException ex) when (ex.Message.Contains(\"over\"))\n{ /* start a new game instead */ }","preventionTips":["Always start a new game grain id once a game is Finished.","Check returned GameState before attempting to join.","Track game lifecycle in the client to avoid joining completed games."],"tags":["game","state-machine","validation","sample"],"backgroundTag":null,"analyzedSha":"fca799fa70ecb6ad975224271703ca43221f58de","analyzedAt":"2026-08-13T19:55:57.938Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}