처음 만든 사다리는 평범했다. 여섯 명이 내려가면 여섯 칸 중 한 칸이 「당첨」이고 나머지는 「세이프」다. 당첨된 한 사람이 커피값을 전부 낸다.
9월 21일 새벽, 폰에 깔아 직접 해 보고 바꾸기로 했다. 이름 옆과 도착 칸에 등수를 보여 주고, 한 명이 다 내는 대신 여러 명이 순위별로 금액을 나눠 내게 하는 것이다.
같은 날 낮에 「등수 표시와 순위별 커피값 분담」 커밋이 들어갔다. 이 글에서는 그때 만든 분담 공식 GameData.ComputeShares와, 그것을 게임과 결과 화면에 붙인 과정을 정리한다.

규칙
커밋 메시지에 적은 규칙은 이렇다.
- 상위 2명은 면제한다.
- 하위 최대 4명이 등차 가중치로 나눠 낸다. 4명이 나누면 40:30:20:10이다.
- 100원 단위로 맞추고, 남는 금액은 꼴등이 낸다.
- 총액이 0이면 금액 없이 꼴등만 벌칙이다.
돈을 내는 인원은 PayerCount 한 줄로 정한다.
/// <summary>분담 인원. 상위 2명은 면제하고, 하위 최대 4명이 나눠 낸다.</summary>
public static int PayerCount(int playerCount)
{
return Mathf.Clamp(playerCount - 2, 1, 4);
}
playerCount - 2를 1~4 사이로 자른다. 인원별로 풀어 보면 다음과 같다.
| 인원 | 내는 사람 (PayerCount) |
면제 | 가중치 (꼴등부터) | 비율 |
|---|---|---|---|---|
| 2명 | 1 | 1 | 1 | 꼴등 100% |
| 3명 | 1 | 2 | 1 | 꼴등 100% |
| 4명 | 2 | 2 | 2 : 1 | 약 67 : 33 |
| 5명 | 3 | 2 | 3 : 2 : 1 | 50 : 약 33 : 약 17 |
| 6명 | 4 | 2 | 4 : 3 : 2 : 1 | 40 : 30 : 20 : 10 |
| 7명 | 4 | 3 | 4 : 3 : 2 : 1 | 40 : 30 : 20 : 10 |
| 8명 | 4 | 4 | 4 : 3 : 2 : 1 | 40 : 30 : 20 : 10 |
「상위 2명 면제」는 3명 이상일 때 이야기다. 2명이면 아래쪽 한계 1에 걸려 꼴등 한 명이 내고 1등만 면제된다. 7명 이상이면 위쪽 한계 4에 걸려 면제 인원이 3명, 4명으로 늘어난다.
ComputeShares
public static List<int> ComputeShares(int total, int playerCount)
{
var shares = new List<int>();
for (int i = 0; i < playerCount; i++) shares.Add(0);
if (total <= 0 || playerCount <= 0) return shares;
int payers = PayerCount(playerCount);
int weightSum = payers * (payers + 1) / 2;
int assigned = 0;
// i = 0 이 꼴등. 꼴등 가중치가 payers 로 가장 크다.
for (int i = 0; i < payers; i++)
{
int weight = payers - i;
int amount = (int)((long)total * weight / weightSum / 100 * 100);
shares[playerCount - 1 - i] = amount;
assigned += amount;
}
shares[playerCount - 1] += total - assigned; // 100원 단위로 자르고 남은 금액은 꼴등이
return shares;
}
반환 순서는 1등부터 꼴등까지다. 가중치는 꼴등이 payers, 그 위가 payers - 1 … 순으로 1씩 줄고, 합은 payers * (payers + 1) / 2다. 4명이면 10이라서 4:3:2:1이 곧 40:30:20:10이 된다.
핵심은 한 줄짜리 금액 계산이다. 계산 순서는 이렇다.
(long)total * weight— 곱셈을 먼저 한다.total이int라서 큰 금액에 4를 곱하면 넘칠 수 있으니long으로 올린다. 금액 입력칸은 8자리까지라 실제로는 넘지 않지만, 함수 자체는int전체를 받는다./ weightSum— 정수 나눗셈이라 소수점 아래는 버린다./ 100 * 100— 한 번 더 버려서 100원 단위로 내린다.
이렇게 내린 금액을 모두 더한 assigned와 총액의 차이를 마지막 줄에서 꼴등 몫에 얹는다. 그래서 몫의 합은 언제나 총액과 정확히 같다.
손으로 계산해 보기
30,000원 · 6명 (payers 4, weightSum 10)
| 등수 | weight | 30,000 x w | ÷ 10 | 100원 내림 |
|---|---|---|---|---|
| 6등(꼴등) | 4 | 120,000 | 12,000 | 12,000 |
| 5등 | 3 | 90,000 | 9,000 | 9,000 |
| 4등 | 2 | 60,000 | 6,000 | 6,000 |
| 3등 | 1 | 30,000 | 3,000 | 3,000 |
합이 30,000이라 나머지는 0이다. 1·2등은 0원이다. 위 사다리 스크린샷의 12,000 / 9,000 / 6,000 / 3,000과 같다.
25,000원 · 6명도 같은 비율이라 10,000 / 7,500 / 5,000 / 2,500으로 딱 떨어진다. 나머지가 생기는 예를 몇 개 더 계산해 보면 이렇다.
23,500원 · 6명 (weightSum 10)
- 6등: 94,000 ÷ 10 = 9,400 → 9,400
- 5등: 70,500 ÷ 10 = 7,050 → 7,000
- 4등: 47,000 ÷ 10 = 4,700 → 4,700
- 3등: 23,500 ÷ 10 = 2,350 → 2,300
assigned23,400, 나머지 100원 → 꼴등 9,500원
25,000원 · 5명 (payers 3, weightSum 6)
- 5등: 75,000 ÷ 6 = 12,500 → 12,500
- 4등: 50,000 ÷ 6 = 8,333 → 8,300
- 3등: 25,000 ÷ 6 = 4,166 → 4,100
assigned24,900, 나머지 100원 → 꼴등 12,600원
10,000원 · 4명 (payers 2, weightSum 3)
- 4등: 20,000 ÷ 3 = 6,666 → 6,600
- 3등: 10,000 ÷ 3 = 3,333 → 3,300
assigned9,900, 나머지 100원 → 꼴등 6,700원
10,050원 · 4명 — 총액이 100원 단위가 아닐 때
- 4등: 20,100 ÷ 3 = 6,700 → 6,700
- 3등: 10,050 ÷ 3 = 3,350 → 3,300
assigned10,000, 나머지 50원 → 꼴등 6,750원
100원 단위로 맞추는 건 꼴등을 뺀 사람들뿐이고, 끝자리는 전부 꼴등이 떠안는다.
10,000원 · 3명은 payers가 1이라 가중치 합도 1이다. 꼴등 혼자 10,000원을 내고, 나머지 두 명은 0원이다.
게임 선택 화면: 입력하는 순간 보여 준다
금액은 게임 선택 화면 맨 위의 「커피값 총액」 칸에 넣는다. 값은 PlayerPrefs에 저장돼 다음 판에도 남는다. 입력이 바뀔 때마다 UpdateHint가 같은 ComputeShares를 불러 안내 문구를 다시 만든다.
void UpdateHint()
{
int n = GameData.Players.Count;
int total;
if (!int.TryParse(_amount.text, out total)) total = 0;
if (total <= 0)
{
_hint.text = "비워 두면 금액 없이 꼴등만 정합니다";
_hint.color = UIFactory.TextDim;
return;
}
var shares = GameData.ComputeShares(total, n);
_hint.text = n + "명 중 하위 " + GameData.PayerCount(n) + "명이 나눠 냅니다 · 꼴등 " + GameData.Won(shares[n - 1]);
_hint.color = UIFactory.Accent;
}

