Sudden Death

When both players win at the same time and neither wins in more ways, the rules call for a tiebreaker game (Sudden Death). TCG ONE has no Sudden Death today: such a game ends as a draw. This topic collects what we know so far, as a brief for implementing it. Details on how it should work will be added later.

1. What exists today: simultaneous wins (commits 0483abd98, a35fc10f8, 2026-10-24)

Reports: forum 15266, twins 10877, 12601, 5996.

Before: simultaneous Knock Outs were handled one Pokémon at a time, and the game ended at the first player who took their last Prize card (or had no Pokémon left). The other Knock Out never gave its Prize card.

0483abd98 (engine)

  • engine/src/main/java/tcgwars/logic/GameManager.java
    • win(PlayerType, WinCondition): every “last Prize card” (NOPRIZE) and “no Pokémon left” (NOPOKEMON) win now goes through here instead of endGame. Outside of a resolution window it calls endGame at once.
    • resolvingSimultaneousWins(Runnable): while it runs, wins are only collected per player (EnumSet of WinCondition). Afterwards: whoever wins in more ways wins (with a log line naming both players’ win conditions). An equal number of ways is a draw: endGame(null, ...), with the log line “Both players won at the same time: … Sudden Death isn’t available, so the game is a draw.” It can be nested (an inner call just runs the effects).
  • engine/src/main/java/tcgwars/logic/effect/basic/CheckFaint.java: each round of Knock Outs (checkAll) runs inside resolvingSimultaneousWins, so all Knock Outs and their Prize cards resolve first.
  • engine/src/main/java/tcgwars/logic/effect/basic/Knockout.java: no Benched Pokémon → win(..., NOPOKEMON); takePrizes stops once the taker has no Prize cards left (instead of when the game ended).
  • engine/src/main/java/tcgwars/logic/effect/basic/TakePrize.java: the last Prize card → win(..., NOPRIZE); checkPrizes checks both players inside one resolvingSimultaneousWins.
  • Attacks that run two new Knockout(...) themselves wrap them in bg.gm().resolvingSimultaneousWins { ... }:
    • Beedrill TEU 5, Destiny Stinger (engine/src/main/java/tcgwars/logic/impl/gen7/TeamUp.groovy)
    • Annihilape SSP 100, Destined Fight (engine/src/main/java/tcgwars/logic/impl/gen9/SurgingSparks.groovy)
  • Tests in engine/src/test/resources/tcgwars/logic/test/ng/test_cases.groovy. The rule is also written down in .cursor/rules/bug-triage.mdc (Engine facts, “Both players win at the same time”).

a35fc10f8 (korstark): korstark/src/main/java/net/tcgone/korstark/service/CareerService.java, the KANTO_CIRCUIT achievement check threw a NullPointerException on a draw (gameLog.getWinnerId() is null). That failed the post-game payload, which is retried and runs calculateScore again on every retry. Now it is user.getId().equals(gameLog.getWinnerId()).

2. What the rules say

Current rulebook (docs/rules/pokemon-tcg-rulebook.md, the September 2026 web rulebook, p. 21):

  • “What if both players win at the same time?”: you win by taking your last Prize card, or when your opponent has no Benched Pokémon to replace their Knocked Out (or removed) Active Pokémon. “If this happens, play a tiebreaker game. However, if you win in both ways and your opponent wins in only one way, you are the victor!”
  • “What’s a tiebreaker game?”: “Both players set up as though they were playing a normal game, including setting up 6 Prize cards and flipping a coin to decide who goes first. The first player to take a Prize card (or to otherwise have fewer Prize cards remaining than their opponent) wins. The other ways to win still apply, but mainly it’s a race to see who can take the first Prize card!”

