데이터/측정 • Koal & KoalStudio

같은 선택은 같은 인생이어야 한다 — 결정론으로 게임과 서버를 함께 검증하기

Called Up은 결과 수치 대신 시드와 초기 조건, 선택 기록을 정본 입력으로 삼고 서버에서 커리어를 다시 실행해요. 난수 흐름 분리와 화면 비의존 코어가 재현, 공유, QA, 저장 검증을 잇는 방식을 다룹니다.

Called Up에서 한 선수의 인생은 드래프트, 부진, 이적, 콜업, 국가대표 같은 갈림길의 선택으로 만들어져요. 같은 출발점에서 같은 선택을 다시 했는데 다른 커리어가 나온다면 플레이어는 선택의 결과를 믿기 어려워져요. 공유받은 커리어를 재현할 수 없고, 서버가 브라우저가 보낸 통산 기록을 그대로 저장해도 같은 문제가 생겨요.

같은 출발점에서 같은 선택을 했다면, 같은 인생이 나와야 해요.

이 약속을 지키려면 난수를 고정하는 것만으로는 부족해요. 무엇을 커리어의 정본 입력으로 볼지, 서로 다른 시스템의 난수 흐름을 어떻게 나눌지, 화면 없이도 같은 규칙을 실행할 수 있을지, 서버가 제출된 커리어를 어떻게 다시 실행할지까지 함께 설계해야 해요.


결과가 아니라 인생의 재료를 정본 입력으로 둬요

커리어가 끝나면 타율, 홈런, 승수, 수상, 소속팀, 연봉 같은 결과가 많이 남아요. 가장 쉬운 저장 방식은 이 값을 브라우저에서 모아 서버로 보내는 거예요. 하지만 브라우저는 플레이어의 기기에서 실행돼요. 값이 바뀌거나 오래된 코드가 잘못된 결과를 보내거나 요청이 조작될 가능성을 서버가 배제할 수 없어요.

Called Up은 결과 대신 커리어를 만들어낸 입력을 정본으로 봐요.

seed + mode + setup + choice log
  • seed(시드)는 그 커리어의 난수 출발점이에요.
  • mode는 선택이 얼마나 촘촘하게 등장하는지 같은 플레이 조건이에요.
  • setup은 선수의 초기 설정이며, 서버가 허용한 항목만 받아요.
  • choice log는 각 사건에서 어떤 선택을 했는지 식별할 수 있게 남긴 기록이에요.

이후에는 이 묶음을 정본 입력이라고 부를게요. 선택 기록은 “공격적으로 플레이했다” 같은 해석문이 아니라 결과를 다시 만들 수 있는 조작 순서예요. 정본 입력이 같으면 결과도 같아야 하지만, 결과 수치가 같다고 같은 커리어인 것은 아니에요. 서로 다른 선택 경로가 우연히 같은 홈런 수에 도달할 수 있으니까요.

신뢰 경계는 다음처럼 정리돼요.

브라우저가 주장한 결과  → 정답으로 채택하지 않음
정본 입력               → 서버가 다시 실행
서버가 계산한 결과       → 저장과 검증의 기준

결과를 저장하기 전에 그 결과가 만들어지는 과정을 재실행할 수 있어야 해요.


시드 하나가 아니라 난수 흐름을 분리해요

한 선수의 커리어 안에서도 성장, 출전 시간, 계약, 이적, 콜업, 포스트시즌, 국가대표, 수상처럼 서로 다른 시스템이 난수를 써요. 모든 시스템이 난수 한 줄을 차례로 소비하면 중간에 새 기능 하나가 난수를 한 번 더 뽑는 순간 뒤의 순서가 전부 밀려요. 새 기능과 무관한 계약이나 수상 결과까지 달라질 수 있어요.

Called Up은 목적이 표시된 하위 시드(labeled sub-seed)를 사용해 난수 흐름을 나눠요. 같은 뿌리 시드에서 출발하되 이름 생성, 선수 진행, 리그 이동처럼 서로 다른 관심사가 각자의 이름으로 흐름을 파생해요.