게임을 시작하기 전에 꼴등이 얼마를 내는지 미리 알게 된다. 금액 표시는 12000을 "12,000원"으로 바꾸는 GameData.Won 하나로 통일했다.
게임마다 등수를 정하는 방법
공식은 등수만 받으면 된다. 그래서 게임 쪽 약속은 하나다. 끝나면 1등부터 꼴등까지 순서대로 GameData.SetResult(ranking)을 부른다.
public static void SetResult(List<Player> ranking)
{
LastRanking = ranking ?? new List<Player>();
LastShares = ComputeShares(TotalAmount, LastRanking.Count);
LastLoser = LastRanking.Count > 0 ? LastRanking[LastRanking.Count - 1] : null;
}
처음 두 게임(사다리, 눈치)은 이 커밋에서 등수를 매기도록 고쳤고, 뒤에 추가한 게임 13개는 처음부터 이 약속에 맞춰 만들었다. 각 게임의 기준은 커밋 메시지와 코드에 남은 대로 정리하면 다음과 같다.
| 유형 | 게임 | 등수 기준 |
|---|---|---|
| 운 | 사다리 타기 | 아래 칸마다 1등~꼴등을 무작위로 섞어 둔다. 도착한 칸이 곧 등수 |
| 운 | 룰렛 | 바늘이 가리킨 사람이 꼴등, 바늘에서 멀수록 안전 |
| 운 | 제비뽑기 | 제비 안에 1등~꼴등이 섞여 있다. 꼴등 제비는 「당첨!」 |
| 순발력 | 눈치 게임 | 먼저 누른 순서. 벌칙자가 꼴등, 안 누른 사람은 뒤로 |
| 순발력 | 폭탄 돌리기 | 터질 때 든 사람이 꼴등, 나머지는 폭탄을 적게 든 순서 |
| 서바이벌 | 369 · 바니바니 · 순서 외우기 · 31 게임 | 끝까지 남은 사람이 1등, 먼저 탈락할수록 아래 |
| 서바이벌 | 업다운 | 먼저 맞혀 탈출할수록 위, 끝까지 남은 한 명이 꼴등 |
| 서바이벌 | 제일 작은 숫자 | 안 겹친 숫자는 작은 수부터 등수 확정, 겹친 사람끼리 다시 |
| 점수 | 초성 퀴즈 | 점수 많은 순 → 맞힌 시간 합이 짧은 순 |
| 점수 | 딱 멈춰! | 총점 순 (같으면 한 라운드 최고 점수로) |
| 점수 | 연타 대결 | 많이 누른 순 (같으면 무작위) |
| 점수 | 끝까지 버텨! | 오래 버틸수록 위, 폭발까지 누른 사람은 맨 아래 (그들끼리는 무작위) |
사다리는 원래 「당첨」 칸 하나만 무작위로 골랐다. 이제는 칸 번호 → 등수 배열을 Fisher–Yates로 섞는다. 코드 주석에는 이유가 한 줄 적혀 있다. 왼쪽이 늘 1등이면 재미가 없다.
// 칸마다 등수를 무작위로 배치 (왼쪽이 늘 1등이면 재미가 없다)
_slotRank = new int[_n];
for (int i = 0; i < _n; i++) _slotRank[i] = i;
for (int i = _n - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
int tmp = _slotRank[i];
_slotRank[i] = _slotRank[j];
_slotRank[j] = tmp;
}
색 네 가지
등수가 생기면서 「당첨/세이프」 두 색으로는 부족해졌다. 사다리·눈치·결과 화면 모두 같은 규칙으로 색을 고른다.
/// <summary>등수별 색. 1등 금색, 면제 초록, 분담 주황, 꼴등 빨강.</summary>
static Color RankColor(int rank, int count, int share)
{
bool pays = share > 0 || (GameData.TotalAmount <= 0 && rank == count - 1);
if (!pays) return rank == 0 ? UIFactory.Accent : UIFactory.Safe;
return rank == count - 1 ? UIFactory.Danger : Warn;
}
「돈을 내는가」는 몫이 0보다 큰지로 판단하고, 총액을 안 넣은 경우에만 꼴등을 내는 사람으로 친다. 사다리에서는 도착하면 위쪽 이름 옆에 「현우 6등」처럼 등수가 붙고, 아래 칸에는 등수와 금액(면제면 「세이프」)이 같은 색으로 뜬다. 눈치 게임은 꼴등 칸 배경이 빨강이라 그 칸만 이름 글씨를 어둡게 바꿨다.
결과 화면, 카톡 문구, 이번 달 기록
결과 화면은 꼴등을 크게 보여 주고 그 아래에 1등부터 꼴등까지 순위표를 깐다. 돈을 내는 줄만 패널 배경을 깔아서 면제와 분담이 한눈에 갈린다. 면제 줄의 금액 자리는 「-」, 총액 없이 꼴등만 정한 판이면 꼴등 줄에 「쏨!」이 들어간다.

