「오늘 누가 쏘냐?」의 게임은 대부분 누구나 아는 놀이다. 369, 바니바니, 업다운, 31 게임. 규칙을 따로 설명할 필요도 없을 것 같았다.
막상 앱으로 옮기니 사정이 달랐다. 술자리에서는 사람이 눈치껏 넘어가던 부분을 앱은 하나하나 정해 줘야 한다. 폰 하나를 돌려 가며 하면 앞사람 화면이 보이고, 여러 폰으로 하면 비밀을 누가 들고 있을지 정해야 한다. 어떤 규칙은 앱으로 옮기자마자 판이 끝나지 않았다.
이 글은 코드보다 규칙 이야기다. 게임 15개를 만들며 규칙을 어떻게 다듬었는지 정리한다.
게임 후보를 고른 방식
사다리·눈치 게임으로 시작했고, 폭탄 돌리기·룰렛·초성 퀴즈는 첫날 로드맵에 넣어 둔 것을 만들었다. 그 뒤로는 후보 목록을 먼저 넓게 뽑아 두고, 그중에서 골라 만들 순서를 정했다. 9월 24일에는 순서 외우기, 딱 멈춰!, 제일 작은 숫자, 제비뽑기를 그 순서대로 넣었다.
9월 26일에는 후보에서 31 게임과 연타 대결을 고르고 업다운을 더해 세 개를 넣었다. 2열 열두 칸이던 게임 선택 화면은 이때 3열 열다섯 칸으로 바꿨다. 9월 30일에는 마지막 한 칸을 채울 게임으로 끝까지 버텨!를 골랐다. 이것이 열다섯 번째 게임이다.
1편에서 정리한 분류로 보면 지금 이렇게 나뉜다.
| 분류 | 게임 |
|---|---|
| 운 | 사다리 타기, 룰렛, 제비뽑기 |
| 순발력 | 눈치 게임, 폭탄 돌리기, 연타 대결, 딱 멈춰!, 끝까지 버텨! |
| 말놀이·숫자 | 369, 바니바니, 31 게임, 업다운, 제일 작은 숫자 |
| 머리 | 초성 퀴즈, 순서 외우기 |
운으로 정하는 게임부터 손이나 머리를 쓰는 게임까지 섞여 있어서, 그날 분위기에 맞춰 고르면 된다.

앞사람을 보고 따라 할 수 없게
폰 하나를 돌려 가며 하는 게임은 뒷사람이 유리하다. 앞사람이 하는 걸 다 봤기 때문이다.
순서 외우기는 네 칸이 빛나는 순서를 따라 누르는 게임이다. 폰 하나 모드에서는 사람마다 다른 순서를 만든다. 설명 화면에도 "각자 다른 순서가 나와서 앞사람 것을 봐도 소용없어요"라고 적었다. 같이 하기에서는 모두 같은 순서를 각자 폰에서 동시에 보므로 이 문제가 없다.
**딱 멈춰!**는 왔다 갔다 하는 바늘을 노란 목표에 멈추는 게임이다. 폰 하나 모드에서 목표와 바늘 출발 위치가 같으면 뒷사람은 앞사람 타이밍을 그대로 따라 하면 된다. 그래서 사람마다 목표(막대의 15~85% 사이)와 출발 위치를 무작위로 뽑는다.
제일 작은 숫자는 각자 1~10 중 하나를 몰래 고르고, 안 겹친 숫자 중 가장 작은 수가 1등이다. 폰을 돌려 가며 고르면 앞사람이 누른 숫자가 화면에 남는다. 그래서 사람 사이에 화면을 완전히 가리는 넘기기 화면을 넣었다. "철수 차례 — 다른 사람은 화면을 보지 마세요"가 뜨고, 본인이 「준비됐어요」를 눌러야 숫자판이 나온다. 모두 고르면 한꺼번에 공개한다.