뿌리 시드
  ├─ 선수 진행
  ├─ 이름 생성
  ├─ 압박 상황
  └─ 시스템별 흐름

중요한 것은 하위 시드의 영문 이름이 아니라 난수 흐름 분리예요. 리그 이동에 새 무작위 판단이 추가돼도 다른 시스템의 난수 순서를 밀어내지 않게 해요. 새 기능이 자기 구역의 확률은 바꿀 수 있지만 무관한 구역의 역사까지 덤으로 흔들어서는 안 돼요.

이 구조가 모든 변경에서 옛 결과를 보존하는 것은 아니에요. 계약 계산식 자체를 고치면 계약 결과는 달라질 수 있어요. 난수 흐름 분리는 의도한 규칙 변경을 막는 장치가 아니라, 한 구역의 변경이 다른 구역의 난수 소비 순서까지 우연히 바꾸는 일을 줄이는 장치예요.


화면 비의존 코어를 한 번만 구현해요

정본 입력이 있어도 브라우저와 서버가 서로 다른 실행기를 쓰면 검증은 금방 어긋나요. 한쪽 규칙이 먼저 바뀌거나 테스트가 세 번째 구현을 흉내 내기 시작하기 때문이에요.

Called Up의 시뮬레이션 코어는 DOM에 의존하지 않아요. DOM-free라는 구현 표현보다 화면 비의존 코어라는 역할이 더 중요해요. 버튼, 카드, 애니메이션, 렌더링 타이밍을 몰라도 선수 생성부터 시즌 진행과 은퇴까지 계산할 수 있어요. 화면은 정본 입력을 전달하고 코어가 만든 상태를 보여줘요.

하나의 규칙을 세 자리에서 실행할 수 있어요.

  1. UI는 플레이어의 선택을 입력으로 전달하고 결과를 표현해요.
  2. 서버는 저장 요청의 정본 입력으로 같은 커리어를 다시 계산해요.
  3. 테스트는 화면을 띄우지 않고 완주, 재현성, 경제와 출전 시간 같은 불변식을 검사해요.

화면 비의존 코어는 단지 코드를 깔끔하게 나누기 위한 선택이 아니에요. 서버와 테스트가 브라우저와 같은 규칙을 실행하기 위한 경계예요. 화면을 고쳐도 시뮬레이션 규칙은 그대로 둘 수 있고, 테스트가 클릭 속도나 렌더링 타이밍 때문에 흔들리는 일도 피할 수 있어요.


서버는 결과를 검사하는 대신 커리어를 다시 실행해요

저장 요청이 오면 서버는 클라이언트가 보낸 통산 기록을 정답으로 채택하지 않아요. server replay라는 표현을 쓸 수 있지만, 여기서는 동작이 드러나는 서버 재실행이라고 부를게요.

플레이어 선택
  → 브라우저가 사건·선택 기록 생성
  → 정본 입력 전송
  → 서버가 입력 형식과 허용 범위 확인
  → 화면 비의존 코어로 커리어 재실행
  → 완주 여부와 서버 계산 결과 확인
  → 검증을 통과한 서버 결과만 저장

클라이언트가 비교용 요약을 함께 보내더라도 최종 기준은 서버가 재실행해 얻은 요약이에요. 둘이 다르면 저장을 거부해요. 은퇴까지 끝나지 않은 커리어를 완료된 기록처럼 저장하는 것도 막아요. 초기 설정의 허용 범위, 선택 기록의 형식과 길이에도 경계를 둬서 서버 재실행이 무제한 작업이 되지 않게 해요.

이 구조는 부정행위만 다루지 않아요.

  • 오래된 클라이언트가 잘못된 결과를 만들면 서버에서 불일치를 발견할 수 있어요.
  • 시뮬레이션 변경이 저장 경로를 깨뜨리면 저장 검증 테스트에서 드러나요.
  • 업적과 커리어 기록이 같은 서버 결과를 기준으로 움직여요.
  • 오류가 생기면 결과 숫자만 보는 대신 정본 입력을 다시 실행할 수 있어요.