Older rulings: Sudden Death with 1 Prize card (Compendium, compendium.pokegym.net):

  • Plasma Freeze FAQ, May 16, 2013 (TPCi): “the player with the most win conditions met wins the game. However, if both players have the same number of win conditions, the game is declared a Draw and a one-prize Sudden Death game takes place to determine the winner.”
  • HS Triumphant FAQ, Nov 4, 2010 (PUI) and Nov 2, 2017 (TPCi), about Palkia & Dialga LEGEND’s Time Control and Nihilego-GX’s Symbiont GX adding Prize cards during Sudden Death: “A Sudden Death match is complete when a player has taken all of their prizes, regardless of whether there is more than one or not.”
  • May 3, 2018 (TPCi): “both players win at same time. Play a sudden death game to decide the outcome.”
  • HS Undaunted FAQ, Sep 9, 2010 (Drifblim’s Take Away): if neither player has Benched Pokémon, “it is a tie and a game of Sudden Death must be played.”
  • EX era (PUI): Mar 3, 2005 (Rocket’s Hitmonchan-EX’s Strikes Back gives both players their last Prize card → Sudden Death); Mar 10, 2005 (Electrode HL’s Mass Destruction: last Prize card vs. no Bench → Sudden Death; two win conditions vs. one → the Electrode player wins).
  • WotC era: Fossil FAQ (Selfdestruct Magneton, both take their last Prize card): the player who can still replace their Active Pokémon wins, “Otherwise, you play Sudden Death”, citing the rulebook’s Expert Rules “What Happens if Both Players Win at the Same Time?”. WotC Chat Sep 13, 2001 Q30: last Prize card vs. no Bench → “Neither, it goes to Sudden Death”. The Compendium doesn’t quote the WotC rulebook’s Prize count for Sudden Death; check a scan of the Base Set rulebook before relying on it.

So the eras differ:

Era Tiebreaker Win condition
WotC to at least 2018 (per the rulings above) Sudden Death, 1 Prize card each take all your Prize cards (the 1, or more if a card added some), or the other ways to win
Current rulebook (2026) Tiebreaker game, 6 Prize cards each first player to take a Prize card (or have fewer left than the opponent), or the other ways to win
All eras more win conditions than the opponent wins outright; equal → tiebreaker

Open: which rule each format/RuleSet should use, and when the rulebook switched from 1 Prize to “6 Prizes, first Prize wins” (check older rulebook PDFs).

3. What exists today for draws

  • Engine: endGame(null, winCondition) in GameManager logs “Game ends with a draw.” and queues the game-end animation with winnerName = "Draw" (TargetPlayer.DEFAULT). There is no DRAW value in WinCondition (common-rpc/src/main/java/tcgwars/logic/WinCondition.java); a simultaneous-win draw uses the shared win condition or OTHER.
  • Play client: common-ui/src/main/resources/tcgwars/common/ui/game/game_end.js shows “Draw!”; the lobby’s game panel (ui/src/main/java/tcgwars/ui/lobby/UGamePanel.java) shows “Winner: Draw”.
  • core: GameService.processGameEndedEvent (core/src/main/java/tcgwars/service/engine/GameService.groovy) saves the GameLog with winnerId null and pushes the post-game payload; the results channel message is skipped when there is no winner.
  • korstark skips draws for: the three-win bonus (CareerService.checkThreeWinBonus), the KANTO_CIRCUIT and DECKOUT achievements, the WinGames quest (model/quest/WinGames.java), career campaign progress (CareerCampaignService, line ~103), the ranked season rating (ScoreService.saveRating only runs with a winner and not on GAME_ERROR), and the winner-only parts of the score in ScoreService.calculateScore.
  • Bug (open follow-up): the Elo update in ScoreService.calculateScore (~line 470) gives each player Result.Win only if they are the winner, otherwise Result.Loss. So any game with no winner (a draw or GAME_ERROR) is an Elo loss for both players. EloRatingCalculator.Result.Draw (0.5) exists but is unused. The ranked season’s Glicko-2 code also has an unused RatingPeriodResults.addDraw (korstark/src/main/java/org/goochjs/glicko2/RatingPeriodResults.java).

