10. 파티 전리품 분배 + 기여도 랭킹 갱신
난이도 최상해설 — 파티 전리품 분배 + 기여도 랭킹 갱신
난이도: 최상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
이 코드는 세 개의 공유 가변 상태(전리품 풀, 굴림 목록, 랭킹 점수) 를 어떤 락도
없이 동시 수정한다. 그 결과 (1) 같은 전리품이 두 명에게 가는 복제 분배, (2)
굴림 제출/비교의 레이스로 승자 오선정/누락, (3) 랭킹 점수의 고전적 lost update,
(4) 클라가 보낸 굴림값을 검증 없이 신뢰하는 치팅, (5) 여러 Dictionary/List
동시 수정으로 인한 자료구조 손상이 한꺼번에 터진다. 동시성·정합성·치팅이 입체적으로
얽힌 최상급 문제다. 핵심은 "분배(승자선정+지급+풀제거+점수가산)는 하나의 직렬화된
트랜잭션이어야 한다" 와 "락 객체가 아예 없다는 점" 을 동시에 보는 것이다.
문제점
(G)+(I)+(L) 분배 전체가 무락 비원자 — 복제 분배 + lost update (정확성/동시성) ★간판
- 증상: 같은 전리품이 두 명에게 분배(복제)되고, 랭킹 점수는 한 번만 가산되거나 엉뚱하게 덮어쓰인다.
- 재현 조건:
- 같은
lootId에 대한Distribute가 두 스레드에서 동시 호출(분배 트리거 중복, 재전송). 둘 다 (G)에서loot.Assigned==false를 관측 → 둘 다 (I)에서 서로 다른 "최고 굴림자" 에게 지급(또는 같은 사람에게 두 번) → 둘 다Assigned=true. 한 아이템이 두 번 지급된다(복제). - 랭킹: 두 분배(또는 다른 길드 분배)가 동시에 (K)에서 같은
GuildScore[g]값을 읽고 각자cur + value를 (L)에서 써, 한쪽 가산이 통째로 유실(lost update).
- 같은
- 근본 원인: "이미 분배됐는가" 검사(G)와 "분배 확정"(Assigned=true) 사이, 그리고 점수 read(K)-modify-write(L) 사이가 원자적이지 않다. 더 근본적으로 이 클래스에는 공유 상태를 보호하는 락 객체가 존재하지 않는다. 분배는 승자선정→지급→풀제거→ 점수가산이 하나의 임계 구역이어야 하는데 전부 노출돼 있다.
(D)+(E) 굴림 제출 레이스 — 목록 손상 + 승자 누락 (정확성/동시성)
- 증상: 여러 파티원이 동시에 굴림을 제출하면
List동시Add로 항목 유실/예외, 또는 (D)에서 두 스레드가 모두 새 리스트를 만들어 한쪽 제출이 사라진다. - 재현 조건:
SubmitRoll동시 호출. (D)TryGetValue가 둘 다 실패 → 둘 다 새List를_rolls[lootId]에 대입 → 먼저 넣은 리스트의 굴림이 통째로 버려진다. 또는 같은List에 동시Add→ 내부 배열 resize 중 손상. - 근본 원인:
Dictionary/List가 스레드 안전하지 않은데 락 없이 동시 변경. get-or-create 패턴이 원자적이지 않다.
(H)+(B) 굴림 비교가 진행 중인데 분배 시작 — race + 클라 신뢰 (정확성/보안)
- 증상: 아직 모든 파티원의 굴림이 안 들어왔는데
Distribute가 돌면 불완전한 집합에서 승자 선정. 또winner가 동률일 때 tie-break 규칙이 없어 비결정적. - 클라 신뢰 결함(★치팅):
LootRoll.Roll은 클라가 보낸 값이다. 어뷰저가roll=int.MaxValue를 보내면 항상 승리. 굴림은 서버 권위 영역인데 클라가 정한다. - 근본 원인: 굴림 수집 완료 조건(모든 파티원 제출/타임아웃)과 분배 시점이 동기화되지 않고, 굴림값 검증/서버 생성이 없다.
(C)+(L) 랭킹 GuildScore 비보호 — lost update + 손상 (정확성/동시성)
- 위 (L)의 lost update 외에도,
GuildScore(Dictionary)를 동시 쓰기 하면 rehash 중 손상/크래시. 랭킹은 서버 전역 공유 상태라 모든 분배가 여기로 몰려 최대 경합 지점.
(F)+(J) 풀 접근/제거 비보호 + 예외 (정확성/동시성)
_pools[bossId].First(...)는 없으면 예외(InvalidOperationException). (J)RemoveAll과 (F)First/다른 분배의 순회가 동시에 일어나면 컬렉션 변경 예외/손상.
(A) Assigned 플래그만으로 멱등성 — 영속/크래시 취약 (정확성)
- 인메모리 플래그라 지급(I) 후 (J)/(L) 전에 크래시하면 재처리 시 재분배 가능. 분배 전체가 복구 가능한 단일 트랜잭션이 아니다.
수정안
핵심: ① 보스(또는 loot) 단위 락을 도입해 굴림 수집과 분배를 직렬화, ② 분배 전체 (승자선정→지급→풀제거→점수가산)를 하나의 임계 구역으로, ③ 랭킹은 별도 락(또는 원자 연산)으로 보호하되 락 순서를 고정(loot 락 → board 락)해 데드락 회피, ④ 굴림은 서버 권위로 생성/검증, ⑤ 분배 완료 조건(전원 제출/타임아웃) 명시.
public class LootService
{
private readonly Dictionary<long, Player> _players;
private readonly Leaderboard _board;
private readonly object _boardLock = new object();
// boss 단위 동기화 객체(분배·굴림 수집 직렬화)
private readonly Dictionary<long, object> _bossLocks = new();
private readonly object _bossLocksGuard = new object();
private readonly Dictionary<long, List<LootItem>> _pools = new();
private readonly Dictionary<long, List<LootRoll>> _rolls = new();
private object BossLock(long bossId)
{
lock (_bossLocksGuard)
{
if (!_bossLocks.TryGetValue(bossId, out var l)) { l = new object(); _bossLocks[bossId] = l; }
return l;
}
}
// 굴림은 서버가 생성하는 것이 원칙. 클라는 "굴린다" 의사만 보낸다.
public void SubmitRoll(long bossId, long lootId, long playerId)
{
lock (BossLock(bossId))
{
int serverRoll = Random.Shared.Next(1, 101); // 서버 권위 굴림(1..100)
if (!_rolls.TryGetValue(lootId, out var list))
{
list = new List<LootRoll>();
_rolls[lootId] = list;
}
// 중복 제출 방지: 같은 player 가 이미 굴렸으면 무시
if (list.Exists(r => r.PlayerId == playerId)) return;
list.Add(new LootRoll { PlayerId = playerId, Roll = serverRoll });
}
}
public bool Distribute(long bossId, long lootId)
{
lock (BossLock(bossId)) // 분배 전체를 보스 단위로 직렬화
{
if (!_pools.TryGetValue(bossId, out var pool)) return false;
var loot = pool.Find(i => i.LootId == lootId);
if (loot == null || loot.Assigned) return false; // 멱등: 이미 분배됨
if (!_rolls.TryGetValue(lootId, out var rolls) || rolls.Count == 0)
return false;
// (완료 조건: 전원 제출 또는 타임아웃 확인 — 정책에 따라 여기서 검사)
// 승자 선정(동률 tie-break: PlayerId 등 결정적 규칙)
var winner = rolls.OrderByDescending(r => r.Roll).ThenBy(r => r.PlayerId).First();
if (!_players.TryGetValue(winner.PlayerId, out var p)) return false;
// 지급 + 풀 제거 + 분배 확정(같은 임계 구역 → 복제 불가)
p.GiveItem(loot.ItemId);
loot.Assigned = true;
pool.RemoveAll(i => i.LootId == lootId);
_rolls.Remove(lootId);
// 랭킹 갱신: 별도 락. 순서 고정(bossLock 보유 → boardLock) → 데드락 없음
lock (_boardLock)
{
long cur = _board.GuildScore.TryGetValue(p.GuildId, out var s) ? s : 0;
_board.GuildScore[p.GuildId] = cur + loot.Value; // RMW가 락 안 → lost update 없음
}
return true;
}
}
}
락 순서는 항상
BossLock → _boardLock로 고정한다._boardLock을 잡은 채 다시BossLock을 잡는 경로가 없으므로 역전이 없어 데드락이 발생하지 않는다._bossLocks자체의 get-or-create 도_bossLocksGuard로 원자화한다.
더 나은 설계
1) 분배를 영속 트랜잭션으로(복제는 환불 불가)
인메모리 락은 크래시/멀티 인스턴스에 무력하다. 아이템 복제는 경제를 파괴하므로:
- 분배를 DB 트랜잭션으로:
UPDATE loot SET assigned=1, owner=? WHERE loot_id=? AND assigned=0이affected=1일 때만 지급·점수 가산을 같은 트랜잭션에 커밋. DB가 "한 번만 분배" 를 직렬화로 보장. 트레이드오프: DB 왕복. 복제 방지가 절대 우선이라 정당.
2) 랭킹은 경합 핫스팟 — 분리·집계로
- 모든 분배가 전역
GuildScore로 몰리면 단일 락이 병목이다. 점수 가산을 이벤트로 append(원장) 하고 랭킹은 주기적/비동기 집계(또는 샤딩된 카운터 + 머지)로 도출하면 핫스팟이 사라진다. 실시간 정확도와 처리량의 트레이드오프를 상황에 맞게 선택. - 원자적 증가만 필요하면 DB
UPDATE ... SET score = score + ?또는Interlocked/분산 카운터로 lost update 를 구조적으로 제거.
3) 굴림은 서버 권위 + 결정적 연출
- 굴림값은 서버 RNG로 생성. 연출 동기화가 필요하면 시드를 내려 클라가 재생. 공정성 증명이 필요하면 commit-reveal. 분배 완료 조건(전원 제출/타임아웃)을 FSM으로 명시해 "불완전 집합 분배" 를 차단.
4) 단일 액터 모델
- 보스 인스턴스(레이드)를 단일 스레드(액터) 가 소유해 굴림 수집·분배를 직렬 처리하면 락이 사라지고 복제·레이스가 구조적으로 불가능. 랭킹만 별도 서비스로 메시지 패싱.
면접 포인트
- 면접관이 듣고 싶은 핵심: 공유 상태가 셋(풀·굴림·랭킹)인데 락이 아예 없다는 것을 먼저 보고, "분배는 승자선정+지급+풀제거+점수가산이 하나의 트랜잭션" + "랭킹은 lost update 핫스팟" + "굴림은 서버 권위" 를 한 번에 엮는 것. 락을 두 개 이상 쓰게 되면 락 순서 고정으로 데드락을 피한다는 점까지 말하면 최상급.
- 예상 질문:
- "같은 전리품이 두 명에게 가는 경로를 정확히 설명하라."
→ Distribute 동시 호출이 둘 다
Assigned==false를 보고 둘 다 지급(TOCTOU). 검사-지급-확정을 한 락/한 조건부 UPDATE로 직렬화해야 한다. - "랭킹 점수가 사라지는(lost update) 이유와 해법은?"
→ read-modify-write 가 비원자. 같은 락 안에서 RMW, 또는 DB
score = score + ?, 또는 이벤트 원장 + 비동기 집계로 핫스팟 분산. - "락을 둘(boss, board) 쓰면 데드락은?" → 항상 boss→board 순서로만 잡는다. board 를 잡은 채 boss 를 잡는 경로를 만들지 않으면 역전이 없어 안전.
- "클라가 보낸 roll 을 쓰면?" → int.MaxValue 로 항상 승리. 서버가 굴려야 하고, 연출이 필요하면 시드/commit-reveal.
- "같은 전리품이 두 명에게 가는 경로를 정확히 설명하라."
→ Distribute 동시 호출이 둘 다
해설 — 파티 전리품 분배 + 기여도 랭킹 갱신
난이도: 최상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
이 코드는 세 개의 공유 가변 상태(전리품 풀, 굴림 목록, 랭킹 점수) 를 어떤 락도
없이 동시 수정한다. 그 결과 (1) 같은 전리품이 두 명에게 가는 복제 분배, (2)
굴림 제출/비교의 레이스로 승자 오선정/누락, (3) 랭킹 점수의 고전적 lost update,
(4) 클라가 보낸 굴림값을 검증 없이 신뢰하는 치팅, (5) 여러 unordered_map/vector
동시 변경으로 인한 자료구조 손상(UB), (6) 풀에서 원소를 erase 한 뒤 그 원소를
가리키는 포인터를 다시 쓰는 use-after-free(J→L) 가 한꺼번에 터진다. 동시성·정합성·
치팅·메모리 안전성이 입체적으로 얽힌 최상급 문제다.
문제점
(G)+(I)+(L) 분배 전체가 무락 비원자 — 복제 분배 + lost update (정확성/동시성) ★간판
- 증상: 같은 전리품이 두 명에게 분배(복제)되고, 랭킹 점수는 한 번만 가산되거나 엉뚱하게 덮어쓰인다.
- 재현 조건:
- 같은
lootId에 대한Distribute가 두 스레드에서 동시 호출(분배 트리거 중복, 재전송). 둘 다 (G)에서loot->assigned==false를 관측 → 둘 다 (I)에서 지급 → 둘 다assigned=true. 한 아이템이 두 번 지급된다(복제). - 랭킹: 두 분배가 동시에 (K)에서 같은
guildScore[g]값을 읽고 각자cur + value를 (L)에서 써, 한쪽 가산이 통째로 유실(lost update).
- 같은
- 근본 원인: "이미 분배됐는가" 검사(G)와 "분배 확정"(assigned=true) 사이, 그리고 점수 read(K)-modify-write(L) 사이가 원자적이지 않다. 더 근본적으로 이 클래스에는 공유 상태를 보호하는 락(mutex)이 존재하지 않는다. 분배는 승자선정→지급→풀제거→ 점수가산이 하나의 임계 구역이어야 하는데 전부 노출돼 있다.
(J)→(L) 풀 원소 erase 후 그 포인터 사용 — use-after-free (정확성/메모리 안전) ★C++ 특유
- 증상: (J)에서
pool.erase(remove_if(...))로loot가 가리키던LootItem원소가 제거/이동된다. 그 직후 (L)에서loot->value를 읽으면 이미 파괴/이동된 메모리 접근 = use-after-free(UB) — 쓰레기 가치로 점수가 가산되거나 크래시. - 재현 조건: 항상(단일 스레드에서도).
loot는vector원소를 가리키는 raw 포인터인데,erase/remove_if가 그vector를 변경하면 포인터가 무효화된다. - 근본 원인:
vector원소를 가리키는 포인터/참조는 그vector의 변경(재할당/ erase/이동)으로 무효화된다. 필요한 값(value,itemId)을 erase 전에 지역 변수로 복사해 두어야 한다.
(D)+(E) 굴림 제출 레이스 — 목록 손상 + 승자 누락 (정확성/동시성)
- 증상: 여러 파티원이 동시에 굴림을 제출하면
vector동시push_back의 재할당으로 항목 유실/UB, 또는 (D)에서 두 스레드가 모두 새 vector 를 만들어 한쪽 제출이 사라진다. - 재현 조건:
SubmitRoll동시 호출. (D)find가 둘 다 실패 → 둘 다 새 vector 를rolls_[lootId]에 대입 → 먼저 넣은 vector 의 굴림이 통째로 버려진다. 또는 같은 vector 에 동시push_back→ 재할당 중 손상(UB). - 근본 원인:
unordered_map/vector가 스레드 안전하지 않은데 락 없이 동시 변경. get-or-create 패턴이 원자적이지 않다.
(H)+(B) 굴림 비교가 진행 중인데 분배 시작 — race + 클라 신뢰 (정확성/보안)
- 증상: 아직 모든 파티원의 굴림이 안 들어왔는데
Distribute가 돌면 불완전한 집합에서 승자 선정. (H)max_element가 빈rolls면end()역참조로 UB. 동률일 때 tie-break 규칙이 없어 비결정적. - 클라 신뢰 결함(★치팅):
LootRoll.roll은 클라가 보낸 값이다. 어뷰저가roll=INT_MAX를 보내면 항상 승리. 굴림은 서버 권위 영역인데 클라가 정한다. - 근본 원인: 굴림 수집 완료 조건(모든 파티원 제출/타임아웃)과 분배 시점이 동기화되지 않고, 굴림값 검증/서버 생성이 없다.
(C)+(L) 랭킹 guildScore 비보호 — lost update + 손상 (정확성/동시성)
- 위 (L)의 lost update 외에도,
guildScore(unordered_map)를 동시 쓰기 하면 rehash 중 손상/크래시. 랭킹은 서버 전역 공유 상태라 모든 분배가 여기로 몰려 최대 경합 지점.
(F)+(J) 풀 접근/제거 비보호 + 컨테이너 변경 (정확성/동시성)
pools_[bossId]의operator[]는 없는 키를 자동 삽입한다(유령 풀). (J)erase와 (F) 순회/다른 분배가 동시에 일어나면 컨테이너 변경/반복자 무효화 → UB.
(A) assigned 플래그만으로 멱등성 — 영속/크래시 취약 (정확성)
- 인메모리 플래그라 지급(I) 후 (J)/(L) 전에 크래시하면 재처리 시 재분배 가능. 분배 전체가 복구 가능한 단일 트랜잭션이 아니다.
수정안
핵심: ① 보스(또는 loot) 단위 mutex 를 도입해 굴림 수집과 분배를 직렬화, ② 분배 전체(승자선정→지급→풀제거→점수가산)를 하나의 임계 구역으로, ③ 랭킹은 별도 락으로 보호하되 락 순서를 고정(boss 락 → board 락)해 데드락 회피, ④ 굴림은 서버 권위로 생성/검증, ⑤ erase 전에 필요한 값을 복사(UAF 차단).
class LootService {
public:
LootService(std::unordered_map<int64_t, Player*>& players, Leaderboard& board)
: players_(players), board_(board) {}
// 굴림은 서버가 생성하는 것이 원칙. 클라는 "굴린다" 의사만 보낸다.
void SubmitRoll(int64_t bossId, int64_t lootId, int64_t playerId) {
std::lock_guard<std::mutex> lk(BossLock(bossId));
thread_local std::mt19937 rng{std::random_device{}()};
std::uniform_int_distribution<int> dist(1, 100);
auto& list = rolls_[lootId];
// 중복 제출 방지: 같은 player 가 이미 굴렸으면 무시
for (auto& r : list) if (r.playerId == playerId) return;
list.push_back(LootRoll{playerId, dist(rng)}); // 서버 권위 굴림
}
bool Distribute(int64_t bossId, int64_t lootId) {
std::lock_guard<std::mutex> lk(BossLock(bossId)); // 분배 전체 직렬화
auto pit = pools_.find(bossId);
if (pit == pools_.end()) return false;
auto& pool = pit->second;
LootItem* loot = nullptr;
for (auto& i : pool) if (i.lootId == lootId) { loot = &i; break; }
if (loot == nullptr || loot->assigned) return false; // 멱등: 이미 분배됨
auto rit = rolls_.find(lootId);
if (rit == rolls_.end() || rit->second.empty()) return false;
// (완료 조건: 전원 제출 또는 타임아웃 확인 — 정책에 따라 여기서 검사)
// 동률 tie-break: roll 내림차순, playerId 오름차순(결정적)
auto& rolls = rit->second;
auto winner = *std::max_element(rolls.begin(), rolls.end(),
[](const LootRoll& a, const LootRoll& b) {
return a.roll != b.roll ? a.roll < b.roll : a.playerId > b.playerId;
});
auto wpit = players_.find(winner.playerId);
if (wpit == players_.end()) return false;
Player* p = wpit->second;
// erase 전에 필요한 값 복사(use-after-free 차단)
int itemId = loot->itemId;
int64_t value = loot->value;
p->GiveItem(itemId);
loot->assigned = true;
pool.erase(std::remove_if(pool.begin(), pool.end(),
[&](const LootItem& i){ return i.lootId == lootId; }), pool.end());
rolls_.erase(lootId);
// 이 시점부터 loot 포인터는 사용 금지(무효화됨)
// 랭킹 갱신: 별도 락. 순서 고정(boss → board) → 데드락 없음
{
std::lock_guard<std::mutex> blk(boardMtx_);
int64_t cur = board_.guildScore.count(p->guildId) ? board_.guildScore[p->guildId] : 0;
board_.guildScore[p->guildId] = cur + value; // RMW가 락 안 → lost update 없음
}
return true;
}
private:
std::mutex& BossLock(int64_t bossId) {
std::lock_guard<std::mutex> g(bossLocksGuard_);
// 포인터 안정성을 위해 unique_ptr<mutex> 보관(mutex 는 이동 불가)
auto it = bossLocks_.find(bossId);
if (it == bossLocks_.end())
it = bossLocks_.emplace(bossId, std::make_unique<std::mutex>()).first;
return *it->second;
}
std::unordered_map<int64_t, Player*>& players_;
Leaderboard& board_;
std::mutex boardMtx_;
std::mutex bossLocksGuard_;
std::unordered_map<int64_t, std::unique_ptr<std::mutex>> bossLocks_;
std::unordered_map<int64_t, std::vector<LootItem>> pools_;
std::unordered_map<int64_t, std::vector<LootRoll>> rolls_;
};
락 순서는 항상
BossLock → boardMtx_로 고정한다.boardMtx_를 잡은 채BossLock을 잡는 경로가 없으므로 역전이 없어 데드락이 없다.bossLocks_자체의 get-or-create 도bossLocksGuard_로 원자화한다.mutex는 복사·이동 불가라unique_ptr로 보관해 map rehash 에도 포인터/참조가 안정적이게 한다.
더 나은 설계
1) 분배를 영속 트랜잭션으로(복제는 환불 불가)
인메모리 락은 크래시/멀티 인스턴스에 무력하다. 아이템 복제는 경제를 파괴하므로:
- 분배를 DB 트랜잭션으로:
UPDATE loot SET assigned=1, owner=? WHERE loot_id=? AND assigned=0이affected=1일 때만 지급·점수 가산을 같은 트랜잭션에 커밋. DB가 "한 번만 분배" 를 직렬화로 보장. 트레이드오프: DB 왕복. 복제 방지가 절대 우선이라 정당.
2) 랭킹은 경합 핫스팟 — 분리·집계로
- 모든 분배가 전역
guildScore로 몰리면 단일 락이 병목이다. 점수 가산을 이벤트로 append(원장) 하고 랭킹은 주기적/비동기 집계(또는 샤딩된 카운터 + 머지)로 도출하면 핫스팟이 사라진다. 원자적 증가만 필요하면 DBUPDATE ... SET score = score + ?또는std::atomic분산 카운터로 lost update 를 구조적으로 제거.
3) 굴림은 서버 권위 + 결정적 연출
- 굴림값은 서버 RNG로 생성. 연출 동기화가 필요하면 시드를 내려 클라가 재생. 공정성 증명이 필요하면 commit-reveal. 분배 완료 조건(전원 제출/타임아웃)을 FSM으로 명시해 "불완전 집합 분배" 를 차단.
4) 단일 액터 모델
- 보스 인스턴스(레이드)를 단일 스레드(액터) 가 소유해 굴림 수집·분배를 직렬 처리하면 락이 사라지고 복제·레이스가 구조적으로 불가능. 랭킹만 별도 서비스로 메시지 패싱.
면접 포인트
- 면접관이 듣고 싶은 핵심: 공유 상태가 셋(풀·굴림·랭킹)인데 락이 아예 없다는 것을 먼저 보고, "분배는 승자선정+지급+풀제거+점수가산이 하나의 트랜잭션" + "랭킹은 lost update 핫스팟" + "굴림은 서버 권위" 를 한 번에 엮는 것. C++ 에선 vector 원소 포인터의 무효화(use-after-free) 와 컨테이너 동시 변경 UB, 락을 두 개 쓸 때 락 순서 고정까지 말하면 최상급.
- 예상 질문:
- "같은 전리품이 두 명에게 가는 경로를 정확히 설명하라."
→ Distribute 동시 호출이 둘 다
assigned==false를 보고 둘 다 지급(TOCTOU). 검사-지급-확정을 한 락/한 조건부 UPDATE로 직렬화해야 한다. - "(J) erase 후 (L)에서
loot->value를 쓰는 게 왜 위험한가?" →vector::erase가 원소를 이동/파괴해loot포인터가 무효화 → use-after-free(UB). 필요한 값을 erase 전에 복사해 둬야 한다. - "락을 둘(boss, board) 쓰면 데드락은?" → 항상 boss→board 순서로만 잡는다. board 를 잡은 채 boss 를 잡는 경로를 만들지 않으면 역전이 없어 안전.
- "클라가 보낸 roll 을 쓰면?" → INT_MAX 로 항상 승리. 서버가 굴려야 하고, 연출이 필요하면 시드/commit-reveal.
- "같은 전리품이 두 명에게 가는 경로를 정확히 설명하라."
→ Distribute 동시 호출이 둘 다