서버는 숫자가 그럴듯한지 추정하지 않아요. 이 입력으로 정말 이 커리어가 나오는지를 제품 규칙 자체로 계산해요.


공유는 같은 인생과 다른 인생을 구분해요

은퇴 카드와 공유 링크에는 서로 다른 재현 목적이 있어요.

  • 같은 커리어 재현: 같은 정본 입력으로 동일한 결과를 복원해요.
  • 같은 출발점에서 재도전: 시드와 초기 조건을 이어받되 이후 선택을 바꿔 다른 인생을 만들어요.

첫 번째는 “그 결과가 다시 나오는가”의 문제이고, 두 번째는 “다른 결정을 내렸다면 어떻게 됐을까”의 문제예요. 시드만 URL에 넣는다고 둘이 자동으로 구분되지는 않아요. 플레이 조건과 초기 설정, 이후 선택도 결과를 함께 만들기 때문이에요.

버그 재현도 같은 정본 입력을 써요. “몇 시즌쯤 이상해졌어요”라는 설명만 받는 대신 입력이 남아 있으면 개발자가 같은 커리어를 같은 순서로 실행할 수 있어요. 공유와 QA가 서로 다른 재현 장치를 만들 필요가 없어요.


테스트는 동일 실행뿐 아니라 경계도 확인해요

기본 테스트는 같은 정본 입력을 두 번 실행해 결과 전체가 같은지 비교해요. 그것만으로 저장 경로와 게임 규칙이 안전하다고 말할 수는 없어요. Called Up에서는 역할이 다른 검증을 함께 둬요.

  • 완주 검증은 커리어가 막히지 않고 은퇴까지 도달하는지 봐요.
  • 재현 검증은 같은 정본 입력이 같은 전체 결과를 만드는지 봐요.
  • 선택 결과 검증은 선택에 선언된 효과와 실제 적용이 맞는지 봐요.
  • 서버 재실행 검증은 입력 검사, 재계산, 불일치 거부가 함께 작동하는지 봐요.
  • 도메인 검증은 계약, 경제, 출전 시간 같은 규칙이 허용 범위 안에 있는지 봐요.

실패한 커리어를 고정된 정본 입력으로 다시 부를 수 있어야 원인을 좁히고 수정 전후를 비교할 수 있어요. 시드는 무작위 시뮬레이션의 실패를 다시 조사할 수 있는 사례로 바꿔줘요.


결정론이 증명하지 못하는 것도 남겨둬요

같은 입력이 같은 결과를 만든다고 해서 그 결과가 재미있거나 공정한 것은 아니에요. 버그도 완벽하게 재현될 수 있어요. 결정론이 직접 확인하는 것은 같은 입력과 출력의 재현, 코드 변경 뒤의 차이 감지, 클라이언트 입력을 서버가 같은 규칙으로 검증할 수 있는지까지예요.

선택지가 흥미로운지, 특정 리그나 포지션이 구조적으로 유리한지, 결과 분포가 현실감과 게임성을 함께 만족하는지, 한 커리어의 감정적 리듬이 좋은지는 별도 검증이 필요해요. 분포 테스트와 도메인 불변식이 일부를 맡을 수 있지만 실제 플레이와 사람의 판단도 필요해요.

버전 변화도 따로 다뤄야 해요. 게임 규칙을 의도적으로 바꾸면 옛 정본 입력이 새 규칙에서 다른 결과를 낼 수 있어요. 난수 흐름 분리는 무관한 변화의 파급을 줄일 뿐 규칙 변경의 의미까지 지우지 않아요. 오래된 커리어를 어느 규칙 버전으로 재실행할지, 어떤 호환성을 보존할지는 별도의 제품 정책으로 남아요.

댓글

댓글 남기기

댓글 삭제 시 사용됩니다
공개되지 않습니다
0/2000
← 목록으로