5. 확률 아이템(가챠) 뽑기 + 천장 시스템
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 확률 아이템(가챠) 뽑기 + 천장(pity) 시스템
// ----------------------------------------------------------------------------
// 시나리오:
// - 플레이어가 유료 재화로 가챠를 돌린다. 등급별 확률표가 있다.
// - "천장(pity)": 90회 연속 최고등급이 안 나오면 90회째 최고등급 확정.
// - 클라이언트는 연출(애니메이션)을 위해 결과를 미리 알아야 부드럽다.
// - 결과 보상은 인벤토리에 지급되고, 뽑기 기록은 영수증으로 남는다.
//
// 요구사항:
// - 가챠 결과는 서버가 권위적으로 결정/검증해야 한다(치팅 방지).
// - 천장 카운터는 정확히 관리되어야 한다.
// - 같은 플레이어가 여러 기기/탭에서 동시에 뽑아도 정합성이 유지돼야 한다.
// - 재화 차감과 보상 지급은 원자적이어야 한다(돈만 빠지고 보상 없음 금지).
// - 클라이언트 재전송으로 인한 중복 뽑기는 한 번만 반영.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Generic;
public class GachaRequest
{
public long PlayerId;
public string BannerId;
public int Count; // 몇 연차
public int[] ClientRolls; // (A) 클라가 미리 굴린 0~9999 난수들(연출 동기화용)
public long Seed; // (B) 클라가 보낸 시드
}
public class GachaItem { public int ItemId; public int Grade; public int Weight; }
public class GachaService
{
private readonly Dictionary<string, List<GachaItem>> _banners;
private readonly Dictionary<long, int> _pityCounter = new Dictionary<long, int>(); // (C)
private readonly Random _rng = new Random(); // (D)
private const int PITY = 90;
private const int MAX_GRADE = 5;
public GachaService(Dictionary<string, List<GachaItem>> banners) { _banners = banners; }
public List<GachaItem> Pull(GachaRequest req, Player player)
{
var pool = _banners[req.BannerId];
int totalWeight = 0;
foreach (var it in pool) totalWeight += it.Weight;
var results = new List<GachaItem>();
// 재화 차감 (1연차당 100)
long cost = req.Count * 100;
lock (player.Lock)
{
if (player.Gold < cost) throw new Exception("not enough");
player.Gold -= cost; // (E)
}
for (int i = 0; i < req.Count; i++)
{
// (F) 클라가 보낸 난수를 사용해 결과 결정(연출과 일치시키려고)
int roll = req.ClientRolls[i];
// 천장: 카운터가 PITY-1 이상이면 최고등급 확정
int pity = _pityCounter.TryGetValue(req.PlayerId, out var c) ? c : 0; // (G)
GachaItem picked;
if (pity >= PITY - 1)
{
picked = PickHighest(pool);
_pityCounter[req.PlayerId] = 0; // (H)
}
else
{
picked = PickByWeight(pool, totalWeight, roll);
if (picked.Grade == MAX_GRADE)
_pityCounter[req.PlayerId] = 0;
else
_pityCounter[req.PlayerId] = pity + 1; // (I)
}
results.Add(picked);
// (J) 보상 지급 (인벤토리에 추가하는 코드는 생략, 여기서 직접)
GrantReward(player, picked);
}
return results;
}
private GachaItem PickByWeight(List<GachaItem> pool, int totalWeight, int roll)
{
// roll: 0..9999 를 가중치 구간에 매핑
int point = roll % totalWeight;
int acc = 0;
foreach (var it in pool)
{
acc += it.Weight;
if (point < acc) return it;
}
return pool[pool.Count - 1];
}
private GachaItem PickHighest(List<GachaItem> pool)
{
GachaItem best = pool[0];
foreach (var it in pool) if (it.Grade > best.Grade) best = it;
return best;
}
private void GrantReward(Player player, GachaItem item) { /* 인벤토리 지급 */ }
}
public class Player
{
public long Id;
public long Gold;
public readonly object Lock = new object();
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 확률 아이템(가챠) 뽑기 + 천장(pity) 시스템
// ----------------------------------------------------------------------------
// 시나리오:
// - 플레이어가 유료 재화로 가챠를 돌린다. 등급별 확률표가 있다.
// - "천장(pity)": 90회 연속 최고등급이 안 나오면 90회째 최고등급 확정.
// - 클라이언트는 연출(애니메이션)을 위해 결과를 미리 알아야 부드럽다.
// - 결과 보상은 인벤토리에 지급되고, 뽑기 기록은 영수증으로 남는다.
//
// 요구사항:
// - 가챠 결과는 서버가 권위적으로 결정/검증해야 한다(치팅 방지).
// - 천장 카운터는 정확히 관리되어야 한다.
// - 같은 플레이어가 여러 기기/탭에서 동시에 뽑아도 정합성이 유지돼야 한다.
// - 재화 차감과 보상 지급은 원자적이어야 한다(돈만 빠지고 보상 없음 금지).
// - 클라이언트 재전송으로 인한 중복 뽑기는 한 번만 반영.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
#include <mutex>
#include <random>
struct GachaRequest {
int64_t playerId;
std::string bannerId;
int count; // 몇 연차
std::vector<int> clientRolls; // (A) 클라가 미리 굴린 0~9999 난수들(연출 동기화용)
int64_t seed; // (B) 클라가 보낸 시드
};
struct GachaItem { int itemId; int grade; int weight; };
struct Player {
int64_t id;
int64_t gold;
std::mutex lock;
void GiveItem(int itemId) { /* 인벤토리 지급 (생략) */ }
};
class GachaService {
public:
explicit GachaService(std::unordered_map<std::string, std::vector<GachaItem>>& banners)
: banners_(banners) {}
std::vector<GachaItem> Pull(const GachaRequest& req, Player& player) {
auto& pool = banners_.at(req.bannerId);
int totalWeight = 0;
for (auto& it : pool) totalWeight += it.weight;
std::vector<GachaItem> results;
// 재화 차감 (1연차당 100)
int64_t cost = (int64_t)req.count * 100;
{
std::lock_guard<std::mutex> lk(player.lock);
if (player.gold < cost) throw std::runtime_error("not enough");
player.gold -= cost; // (E)
}
for (int i = 0; i < req.count; i++) {
// (F) 클라가 보낸 난수를 사용해 결과 결정(연출과 일치시키려고)
int roll = req.clientRolls[i];
// 천장: 카운터가 PITY-1 이상이면 최고등급 확정
int pity = pityCounter_.count(req.playerId) ? pityCounter_[req.playerId] : 0; // (G)
GachaItem picked;
if (pity >= PITY - 1) {
picked = PickHighest(pool);
pityCounter_[req.playerId] = 0; // (H)
} else {
picked = PickByWeight(pool, totalWeight, roll);
if (picked.grade == MAX_GRADE)
pityCounter_[req.playerId] = 0;
else
pityCounter_[req.playerId] = pity + 1; // (I)
}
results.push_back(picked);
// (J) 보상 지급
GrantReward(player, picked);
}
return results;
}
private:
GachaItem PickByWeight(const std::vector<GachaItem>& pool, int totalWeight, int roll) {
// roll: 0..9999 를 가중치 구간에 매핑
int point = roll % totalWeight;
int acc = 0;
for (auto& it : pool) {
acc += it.weight;
if (point < acc) return it;
}
return pool[pool.size() - 1];
}
GachaItem PickHighest(const std::vector<GachaItem>& pool) {
GachaItem best = pool[0];
for (auto& it : pool) if (it.grade > best.grade) best = it;
return best;
}
void GrantReward(Player& player, const GachaItem& item) { player.GiveItem(item.itemId); }
std::unordered_map<std::string, std::vector<GachaItem>>& banners_;
std::unordered_map<int64_t, int> pityCounter_; // (C)
std::mt19937 rng_{std::random_device{}()}; // (D)
static constexpr int PITY = 90;
static constexpr int MAX_GRADE = 5;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.