같이 하기에서 비밀은 방장 폰만 안다
여러 폰으로 하는 같이 하기는 방장 폰이 규칙을 판정한다(7편). 그러면 비밀도 방장 폰에 있어야 한다. 문제는 방장도 참가자라는 점이다. 그래서 원칙을 하나 정했다. 비밀은 방장 폰만 알고, 방장 화면에도 보이지 않는다. 게스트에게는 공개 전까지 아예 보내지 않는다.
- 업다운: 비밀 숫자는 방장 폰만 알고 방장 화면에도 안 보인다.
- 제비뽑기: 방장은 펼친 제비의 등수만 보낸다. 안 펼친 제비 내용은 방장 화면에도 안 보인다.
- 제일 작은 숫자: 공개 전에는 누가 골랐는지만 보낸다. 숫자는 보내지 않는다.
- 끝까지 버텨!: 폭발 시각은 공개할 때까지 절대 보내지 않는다.
코드로 보면 상태 메시지를 만들 때 공개 단계가 아니면 0을 넣는 한 줄이다. 제일 작은 숫자는 이렇다.
bool reveal = phase == "reveal";
foreach (var e in _entries)
{
// ...
m.picked.Add(e.Picked ? 1 : 0);
m.nums.Add(reveal ? e.Pick : 0); // 공개 전에는 숫자를 절대 보내지 않는다
}
제비뽑기도 m.lotRank.Add(lot.Open ? lot.Rank : 0), 끝까지 버텨!도 boomAt = phase == "boom" ? _boomAt : 0f로 같은 모양이다. 공개 전 값은 게스트 폰에 아예 도착하지 않는다.
비밀이 화면 연출로 새는 경우도 있었다. 끝까지 버텨!는 5~15초 사이 몰래 정한 시각에 폭탄이 터지고, 그 전에 가장 늦게 손을 뗀 사람이 1등이다. 폭탄이 터질 때가 가까울수록 빨리 째깍거리면 그게 곧 힌트가 된다. 그래서 폭탄은 폭발 시각과 상관없이 경과 시간으로만 달아오르게 했다.
/// <summary>폭탄은 경과 시간으로만 달아오른다. 폭발 시각과 무관해서 힌트가 되지 않는다</summary>
void HeatBomb(float t)
{
int heat = t < 5f ? 1 : t < 10f ? 2 : 3;
float interval = heat == 1 ? 1f : heat == 2 ? 0.5f : 0.25f;
// ... 째깍 소리, 스프라이트, 흔들림
}