위 화면이 앞의 사다리 판 결과다. 「결과 복사」를 누르면 카카오톡에 붙여 넣을 글이 클립보드에 들어간다. 이 판이라면 BuildShareText가 만드는 글은 다음과 같다.
[오늘 누가 쏘냐?]
사다리 타기 결과 · 총 30,000원
1등 영희 0원
2등 민수 0원
3등 지영 3,000원
4등 수진 6,000원
5등 철수 9,000원
6등 현우 12,000원
총액을 넣으면 면제자도 0원으로 찍힌다. 화면의 「-」와 다르지만, 코드는 총액이 있으면 모든 줄에 금액을 붙인다. 총액이 0이면 금액 없이 꼴등 줄 끝에만 ← 쏨!이 붙는다. 「이미지 공유」는 버튼을 숨기고 워터마크를 붙여 이 화면을 그대로 캡처한다. Android 쪽 저장·공유 처리는 9편에서 다룬다.
결과 화면이 뜰 때 RecordLoss가 꼴등 이름으로 벌칙 1회를 기록한다. 기록은 "yyyy-MM" 월 문자열과 이름을 키로 PlayerPrefs에 JSON으로 쌓이고, 「벌칙 기록 보기」 화면이 이번 달 것만 꺼내 횟수 순으로 최대 8명까지 막대로 보여 준다. 분담 금액이 아니라 꼴등 횟수만 센다. 결과 화면 부제에도 「이번 달 1번째 벌칙」처럼 몇 번째인지가 붙는다.
정리
- 분담 인원은
Mathf.Clamp(n - 2, 1, 4). 2·3명은 꼴등 혼자, 4·5명은 2·3명이 나누고, 6명 이상은 하위 4명이 40:30:20:10으로 낸다. - 금액은
(long)total * weight / weightSum / 100 * 100순서로 계산한다. 곱셈을 먼저, 내림은 두 번, 나머지는 꼴등에게 몰아서 합이 총액과 정확히 맞는다. - 게임은 1등부터 꼴등까지 줄 세워
SetResult만 부른다. 분담 계산·색·결과 화면·복사 문구는 공통이라 게임을 추가해도 손댈 곳이 없다. - 게임 선택 화면의 안내와 실제 결과가 같은 함수를 쓰므로, 시작 전에 본 꼴등 금액이 끝나고 나오는 금액과 같다.
다음 글 6편 (10월 5일 공개)에서는 서버 없이 여러 폰으로 같이 하는 LAN 기능을 다룬다.