4. Where draws (or simultaneous wins) come from

  • Simultaneous wins (section 1): any CheckFaint round, e.g. damage to both Active Pokémon, recoil, Poison/Burn in Pokémon Checkup, and the double-KO attacks.
  • Both players out of game time with no extension left: BetweenTurns.handleGameTimers (engine/src/main/java/tcgwars/logic/effect/basic/BetweenTurns.java) ends the game as a draw, endGame(null, TIMEOUT). The rulebook has no Sudden Death for this; this is our own timer.
  • Epic Splash environment effect: both players have all 5 Eeveelutions → endGame(null, EEVEELUTION) (engine/src/main/java/tcgwars/logic/effect/environment/EpicSplashEffect.java).
  • Engine error: endGame(null, GAME_ERROR) (GameManager, util/Future.java). Not a real draw, but korstark treats it like one for Elo (see the bug above).
  • Cards that Knock Out both Active Pokémon themselves:
    • Beedrill TEU 5, Destiny Stinger: wrapped (0483abd98).
    • Annihilape SSP 100, Destined Fight: wrapped (0483abd98).
    • Forretress LHB 45, Double KO (engine/src/main/contrib/src/tcgwars/logic/impl/gen8/LegendaryHeartbeat.groovy, ~line 1041, in the contrib submodule): not wrapped yet; its two new Knockout(...) calls in afterDamage still end the game at the first win.
  • Not audited: other attacks that call new Knockout(self).run(bg) directly while the attack also Knocks Out the Defending Pokémon (git grep new Knockout(self) lists about 20, in gen1, gen4 contrib, gen5 to gen9), and the direct endGame(..., NOPOKEMON) calls in TcgStatics.removePCS and Jungle.java. Outside a resolvingSimultaneousWins window those end the game before the other side’s win is counted.

5. What implementing Sudden Death would need (open list, not a design)

Engine / game setup

  • Instead of endGame(null, ...) on equal win counts, end the game with a result that says “go to Sudden Death” (a new WinCondition or flag in GameEndedEvent), so core can start the tiebreaker.
  • A new game with the same two decks, same format/RuleSet/environment effects/custom format settings, new coin flip for who goes first. Battleground.startingPrizeCount already comes from CreateBattlegroundRequest.starting_prize_count (common-rpc/src/main/proto/updater.proto, set in GameService.groovy ~line 719, also used by custom formats), so a 1-Prize game is mostly a parameter.
  • If the current rulebook’s variant is wanted for modern formats: 6 Prizes and “first player to take a Prize card (or have fewer Prizes left) wins”, which needs a new win check (not a parameter).
  • Prize-adding effects during Sudden Death (Time Control, Symbiont GX): per the rulings the player must take all Prize cards, which the 1-Prize setup gives for free.
  • Can Sudden Death itself end in a tie? (Another Sudden Death, or a draw after N tries?)
  • Timers: does Sudden Death get fresh game time, and what if both time out in it?
  • Fix or wrap the remaining double-KO paths in section 4 first, so Sudden Death triggers wherever it should.

core / lobby

  • Link the two games (e.g. a suddenDeathOf game id on Game/GameLog), so the result counts once.
  • What if a player leaves, logs out or surrenders between the games, or refuses the Sudden Death game?
  • Which game types offer it: ranked/casual/career/campaign/tournament/solo (vs AI) — or a draw where Sudden Death makes no sense?
  • Spectators and the replay/game log for the pair of games.

korstark (career, ranked, quests)

  • Count the pair as one game with the Sudden Death winner, or as a draw plus a game? This decides Elo, the ranked season rating, win streaks/three-win bonus, quests (WinGames, play-N-games quests), achievements (KANTO_CIRCUIT, deck-out ones, POWER_OF_ONE), campaign progress, coins/ink/bits from calculateScore, and the win count ScoreService reads from GameLog.winnerId.
  • Fix the Elo-loss-on-draw bug (section 3) either way, using Result.Draw / addDraw for draws that stay draws (timeouts, Epic Splash) and skipping GAME_ERROR.

UI (play client ui/common-ui, front3)

  • A game-end screen for “Both players won: Sudden Death!” and a hand-off into the new game (desktop and touch layouts).
  • Showing a Sudden Death game as such: label, 1 Prize card, and the result in the lobby’s game panel and in front3’s game history.

@CoconutLaCroix I think you have a list of rules how Sudden Death should work, could you share them here?