9월 한 달 동안 「학교가는 길」의 등굣길에는 송편이 떨어져 있습니다. 주워 모아 윷을
던지고, 윷판을 한 바퀴 돌면 한복, 두 바퀴면 복주머니가 열립니다. 이벤트 자체는 공지에
적혀 있으니, 여기엔 만들면서 정한 것과 틀린 것을 적습니다.
한 바퀴를 돌면 열리는 한복 — 보름달 아래에서
지켜야 했던 것 셋
이 게임의 순위표는 서버가 판을 다시 돌려 검증합니다(이 글).
그래서 이벤트를 얹을 때 절대 건드리면 안 되는 것이 있었습니다.
송편은 별도의 난수 줄기에서 뽑는다. 트랙을 만드는 난수를 한 번이라도 더
당기면 물웅덩이와 강아지 자리가 통째로 밀립니다 — 실제로 한 번 그랬습니다. 그래서 송편
전용 난수 줄기를 따로 두고, 트랙의 지문이 이전과 한 글자도 안 바뀐 것을 확인했습니다.
점수·에너지·판정에 영향이 없다. 송편을 주워도 점수는 안 오릅니다. 그래서
검증 서버는 송편을 몰라도 되고, 이벤트를 안 해도 순위표에서 손해가 없습니다.
"오늘이 며칠인가"는 규칙 밖에서 판단한다. 판정 코드에 날짜가 들어가면
서버와 클라이언트가 다른 날에 다른 트랙을 만들 여지가 생깁니다. 기간 판정은 그 바깥에서
하고, 개수만 계산해 넘겨 줍니다.
송편은 물웅덩이나 금 간 블록 위에는 놓이지 않고, 뛰어넘는 중에도 주워집니다. 한 판에
왕초보 10개, 초보 15개, 일반과 일일 도전 20개. 열 개로 한 번 던집니다.
윷판은 전통 그대로, 지름길까지
바깥 20자리, 대각선 8자리, 가운데 방 1자리 — 29자리 전통 윷판입니다. 지름길은
정확히 그 자리에 멈췄을 때만 열립니다. 지나쳐 가면 안 열립니다. 이게 윷놀이를
윷놀이답게 만드는 규칙이라 그대로 옮겼습니다. 첫 모서리에 서면 대각선으로 방을 지나
맞은편으로(10칸이 6칸), 꺾은 모서리에 서면 대각선으로 방을 지나 도착으로, 방에 서면 도착 쪽
대각선으로.
그래서 가운데 방은 이름이 둘입니다. 그리기는 같은 자리지만, 어느 대각선을
타고 들어왔느냐에 따라 그냥 지나칠 때 나가는 쪽이 다릅니다. 한 이름으로 두면 어느 쪽에서
왔는지를 잃어버려 규칙이 무너집니다. 윷·모가 나오면 한 번 더 — 그 한 번은 송편을 안 씁니다.
도착은 정확히 맞출 필요가 없습니다. 말이 하나뿐인 혼자 놀이에서 딱 맞아야 한다고 하면 마지막
한 칸을 못 채워 몇 판씩 헛돕니다. 뒷도·잡기·업기는 없습니다 — 잡을 상대가 없습니다.
한편 지름길로 도착해도 "한 바퀴"로 칩니다. 짧게 가나 길게 가나 도착하면 한 바퀴 — 전통
규칙 그대로인데, "다 안 돌았는데 완주됐다"로 읽혀 버그 신고를 받았습니다. 다음 이벤트에서는
이 규칙을 화면에 더 분명히 써 둬야겠습니다.
말이 거꾸로 돌고 있었다
공개 며칠 뒤 "윷놀이가 반대 방향으로 출발한다"는 신고를 받았습니다. 코드 주석은
처음부터 "시계 반대 방향"이라고 적혀 있었는데, 실제 좌표는 시계 방향으로 그려지고 있었습니다.
이동 순서(어느 자리 다음이 어느 자리인가)는 원래도 맞았고, 그리는 위치만 틀렸습니다. 고치는
법은 좌표를 x와 y로 맞바꾸는 것뿐이었습니다 — 출발점과 맞은편 모서리는 대각선 위에 있어
그대로 있고, 나머지가 그 대각선을 기준으로 뒤집혀 방향이 반대가 됩니다. 지름길 연결도 좌표만
따라가므로 자동으로 맞게 옮겨졌습니다.
같은 신고에 "출발점이 안 보인다"도 있었습니다. 출발점이 다른 세 모서리와 똑같은 금색
큰 원이라 구분이 안 됐던 겁니다. 출발점만 초록 테두리로 다르게 칠했더니, 이번엔 "초록 자리에
있었는데 던지자마자 대각선으로 튀었다"가 왔습니다. 말이 실제로는 다른 모서리에 있었는데,
항상 켜져 있는 초록 표시를 자기 위치로 본 겁니다. 그래서 지금은 내 말을 판
위 어디에도 없는 색(주황, 퍼지는 테두리)으로 따로 그립니다. 던질 때는 네 짝이 0.4초쯤
빠르게 뒤집히는 손맛도 넣었습니다 — 말이 기어가는 연출은 여러 번 던질 때 지루해져서 일부러
뺐지만, 이건 짧아서 그 문제를 다시 불러오지 않습니다.
쓴 송편이 되살아나던 버그
가장 배울 게 많았던 버그입니다. "송편을 다 썼는데 오후에 다시 들어오니 그대로였다."
원인은 언제 서버에 올리느냐였습니다. 계정 진행은 게임이 켜질 때마다 로컬값과
서버값 중 "더 앞선 쪽"으로 맞춰지는데, 윷을 던진 결과는 한복 같은 보상을 새로 받았을 때만
서버에 올리고 있었습니다. 바퀴를 못 채우고 송편만 쓴 던지기는 로컬에만 남습니다. 다음에 다시
열면 서버의 옛(더 많은 송편) 값이 "더 앞선 쪽"으로 뽑혀 방금 쓴 송편이 되살아납니다.
고치는 것 자체는 한 줄입니다 — 매 던지기마다 무조건 올린다. 교훈은 더 큽니다. "큰 쪽이
이긴다"는 합치기 규칙은 거리나 기록처럼 늘기만 하는 값에는 안전하지만, 송편처럼
쓰면 줄어드는 값에는 그 값이 바뀌는 모든 지점에서 즉시 올려야 합니다. "보상을
받을 때만 올린다" 식으로 조건을 걸면 이 버그가 재발합니다.
그리고 하나 더. 이 버그를 쫓는 동안 테스트한다고 계정의 서버 값을 직접 몇 번 고쳤는데,
그때마다 브라우저가 자기 값으로 도로 덮어써서 "안 먹혔다"는 신고가 반복됐고, 나중엔 무엇이
진짜 버그이고 무엇이 제 수정의 부작용인지 가릴 수 없게 됐습니다. 라이브 데이터는
손대지 않고, 시크릿 창으로 테스트한다 — 이것도 이번에 세운 규칙입니다.
Through September, songpyeon rice cakes lie along the road in The Way to School. Collect
them, throw yut sticks, and one lap of the yut board unlocks the hanbok, two laps the lucky
pouch. The event itself is described in the news; this post is about what we decided
while building it, and what we got wrong.
The hanbok, unlocked after one lap — under the harvest moon
Three things that could not change
This game's leaderboard is verified by replaying runs on the server (see
this post). That set hard limits on adding an event.
Songpyeon come from a separate random stream. Pull the track's random
generator even once more and every puddle and dog shifts — it happened to us once. So
songpyeon get their own stream, and we confirmed the track's fingerprint was unchanged to
the character.
No effect on score, energy or judgement. Picking up songpyeon adds no
points. The verification server doesn't need to know they exist, and skipping the event
costs nothing on the board.
"What day is it" is decided outside the rules. Put the date inside the
judgement code and server and client can build different tracks on different days. The
date check lives outside and only passes in a count.
Songpyeon are never placed on puddles or cracked tiles, and you collect them mid-jump. Per
run: 10 on Very Easy, 15 on Easy, 20 on Normal and Daily Challenge. Ten buys one throw.
A traditional board, shortcuts included
Twenty spots around the outside, eight on the diagonals, one in the centre — the classic
29-spot yut board. A shortcut opens only when you stop exactly on it; pass over
it and nothing happens. That rule is what makes yut feel like yut, so it was kept. Stop on the
first corner and you cut diagonally through the centre to the far side (ten squares become
six); stop on the turning corner and you cut through the centre to the finish; stop in the
centre and you take the diagonal to the finish.
That is why the centre has two names. It's drawn once, but which diagonal you
entered by decides which way you leave if you merely pass through. One name would forget
where you came from and the rule would collapse. Roll Yut or Mo and you throw again — that
throw is free. Reaching the finish doesn't need an exact count; in a solo game with a single
piece, exact landing means spinning on the last square for rounds. No back-do, no capturing,
no stacking — there's no opponent to capture.
Arriving by shortcut counts as a lap — the traditional rule, short way or long. But it read
as "I didn't go all the way round and it counted", and we got a bug report. Next time the rule
goes on screen.
The piece was going around backwards
Days after launch: "the yut game sets off in the wrong direction". The code comment had said
"counter-clockwise" from day one; the coordinates drew it clockwise. The movement order (which
spot follows which) had always been right — only the drawn positions were wrong. The fix was
to swap x and y for every spot: the start and the opposite corner sit on the diagonal and stay
put, everything else reflects across it, and the direction flips. The shortcut links follow
the coordinates, so they came along correctly by themselves.
The same report said "I can't see the start". The start was a big gold circle exactly like
the other three corners. We gave it a green ring — and the next report was "it was on the green
spot and jumped diagonally the moment I threw". The piece had actually been on a different
corner; the always-on green marker had been read as "my position". So now your
piece is drawn in a colour that appears nowhere else on the board (orange, with a
spreading ring). We also added a 0.4-second tumble of the four sticks on each throw — we had
deliberately left out a piece-crawling animation because it gets tedious over many throws,
and this is short enough not to bring that back.
The bug that resurrected spent songpyeon
The most instructive one. "I used up my songpyeon, came back in the afternoon, and they were
all there again." The cause was when we uploaded to the server. Account
progress syncs to "whichever side is further ahead" every time the game starts, but a throw's
result was only uploaded when it newly unlocked a reward like the hanbok. A throw that spent
songpyeon without completing a lap stayed local only. Next launch, the server's old (higher
songpyeon) value won as "further ahead", and the songpyeon you'd just spent came back.
The fix is one line — upload after every throw. The lesson is bigger. A "bigger wins" merge
is safe for values that only grow, like distance or best score, but a value that
goes down when spent must be uploaded at every point it changes. Any condition
like "only when a reward is earned" reintroduces this bug.
One more. While chasing it, we edited the account's server value directly a few times "for
testing" — each time the browser overwrote it with its own value, "it didn't work" came back,
and eventually we could no longer tell real bugs from side effects of our own edits.
Don't touch live data; test in a private window is a rule we wrote down this
week.