여러 번의 올인(All-in)이 발생할 때 사이드 팟(Side Pots)을 올바르게 처리하는 방법
요약
포커 엔진 개발 시 발생하는 복잡한 사이드 팟(Side Pots) 처리 로직을 해결하기 위한 알고리즘 가이드를 제공합니다. 팟 생성과 핸드 평가를 분리하는 결정론적 다중 패스 알고리즘과 데이터 모델링 방식을 설명합니다.
핵심 포인트
- 팟 생성과 핸드 평가 로직의 분리 필요성
- 플레이어별 기여도를 추적하는 데이터 모델 설계
- 스택 정렬을 통한 2패스 알고리즘 구현
- 금전적 손실 및 규제 실패 방지를 위한 정확성 확보
사이드 팟(Side Pots)을 올바르게 처리하는 것은 포커 엔진에서 가장 중요한 로직 과제 중 하나입니다. 여기서 발생하는 오류는 직접적인 금전적 손실과 규제 실패로 이어지기 때문입니다. 이 문제의 해결책은 **팟 생성(Pot Creation)**과 **핸드 평가(Hand Evaluation)**를 분리하는 **결정론적 다중 패스 평가 알고리즘(Deterministic, multi-pass evaluation algorithm)**을 필요로 합니다.
핵심 원칙은 다음과 같습니다: 플레이어는 자신이 기여한 팟만 승리할 수 있습니다. 엔진은 베팅의 각 단계에서 발생하는 최소 유효 스택(Effective Stack)을 기준으로 테이블 위의 총 칩을 별도의 "팟(Pots)"(메인 팟, 사이드 팟 1, 사이드 팟 2 등)으로 논리적으로 분리해야 합니다.
1. 데이터 모델: 팟 분리 (Pot Segregation)
단일 totalPot 정수를 저장하지 마십시오. 대신, 다음 사항을 추적하는 Pot 객체 리스트를 유지해야 합니다:
id: 고유 식별자.eligiblePlayers: 이 팟을 두고 경쟁할 수 있는 플레이어 ID들의Set.amount: 팟에 있는 총 칩.contributions: 누가 정확히 얼마를 넣었는지 추적하기 위한{ playerId: amount }맵 (플레이어가 조기에 탈락했을 때의 환불이나 감사 추적(Audit trails)을 위해 매우 중요함).
상태 전이 로직 (State Transition Logic):
플레이어가 올인(All-In)을 하면, 엔진은 즉시 이 올인 금액이 현재의 highestBet(최고 베팅액)보다 적은지 확인해야 합니다. 만약 그렇다면, 사이드 팟 생성(Side Pot Creation) 이벤트를 트리거합니다.
2. 알고리즘: 다중 패스 사이드 팟 계산 (Multi-Pass Side Pot Calculation)
가장 견고한 구현 방식은 수학적 정확성을 보장하기 위해 2패스 알고리즘(Two-pass algorithm)(또는 분배를 지연시키는 단일 패스 방식)을 사용하는 것입니다.
패스 1: 팟 경계 결정 ("스택 정규화" 단계)
카드를 나누거나 핸드를 평가하기 전에, 엔진은 정확히 몇 개의 팟이 존재하는지와 각 팟의 크기를 계산해야 합니다.
- 플레이어를 스택별로 정렬 (Sort Players by Stack): 모든 활성 플레이어를 가져와
currentStack(현재 베팅 라운드 전 남은 칩)을 기준으로 정렬합니다. - 유효 스택 식별 (Identify Effective Stacks): 정렬된 플레이어들을 순회하며 "임계점 (breakpoints)"을 찾습니다.
- 예시: 플레이어 A (100), 플레이어 B (100), 플레이어 C (50), 플레이어 D (200).
- 첫 번째 "레이어 (layer)"는 가장 작은 스택(플레이어 C: 50)에 의해 제한됩니다. 모든 플레이어는 **메인 팟 (Main Pot)**에 50씩 기여합니다.
- 플레이어 C는 이제 메인 팟 레이어에 대해 "올인 (All-In)" 상태입니다.
- 남은 스택: A (50), B (50), D (150).
- 다음 "레이어"는 그다음으로 작은 스택(A/B: 50)에 의해 제한됩니다. A와 B는 **사이드 팟 1 (Side Pot 1)**에 50씩 기여합니다.
- 플레이어 D는 **사이드 팟 1 (Side Pot 1)**에 50을 기여합니다.
- 남은 스택: D (100).
- 사이드 팟 2 (Side Pot 2): D가 남은 100을 기여합니다. D만 자격이 있나요? 아니요, D가 유일하게 남았지만, 보통 이는 D가 다른 누군가를 상대로 레이즈(raise)하고 있음을 의미합니다.
- 수정: 로직은 다음과 같습니다:
- 메인 팟 (Main Pot): 모든 플레이어는
min(stack, current_bet)을 기여합니다. - 사이드 팟 1 (Side Pot 1):
stack > min_bet인 플레이어들이 그다음으로 낮은 스택까지의 차액을 기여합니다. - 사이드 팟 N (Side Pot N): 모든 칩이 할당될 때까지 반복합니다.
- 메인 팟 (Main Pot): 모든 플레이어는
팟 계산을 위한 의사코드 (Pseudocode for Pot Calculation):
function calculateSidePots(players: Player[], currentHighestBet: number): Pot[] {
// 1. 활성 플레이어 필터링 (폴드하지 않은 플레이어)
const activePlayers = players.filter(p => p.status === 'active' || p.status === 'all_in');
...
패스 2: 핸드 평가 및 분배 (Pass 2: Hand Evaluation & Distribution)
팟이 정의되면, 엔진은 각 팟에 대해 독립적으로 핸드를 평가합니다.
- 팟 반복 (Iterate Pots): 메인 팟 (Main Pot)부터 시작하여 사이드 1 (Side 1) 등으로 순차적으로 진행합니다.
- 참가 가능 플레이어 필터링 (Filter Eligible Players):
Pot X에 대해서는Pot X.eligiblePlayers에 포함된 플레이어들만 고려됩니다. - 평가 (Evaluate): 해당 플레이어들의 홀 카드 (Hole cards)와 커뮤니티 카드 (Community cards)를 사용하여 참가 가능 플레이어들에 대해
HandEvaluator를 실행합니다. - 배분 (Distribute):
Pot X에서 여러 플레이어가 공동 최고 핸드를 가질 경우, 팟을 분할 (Split)합니다.- 플레이어가 올인 (All-in)을 했으나 패배한 경우, 해당 플레이어는 이후 팟들의
eligiblePlayers집합에서 제외됩니다.
중요한 예외 케이스: "매칭되지 않은" 올인 (The "Unmatched" All-In)
플레이어 A (100)가 플레이어 B (150)의 올인에 콜 (Call)을 하고, 플레이어 B가 200으로 레이즈 (Raise)한 경우:
- 플레이어 A는 100을 투입합니다.
- 플레이어 B는 100 (매칭된 금액) + 50 (사이드 팟 금액)을 투입합니다.
- 메인 팟 (Main Pot): 200 (각각 100씩).
- 사이드 팟 (Side Pot): 0 (플레이어 A는 기여할 수 없음).
- 플레이어 B가 사이드 팟을 자동으로 승리하지만 (다른 누구도 경쟁할 수 없으므로), 엔진은 여전히 메인 팟 승자를 확인하기 위한 로직을 실행해야 합니다.
3. 아키텍처 및 데이터 흐름 (Architecture & Data Flow)
"팟 매니저" 마이크로서비스 (The "Pot Manager" Microservice)
대규모 아키텍처에서 팟 로직은 전용 서비스나 게임 엔진 (Game Engine) 내의 순수 함수 (Pure function)로 격리되어야 합니다.
- 입력 (Input):
List<Player>,CurrentHighestBet,BettingHistory. - 출력 (Output): 승자와 금액이 포함된
List<Pot>. - 상태 (State):
Pot객체는 이벤트 로그 (Event log)에 저장되는 불변 스냅샷 (Immutable snapshots)입니다.
데이터베이스 스키마 (Database Schema - PostgreSQL)
감사 가능성 (Auditability)을 위해 팟 구조를 JSONB 또는 정규화된 테이블 (Normalized tables)로 저장합니다.
CREATE TABLE game_pots (
id UUID PRIMARY KEY,
game_id UUID NOT NULL,
...
4. 확장성 및 성능 (Scalability & Performance)
- 알고리즘 복잡도 (Algorithmic Complexity): 정렬 및 레이어링 알고리즘은 $O(N \log N)$이며, 여기서 $N$은 플레이어의 수입니다. $N \le 10$ (보통 최대 9명 또는 10명)이므로, 이는 사실상 $O(1)$이며 마이크로초 단위로 실행됩니다.
- 동시성 (Concurrency): 팟 계산은 읽기 집약적인 작업(쇼다운 시 트리거됨)이지만 일관성을 유지해야 합니다. 이는 게임 상태의 순수 함수 (pure function)이므로, 메인 게임 루프를 잠그지 않고 별도의 워커 스레드 (worker thread)에서 실행할 수 있습니다.
- 메모리 (Memory): 가능하다면 매 계산마다 새로운 객체를 생성하지 마십시오. 고빈도 환경(예: 시간당 10만 핸드)에서 가비지 컬렉션 (GC) 압박을 줄이기 위해
Pot및Contribution객체에 대한 객체 풀 (object pool)을 재사용하십시오.
5. 보안 및 준수 (Security & Compliance)
- 감사 추적 (Audit Trail): 모든 사이드 팟 생성은 이벤트로 기록되어야 합니다:
SidePotCreated(potId, amount, eligiblePlayers). - 결정론 (Determinism): 알고리즘은 어떤 서버에서든 정확히 동일한 결과를 생성해야 합니다. 부동 소수점 연산 (floating-point math)을 사용하지 마십시오. 정수 산술 (integer arithmetic, 센트/최소 통화 단위)만을 독점적으로 사용하십시오.
- 환불 로직 (Refund Logic): 사이드 팟이 생성된 _후_에, 하지만 쇼다운(showdown)이 발생하기 _전_에 플레이어가 폴드(fold)한다면, 해당 플레이어는 단순히
eligiblePlayers집합에서 제거됩니다. 그들의 기여금(contribution)은 팟에 그대로 남습니다. 만약 플레이어가 올인(all-in)을 한 후 쇼다운 _전_에 폴드한다면 (포커에서는 불가능하지만 가설적으로), 로직은 환불을 올바르게 처리해야 합니다. (포커에서는 일단 올인을 하면, 폴드 여부와 관계없이 팟에 참여하게 됩니다).
실제 사례: 3인 올인 (Three-Way All-In)
- Player A: 500 칩 (chips)
- Player B: 300 칩 (chips)
- Player C: 150 칩 (chips)
- 현재 베팅 (Current Bet): 100 (모두 100씩 콜 (call)).
- 액션 (Action):
- C가 올인 (All-In) (150).
- A와 B가 콜 (Call) (각각 150).
- 메인 팟 (Main Pot): 450 (각각 150). 참여 자격: A, B, C.
- 남은 금액: A (350), B (150).
- B가 올인 (All-In) (150 추가).
- A가 콜 (Call) (150).
- 사이드 팟 1 (Side Pot 1): 300 (A로부터 150, B로부터 150). 참여 자격: A, B. (C는 자격 없음).
- 남은 금액: A (200).
- A가 올인 (All-In) (200).
- 사이드 팟 2 (Side Pot 2): 200 (A로부터 200). 참여 자격: A만? 아니오, A가 기여할 유일한 인원이지만, B와 C는 이미 올인 상태입니다.
- 수정 (Correction): 만약 A에게 200이 남고 B/C가 올인 상태라면, A는 그들을 상대로 레이즈 (raise)할 수 없습니다. A는 그냥 콜 (call)만 합니다.
- 시나리오 변경 (Scenario Change): A가 500으로 레이즈 (raise). B는 300 콜 (전부 올인). C는 150 콜 (전부 올인).
- 메인 (Main): 450 (각각 150).
- 사이드 1 (Side 1): 300 (A로부터 150, B로부터 150).
- 사이드 2 (Side 2): 200 (A로부터 200).
- 승자 (Winners):
- 메인 팟 (Main Pot): A, B, C 중 가장 높은 족보 (best hand).
- 사이드 1 (Side 1): A, B 중 가장 높은 족보 (best hand).
- 사이드 2 (Side 2): A (자동 승리, 다른 참여 자격자 없음).
구현 체크리스트 (Implementation Checklist)
- 스택 정규화 (Normalize Stacks): 총 베팅액이 아닌 남은 스택 (remaining stacks)을 기준으로 항상 작업하십시오.
- 계층화 (Layering): 가장 작은 스택에서 가장 큰 스택 순으로 팟을 생성하십시오.
- 참여 자격 (Eligibility): 플레이어가 기여하지 않은 팟을 가져갈 수 없도록 엄격하게 적용하십시오.
- 타이브레이킹 (Tie-Breaking): 각 계층 내에서의 스플릿 팟 (split pots)을 독립적으로 처리하십시오.
- 로깅 (Logging): 모든 팟 분배 이벤트를 기록하십시오.
::search[poker side pot algorithm implementation] {type=web}
::search[poker all-in logic edge cases programming] {type=web}
FAQ 명확화 (Clarifying FAQs)
Q1: 플레이어가 사이드 팟(Side Pot)에 기여한 유일한 사람이라면 그 팟을 가져갈 수 있나요?
네. 플레이어 A가 레이즈(Raise)를 하고, 플레이어 B가 폴드(Fold)했으며, 플레이어 C가 레이즈 금액보다 적은 금액으로 올인(All-in)을 했다면, 플레이어 A와 C는 메인 팟(Main Pot)을 두고 플레이합니다. 만약 플레이어 A가 다시 레이즈할 수 있는 충분한 칩을 가지고 있고 플레이어 B가 탈락한 상태라면, A가 추가로 투입하는 모든 칩은 A만이 자격을 갖는 "사이드 팟(Side Pot)"을 형성합니다. A는 이 팟을 자동으로 승리합니다. 엔진은 감사(Audit) 목적으로 해당 팟 객체(Pot object)를 반드시 생성해야 합니다.
Q2: 사이드 팟에서 두 명의 플레이어가 동일한 최고의 핸드(Best hand)로 비길 경우 어떻게 되나요?
사이드 팟은 비긴 플레이어들 사이에 균등하게 분할됩니다. (금액이 홀수인 경우) 남는 칩은 일반적으로 해당 핸드에서 가장 높은 카드를 가진 플레이어에게 지급되거나(또는 특정 게임 변형 규칙에 정의된 포지션 규칙에 따라), 타이브레이커(Tie-breaker)가 지정되지 않은 경우 단순히 딜러 버튼(Dealer button)의 왼쪽에 있는 플레이어에게 지급됩니다. 엔진은 정수 나눗셈(Integer division)과 나머지 분배(Remainder distribution)를 처리하는 "스플릿 팟(Split pot)" 함수를 구현해야 합니다.
Q3: 엔진은 폴드(Fold)하는 "올인(All-In)" 플레이어를 어떻게 처리하나요?
포커에서 올인한 플레이어는 폴드할 수 없습니다. 그들은 자신이 기여한 모든 팟에 대해 쇼다운(Showdown)이 일어날 때까지 핸드에 남아 있습니다. 유한 상태 머신(FSM, Finite State Machine)은 status == 'all_in'인 플레이어에 대해 "폴드(Fold)" 액션이 발생하는 것을 방지해야 합니다. 이들이 활성 베팅(Active betting)에서 벗어나는 유일한 방법은 팟을 잃거나 핸드가 종료되는 것뿐입니다.
Q4: 단 한 명의 플레이어만 있는 사이드 팟(Side Pot)이 가능한가요?
네, 하지만 그 플레이어가 유일하게 자격이 있는 경우에만 가능합니다. 이는 보통 한 플레이어가 레이즈(Raise)를 하고 다른 모든 플레이어가 폴드(Fold)했을 때, 레이저에게 남은 칩이 있는 경우에 발생합니다 (표준적인 핸드에서는 특정 토너먼트 규칙이 있거나, 공격자의 남은 스택이 다른 누구와도 대결하지 않고 사이드 팟을 생성하는 멀티웨이 올인(Multi-way all-in) 시나리오가 아닌 한 불가능합니다). 표준적인 3-way 올인 상황에서, 만약 B와 C가 올인(All-in) 상태이고 A가 추가로 레이즈를 했는데 B와 C가 콜(Call)할 수 없다면, 사이드 팟 2(Side Pot 2)에는 플레이어 A만 있을 수 있습니다. 잠시만요, B와 C가 올인 상태라면 그들은 콜을 할 수 없습니다. 따라서 A의 추가 칩은 오직 A만이 자격이 있는 팟으로 들어가게 됩니다. A가 이를 가져갑니다.
Q5: 사이드 팟 상황에서 "데드 버튼(Dead Button)" 또는 특수한 토너먼트 규칙을 어떻게 처리하나요?
사이드 팟 로직은 버튼(Button) 위치와는 독립적입니다. 버튼은 _액션의 순서(Order of action)_를 결정하지만, 사이드 팟 계산은 스택 크기(Stack sizes)에 기반한 순수하게 수학적인 과정입니다. "데드 버튼"(플레이어가 자리에 없지만 칩은 남아 있는 경우)은 eligiblePlayers 세트에 status != 'folded' 및 status != 'sitting_out'(만약 'sitting out'이 승리할 수 없음을 의미한다면)인 플레이어만 포함되도록 보장함으로써 처리됩니다. 엔진은 eligiblePlayers 필터 내에서 "sitting out" 플레이어에 관한 특정 토너먼트 규칙을 준수해야 합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기