끝나지 않는 판을 막는다
업다운은 규칙을 뒤집었다. 처음에는 흔히 하는 대로 「맞히면 탈락」으로 만들었다. 그런데 맞히면 지는 규칙이면 아무도 맞히려 하지 않는다. 모두 범위 끝 숫자만 불러서 판이 끝나지 않았다. 그래서 「맞히면 탈출」로 뒤집었다. 맞힌 사람은 빠지고, 먼저 탈출할수록 등수가 높고, 끝까지 못 맞힌 사람이 벌칙이다. 맞혀야 이득이니 범위가 계속 좁아진다.
범위도 사람 수에 맞췄다. 남은 사람이 적을수록 판이 빨리 끝나야 덜 지루하다.
const int RangePerPlayer = 10; // 범위 = 남은 사람 수 x 10
const int MinRange = 20;
const int MaxRange = 80;
// ...
_hi = Mathf.Clamp(AliveCount * RangePerPlayer, MinRange, MaxRange);
_secret = Random.Range(_lo, _hi + 1);
제일 작은 숫자는 제비로 끊는다. 겹친 사람끼리 다시 고르는데, 두 명이 남아 서로 1만 고르면 영원히 겹친다. 세 번 연속 모두 겹치면 "세 번 연속 모두 겹쳤어요. 제비로 정합니다"로 끝낸다.
31 게임은 탈락 숫자를 판마다 바꾼다. 13개씩 이어 부르다 31을 부르면 지는 게임인데, 두 명만 남으면 필승법이 생겨 외워 둔 사람이 유리하다. 그래서 판마다 2131 중 하나를 탈락 숫자로 무작위로 정한다. 숫자는 화면에 공개되지만 판마다 달라지니 미리 외워 둘 수 없다.
시간을 넘기면 앱이 대신 정한다. 한 사람이 고민하느라 판이 멈추지 않게 게임마다 기본 처리를 두었다. 31 게임은 6초를 넘기면 1개만 부른 것으로 친다. 업다운은 10초를 넘기면 범위 안에서 무작위로 부른다. 제일 작은 숫자(같이 하기)는 20초, 제비뽑기(같이 하기)는 15초 안에 안 고르면 무작위다.
긴장감은 시간으로 조절한다
서바이벌형 게임은 처음에 여유를 주고 점점 조인다.
- 369: 제한시간 3.0초에서 턴마다 0.06초씩 줄어 최소 1.3초. 33처럼 짝을 두 번 이상 쳐야 하면 한 번마다 0.7초를 더 준다.
- 바니바니: 3.0초에서 넘길 때마다 0.05초씩 줄어 최소 1.2초. 탈락이 나오면 다시 넉넉해진다.
- 순서 외우기: 3개부터 시작해 라운드마다 하나씩 길어진다.
- 딱 멈춰!: 3라운드, 라운드마다 바늘이 빨라진다.
- 폭탄 돌리기: 심지는 20~40초 사이 비밀이고, 받은 뒤 1초는 넘길 수 없다(「뜨거워! 잠깐만...」). 폭탄을 든 시간이 짧을수록 높은 등수다.
모든 게임은 1등부터 꼴등까지 한 줄을 낸다
이 앱은 꼴등 한 명만 정하지 않는다. 등수별로 커피값을 나누는 공식(5편)에 넣으려면 모든 게임이 1등부터 꼴등까지 동점 없는 한 줄을 내야 한다. 게임을 추가할 때마다 이 부분을 따로 정했다.
서바이벌형(369, 바니바니, 31 게임, 순서 외우기)은 탈락 순서가 곧 등수다. 마지막 생존자가 1등, 가장 먼저 탈락한 사람이 꼴등이다. 업다운은 반대로 탈출 순서가 등수다. 제일 작은 숫자는 안 겹친 수를 작은 수부터 확정하고 겹친 사람끼리 다시 골라 남은 등수를 채운다.
점수형은 동점 규칙이 필요했다.
| 게임 | 순서 | 같을 때 |
|---|---|---|
| 딱 멈춰! | 3라운드 총점 | 한 라운드 최고 점수 |
| 초성 퀴즈 | 맞힌 수 | 맞힌 시간 합이 짧은 쪽 |
| 연타 대결 | 누른 횟수 | 무작위 |
| 끝까지 버텨! | 버틴 시간 (펑은 맨 아래) | 펑끼리는 무작위 |
끝까지 버텨!의 정렬은 이렇게 생겼다. TieKey는 판을 만들 때 참가자마다 뽑아 둔 무작위 값이다(연타 대결도 같은 방식).
var ranked = _entries
.OrderBy(e => e.Boomed ? 1 : 0)
.ThenByDescending(e => e.Boomed ? 0f : e.Held)
.ThenBy(e => e.TieKey)
// ...
초성 퀴즈는 정답 판정도 넓혀 두었다. 출제 단어와 달라도 초성이 같고 내장 사전(8개 분류 160단어)에 있는 단어면 정답이다. 같은 초성의 다른 단어를 떠올린 사람도 점수를 받는다.
손에 맞추는 작은 규칙들
규칙 문서에 적기는 애매하지만 해 보면 차이가 나는 것들이다.
- 순서 외우기: 네 칸에 색만이 아니라 캐릭터와 음(도·미·솔·도)을 달았다. 색만으로 칸을 구분하지 않게 하려는 것이다. 틀리면 원래 눌러야 했던 칸을 빛으로 알려 준다.
- 딱 멈춰!·연타 대결: 버튼을
PointerDown으로 받는다. 손을 뗄 때가 아니라 닿는 순간 반응하고, 연타 대결은 손가락마다 세서 양손 엄지로 눌러도 된다. - 제일 작은 숫자: 숫자를 누른 뒤 「이걸로!」를 눌러야 확정된다. 잘못 눌러도 바꿀 수 있다.
- 바니바니: 방금 넘겨준 사람에게 되돌리면 탈락인데, 그 사람 칸에 「되받기 금지」를 표시해 준다.
상표는 쓰지 않는다
31 게임은 흔히 「베스킨라빈스 31」로 부른다. 하지만 상표라 앱 안에서는 「31 게임」으로만 쓴다. Play 스토어 출시 노트를 관리하는 파일에도 "상표명 금지"를 규칙으로 적어 두었다. 스토어에서 상표권 신고를 받을 수 있어서다. 출시 노트의 새 게임 소개도 「31 게임 — 숫자를 1~3개씩 이어 부르다 탈락 숫자를 부르면 탈락」으로 앱 안 이름과 똑같이 썼다.
정리
- 게임은 후보 목록을 넓게 뽑아 골랐고, 운·순발력·말놀이·숫자·머리가 섞이게 했다.
- 폰 하나 모드에서는 앞사람을 보고 따라 할 수 없게 사람마다 순서·목표를 바꾸고, 몰래 고르는 게임은 넘기기 화면으로 가렸다.
- 같이 하기에서 비밀은 방장 폰만 알고 방장 화면에도 안 보인다. 공개 전 값은 메시지에 넣지 않는다. 연출도 비밀과 무관하게 만든다.
- 판이 끝나지 않는 규칙은 뒤집거나(업다운 「맞히면 탈출」) 끊는 장치(세 번 겹치면 제비, 시간 초과 자동 처리)를 넣었다. 31 게임은 판마다 탈락 숫자를 바꿔 필승법을 막았다.
- 모든 게임은 동점 규칙까지 정해 1등부터 꼴등까지 한 줄을 낸다. 그래야 분담 공식에 넣을 수 있다.
- 상표명은 앱 안에서도 스토어에서도 쓰지 않는다.
다음 글 9편 (10월 6일 공개)에서는 Android AAB 빌드부터 Play 스토어 비공개 테스트까지를 다룬다.