feat: expand experimental online multiplayer
This commit is contained in:
@@ -32,7 +32,16 @@ npm install
|
||||
|
||||
## 개발 서버 실행
|
||||
|
||||
실시간 대기실을 사용하므로 터미널 두 개를 연다.
|
||||
가장 간단한 방법은 프로젝트 폴더에서 다음 명령 하나를 실행하는 것이다.
|
||||
|
||||
```bash
|
||||
npm run dev:all
|
||||
```
|
||||
|
||||
이 명령은 Vue 개발 서버와 Socket.IO 서버를 동시에 실행한다. 화면에 표시되는
|
||||
Vite의 `Local` 주소를 브라우저에서 연다.
|
||||
|
||||
각 서버 로그를 별도 터미널에서 보고 싶다면 터미널 두 개를 연다.
|
||||
|
||||
첫 번째 터미널에서 Socket.IO 서버를 실행한다.
|
||||
|
||||
@@ -57,6 +66,12 @@ Local: http://localhost:5173/
|
||||
|
||||
Vite는 브라우저의 `/socket.io` 요청을 로컬 `31873` 포트로 전달한다. 두
|
||||
개발 서버를 종료하려면 각각의 터미널에서 `Control + C`를 누른다.
|
||||
`npm run dev:all`을 사용했다면 해당 터미널에서 `Control + C`를 한 번 누르면
|
||||
두 서버가 함께 종료된다.
|
||||
|
||||
- Vue 파일 변경은 자동으로 화면에 반영된다.
|
||||
- `server/` 파일을 변경한 경우에는 `Control + C`로 종료한 뒤
|
||||
`npm run dev:all`을 다시 실행한다.
|
||||
|
||||
## 같은 네트워크의 다른 기기에서 테스트
|
||||
|
||||
@@ -105,7 +120,7 @@ VITE_CARD_ASSET_BASE_URL=http://playuniare.sori.studio/card-assets
|
||||
```bash
|
||||
git pull
|
||||
npm install
|
||||
npm run dev
|
||||
npm run dev:all
|
||||
```
|
||||
|
||||
작업이 끝난 뒤:
|
||||
@@ -142,11 +157,24 @@ SOCKET_SERVER_PORT=31874 npm run dev
|
||||
### 실시간 서버에 연결되지 않는 경우
|
||||
|
||||
- `npm run dev:server` 터미널이 실행 중인지 확인한다.
|
||||
- 명령 하나로 실행하려면 기존 개발 서버를 모두 종료한 뒤
|
||||
`npm run dev:all`을 사용한다.
|
||||
- 서버 터미널에 `listening on port 31873` 메시지가 있는지 확인한다.
|
||||
- 포트를 바꿨다면 Vue와 Socket.IO 서버를 같은 `SOCKET_SERVER_PORT`로
|
||||
실행했는지 확인한다.
|
||||
- 브라우저 개발자 도구 Network 탭에서 `/socket.io` 요청을 확인한다.
|
||||
|
||||
### `EMFILE: too many open files`가 나오는 경우
|
||||
|
||||
이 프로젝트의 기본 `dev:server`는 파일 감시를 사용하지 않으므로 최신
|
||||
`package.json`에서는 발생하지 않아야 한다. 기존 터미널을 모두 종료하고
|
||||
다음 명령을 다시 실행한다.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev:all
|
||||
```
|
||||
|
||||
### 패키지 오류가 발생하는 경우
|
||||
|
||||
먼저 다음을 다시 실행한다.
|
||||
|
||||
@@ -85,3 +85,58 @@
|
||||
- Socket.IO 포트는 NAS 호스트에 직접 공개하지 않는다.
|
||||
- 현재 서버 재시작 시 방이 사라지는 단순한 구조를 유지하고, 게임 상태
|
||||
동기화와 재접속 복구는 후속 단계에서 구현한다.
|
||||
|
||||
## 2026-06-19: 온라인 선후공·턴·공개 카드 동기화
|
||||
|
||||
- 선후공은 클라이언트 선택이 아니라 서버가 무작위로 한 명만 지정한다.
|
||||
- 양쪽 초기 손패 확정 후 선공부터 현재 차례를 시작한다.
|
||||
- 현재 차례인 플레이어만 서버에 턴 종료를 요청할 수 있다.
|
||||
- 플레이어는 자신의 공개 카드 상태만 제출하며 서버는 상대 상태를 직접
|
||||
수정하는 요청을 제공하지 않는다.
|
||||
- 손패와 덱은 장수만 공유하고 카드 식별 정보는 서버 공개 상태에서 제외한다.
|
||||
- 구현 복잡도를 낮추기 위해 현재는 카드별 이벤트 대신 플레이어별 공개 상태
|
||||
스냅샷을 전송한다. 재접속과 순서 보장은 후속 단계에서 보강한다.
|
||||
|
||||
## 2026-06-19: 손패 트레이와 단기 재접속
|
||||
|
||||
- 상하 3행 플레이매트 구조를 유지하고 좌우 회전 배치는 채택하지 않았다.
|
||||
- 하단 Action Point 영역을 손패가 덮지 않도록 플레이매트 밖에 전용 손패
|
||||
트레이를 추가했다.
|
||||
- 손패 카드는 축소해 한 줄로 표시하고 넘치는 카드는 가로 스크롤로 확인한다.
|
||||
- Socket.IO 소켓 ID와 별개인 플레이어 식별자와 복구 토큰을 도입했다.
|
||||
- 연결 종료 즉시 방에서 제거하지 않고 60초 동안 같은 좌석의 재접속을
|
||||
허용한다.
|
||||
- 비공개 덱·손패와 카드 위치는 플레이어별 브라우저 저장소에 보관해
|
||||
새로고침 후 복구한다.
|
||||
- 서버는 상대에게 공개할 카드만 보관하며 개인 손패 원본은 받지 않는 구조를
|
||||
유지한다.
|
||||
|
||||
## 2026-06-19: 로컬 통합 개발 명령
|
||||
|
||||
- Vue만 실행했을 때 발생하는 Socket.IO 연결 오류를 줄이기 위해
|
||||
`concurrently`를 개발 의존성으로 추가했다.
|
||||
- `npm run dev:all` 명령 하나로 Vite와 Socket.IO 개발 서버를 함께 실행한다.
|
||||
|
||||
## 2026-06-19: 방 종료와 진행 중 게임 복귀
|
||||
|
||||
- 방 상태가 꼬여도 사용자가 직접 초기화할 수 있도록 대기실과 게임 화면에
|
||||
`방 나가기`를 추가했다.
|
||||
- 한 플레이어가 명시적으로 방을 나가면 방 전체를 종료하고 상대에게 종료
|
||||
사유를 전달한다.
|
||||
- 대기실에서 기존 방을 종료하고 즉시 새 방을 만드는 동선을 추가했다.
|
||||
- 진행 중 게임 세션으로 재접속하면 서버가 덱 쿼리와 좌석을 다시 전달해
|
||||
게임 화면으로 자동 복귀시킨다.
|
||||
- 명시적 종료와 서버의 방 종료 알림에서는 브라우저 재접속 정보와 개인 게임
|
||||
스냅샷을 함께 삭제한다.
|
||||
|
||||
## 2026-06-22: 서버 권위 비공개 덱 순서
|
||||
|
||||
- 서버는 카드 내용 대신 플레이어별 카드 인스턴스 식별자 순서만 보관한다.
|
||||
- 온라인 셔플은 Node.js 암호학적 난수를 사용하고 결과 순서를 해당
|
||||
플레이어에게만 반환한다.
|
||||
- 드로우, 여러 장 공개와 라이프 배치는 서버 비공개 덱의 맨 위 식별자를
|
||||
제거한 결과로 처리한다.
|
||||
- 덱 위·아래 이동은 서버 순서에도 반영해 화면 장수와 실제 드로우 결과가
|
||||
어긋나지 않도록 했다.
|
||||
- 공개 카드 상태에는 단조 증가 리비전을 추가해 지연된 이전 상태가 최신
|
||||
상태를 덮지 못하게 했다.
|
||||
|
||||
18
docs/map.md
18
docs/map.md
@@ -15,6 +15,9 @@
|
||||
- 서버 기반 방 생성·방 코드 참가
|
||||
- 양쪽 플레이어의 좌석·덱 등록·준비 상태
|
||||
- 양쪽 준비 완료 시 게임 화면 자동 이동
|
||||
- 방 나가기
|
||||
- 현재 방 종료 후 새 방 만들기
|
||||
- 진행 중 게임 세션 발견 시 게임 화면 자동 복귀
|
||||
- 유효한 덱으로 로컬 게임 테스트
|
||||
|
||||
### `/game`
|
||||
@@ -33,14 +36,23 @@
|
||||
- 불러온 덱을 내 덱 영역에 뒷면으로 쌓기
|
||||
- 잘못된 덱 데이터 오류 안내
|
||||
- 내 덱 1장 드로우와 셔플
|
||||
- 온라인 서버 권위 셔플·드로우·여러 장 공개
|
||||
- 덱 위 1~10장 공개
|
||||
- 중앙 공개 영역 자동 정렬
|
||||
- 하단 손패 자동 정렬
|
||||
- 플레이매트와 분리된 화면 하단 한 줄 손패 트레이
|
||||
- 축소 손패 카드와 가로 스크롤
|
||||
- 우클릭 메뉴의 손패·덱 위·덱 아래·무덤 이동
|
||||
- 첫 손패 7장
|
||||
- 선후공 선택과 1회 멀리건
|
||||
- 로컬 선후공 선택과 1회 멀리건
|
||||
- 온라인 서버 무작위 선후공 지정
|
||||
- 라이프 7장 세팅
|
||||
- 턴 시작 AP·드로우·일괄 언탭
|
||||
- 현재 차례 강조와 서버 기반 턴 종료·교대
|
||||
- 상대 공개 카드·덱·손패·라이프 장수 실시간 표시
|
||||
- 상대 카드 반대편 좌표 표시와 조작 차단
|
||||
- 새로고침·단기 연결 끊김의 같은 좌석 재접속
|
||||
- 게임 화면의 방 나가기
|
||||
- 상대가 방을 나가면 대기실 자동 복귀
|
||||
- 카드별 자동 언탭 제외 잠금
|
||||
- 마우스·터치 포인터 기반 자유 드래그
|
||||
- 카드 더블클릭 탭·언탭
|
||||
@@ -50,7 +62,7 @@
|
||||
- 선택 카드 좌표·레이어·앞뒤·탭 상태 표시
|
||||
- 좌우 플레이어 정보 패널
|
||||
- 라이프, 덱·손패 장수, 연결 상태와 메모 자리
|
||||
- 다음 변경: 게임 화면의 카드 상태를 Socket.IO 서버와 동기화
|
||||
- 다음 변경: 카드 조작 이벤트별 세부 검증과 진행 중 표시
|
||||
- 문서 제목과 기본 언어: `index.html`의 `Play Uniare`, 한국어
|
||||
|
||||
## 예정 화면
|
||||
|
||||
43
docs/spec.md
43
docs/spec.md
@@ -84,7 +84,26 @@
|
||||
- 방 상태와 덱 등록 결과는 Socket.IO 서버가 최종 검증한다.
|
||||
- 두 플레이어가 모두 준비하면 서버가 각 플레이어에게 자신의 덱 쿼리와
|
||||
좌석을 전달하고 `/game`으로 자동 이동시킨다.
|
||||
- 서버가 게임 시작 시 두 플레이어 중 한 명을 무작위 선공으로 지정한다.
|
||||
- 온라인 게임에서는 사용자가 선공·후공을 직접 선택할 수 없다.
|
||||
- 양쪽 플레이어가 첫 손패와 라이프를 확정한 뒤 선공 플레이어의 턴부터
|
||||
시작한다.
|
||||
- 현재 차례의 플레이어만 턴 종료를 요청할 수 있으며 서버가 상대에게 차례를
|
||||
넘긴다.
|
||||
- 방은 서버 메모리에 저장하므로 서버 재시작 시 종료된다.
|
||||
- 브라우저 새로고침이나 짧은 네트워크 끊김에는 재접속 식별자로 같은 좌석을
|
||||
복구한다.
|
||||
- 내 덱 순서, 손패, 카드 위치와 준비 상태는 플레이어별 브라우저 저장소에
|
||||
보관해 새로고침 후 복구한다.
|
||||
- 개인 저장 상태는 상대 플레이어에게 전송하지 않는다.
|
||||
- 연결이 끊긴 플레이어의 자리는 60초 동안 보존하고, 그 안에 복구되지 않으면
|
||||
진행 중인 게임을 종료한다.
|
||||
- 진행 중인 게임이 있는 사용자가 대기실이나 사이트 첫 화면으로 돌아오면
|
||||
서버가 자신의 덱 쿼리와 좌석을 다시 전달해 게임 화면으로 복귀시킨다.
|
||||
- 명시적인 `방 나가기`는 방 전체를 종료하고 양쪽 플레이어의 재접속 정보와
|
||||
개인 게임 스냅샷을 삭제한다.
|
||||
- 대기실에서 `현재 방 종료 후 새 방 만들기`로 먹통이 된 방을 정리하고 즉시
|
||||
새 초대 코드를 만들 수 있다.
|
||||
- 기능 개발을 위해 유효한 덱으로 `/game`에 진입하는 로컬 게임 테스트를
|
||||
제공한다.
|
||||
|
||||
@@ -94,6 +113,9 @@
|
||||
- 상대 효과로 내 카드가 제거될 때에도 내가 직접 내 카드를 이동한다.
|
||||
- 서버는 모든 카드 조작에서 요청 사용자와 카드 소유자를 비교한다.
|
||||
- 상대 손패에는 장수만 공개하고 카드 식별 정보는 공개하지 않는다.
|
||||
- 각 플레이어는 자신의 공개 카드 상태만 서버에 제출할 수 있다.
|
||||
- 서버는 필드, 공개, 무덤, 라이프 카드만 상대에게 전달한다.
|
||||
- 덱과 손패는 카드 목록을 보내지 않고 장수만 전달한다.
|
||||
|
||||
## 화면 좌표
|
||||
|
||||
@@ -113,6 +135,9 @@
|
||||
|
||||
- 로컬 모드: 브라우저 메모리, 추후 복구가 필요하면 localStorage 검토
|
||||
- 온라인 모드: 서버가 방 상태의 최종 권위를 가진다.
|
||||
- 온라인 개인 비공개 상태: 현재 단계에서는 플레이어 브라우저 저장소에
|
||||
암호화 없이 저장한다. 공용 PC에서는 게임 후 브라우저 데이터를 정리해야
|
||||
한다.
|
||||
- 초기 서버: Node.js와 Socket.IO, 메모리 기반 방 상태
|
||||
- 운영 안정화 단계: Redis 또는 데이터베이스 저장 검토
|
||||
- 서버의 공개 방 상태를 Pinia가 수신해 화면에 표시한다.
|
||||
@@ -133,13 +158,25 @@
|
||||
- 키보드 포커스로도 카드를 선택할 수 있어야 한다.
|
||||
- 개발 기준 화면에서는 같은 카드의 독립 인스턴스 3장으로 겹침과 선택
|
||||
동작을 검증한다.
|
||||
- 온라인 게임에서 내 공개 카드는 상대 화면의 반대편 좌표로 변환해 표시한다.
|
||||
- 상대 카드는 선택, 드래그, 더블클릭, 우클릭 조작을 할 수 없다.
|
||||
- 공개 상태 전송에는 증가하는 리비전 번호를 붙이고, 서버와 상대 브라우저는
|
||||
이전 리비전이 늦게 도착하면 무시한다.
|
||||
|
||||
## 덱·손패·무덤 동작
|
||||
|
||||
- 덱 카드는 보드의 내 덱 위치에 뒷면으로 겹쳐 표시한다.
|
||||
- 온라인 게임에서 서버는 카드 내용이 아닌 인스턴스 식별자 순서만 비공개로
|
||||
관리한다.
|
||||
- 온라인 셔플은 서버의 암호학적 난수로 순서를 결정한다.
|
||||
- 온라인 드로우, 덱 위 여러 장 공개, 라이프 배치는 서버가 반환한 맨 위 카드
|
||||
식별자를 사용한다.
|
||||
- 덱 위·아래 이동은 서버의 비공개 덱 순서에도 같은 위치로 반영한다.
|
||||
- `1장 드로우`는 가장 높은 `stackOrder`의 덱 카드를 손패로 이동한다.
|
||||
- 손패 카드는 앞면으로 표시하고 보드 하단 중앙에 자동 정렬한다.
|
||||
- 손패 카드가 많아지면 제한된 폭 안에서 카드 간격을 줄인다.
|
||||
- 손패는 플레이매트 위가 아니라 화면 하단의 별도 한 줄 트레이에 표시한다.
|
||||
- 손패 카드는 필드 카드보다 작게 표시하고 장수가 많아지면 가로로 스크롤한다.
|
||||
- 플레이매트의 `ACTION POINT AREA`는 손패 카드로 가리지 않는다.
|
||||
- 손패·덱·무덤 카드를 드래그하면 필드 카드로 전환한다.
|
||||
- 셔플은 덱 카드의 레이어 순서를 무작위로 다시 배정하고 모두 뒷면·언탭으로
|
||||
정리한다.
|
||||
@@ -170,11 +207,15 @@
|
||||
- 턴 시작 시 내 필드의 탭 카드를 모두 언탭한다.
|
||||
- `isUntapLocked`가 설정된 카드는 턴 시작 자동 언탭에서 제외한다.
|
||||
- 언탭 제외 잠금은 카드 우클릭 메뉴에서 설정·해제한다.
|
||||
- 온라인 게임에서는 서버의 현재 차례가 내 플레이어로 바뀔 때 턴 시작
|
||||
언탭, AP 갱신과 드로우를 한 번 적용한다.
|
||||
|
||||
## 게임 화면
|
||||
|
||||
- 실제 게임 화면에는 고정 헤더를 표시하지 않는다.
|
||||
- 16:9 플레이 보드를 뷰포트 안에서 가능한 가장 크게 표시한다.
|
||||
- 하단 손패 트레이 높이를 제외한 영역에서 16:9 플레이 보드를 가능한 가장
|
||||
크게 표시한다.
|
||||
- 남는 공간은 검은색 레터박스로 처리한다.
|
||||
- `F` 키 또는 우측 상단의 반투명 버튼으로 브라우저 전체화면을 전환한다.
|
||||
- `Esc` 키로 브라우저 전체화면과 카드 우클릭 메뉴를 닫을 수 있다.
|
||||
|
||||
20
docs/todo.md
20
docs/todo.md
@@ -47,10 +47,20 @@
|
||||
- [x] 방 만들기·참가 대기실 UI
|
||||
- [x] 방 만들기·참가 서버 동기화
|
||||
- [x] 플레이어 좌석 할당
|
||||
- [ ] 게임 카드 플레이어 소유권
|
||||
- [ ] 카드 조작 권한 서버 검증
|
||||
- [ ] 상대 손패 비공개 전송
|
||||
- [ ] 상태 스냅샷과 재접속
|
||||
- [x] 게임 카드 플레이어 소유권 기반 상태 제출
|
||||
- [x] 상대 카드 직접 조작 차단
|
||||
- [x] 상대 손패·덱 카드 식별 정보 비공개
|
||||
- [x] 공개 카드 이동·탭·뒤집기·영역 상태 동기화
|
||||
- [x] 서버 무작위 선후공과 현재 차례 동기화
|
||||
- [ ] 카드 조작 이벤트별 세부 서버 검증
|
||||
- [x] 덱 인스턴스 순서와 셔플의 서버 권위 처리
|
||||
- [x] 서버 권위 드로우·여러 장 공개·라이프 배치
|
||||
- [x] 60초 유예 기반 같은 좌석 재접속
|
||||
- [x] 브라우저 저장소 기반 내 비공개 덱·손패 상태 재접속 복구
|
||||
- [x] 진행 중 게임의 대기실·첫 화면 접근 시 자동 복귀
|
||||
- [x] 방 나가기와 새 방 만들기 복구 동선
|
||||
- [ ] 전체 게임 상태 스냅샷 복구
|
||||
- [x] 명시적인 방 종료 시 플레이어별 로컬 스냅샷 자동 삭제
|
||||
- [x] 연결 종료 시 방 참가자 정리
|
||||
- [x] NAS WebSocket 프록시 배포
|
||||
- [ ] 비어 있지 않은 방의 유휴 만료 처리
|
||||
@@ -64,6 +74,8 @@
|
||||
- [ ] 이미지 로딩 실패 폴백
|
||||
- [ ] 접근성 키보드 조작 범위 검토
|
||||
- [ ] 서버 재시작 시 방 종료 안내
|
||||
- [x] 공개 카드 상태 리비전으로 오래된 제출 무시
|
||||
- [ ] 비공개 덱 조작 중 중복 클릭 잠금
|
||||
- [ ] Redis 또는 상태 저장 필요성 검토
|
||||
|
||||
## 확인이 필요한 정책
|
||||
|
||||
@@ -64,3 +64,42 @@
|
||||
- 방을 이동할 때 이전 Socket.IO 채널에서 확실히 나가도록 정리했다.
|
||||
- Nginx의 `/socket.io` 프록시와 Docker 내부 실시간 서버 서비스를 추가했다.
|
||||
- 방 생성부터 양쪽 게임 시작까지 검증하는 2인 통합 테스트를 추가했다.
|
||||
- 온라인 선후공을 서버 무작위 지정으로 변경하고 양쪽의 중복 선공 선택을
|
||||
제거했다.
|
||||
- 양쪽 손패 확정 후 선공부터 시작하는 현재 차례와 턴 종료 교대를 추가했다.
|
||||
- 필드·공개·무덤·라이프 카드 상태와 비공개 영역 장수를 상대 화면에
|
||||
동기화했다.
|
||||
- 상대 카드는 반대편 좌표로 표시하고 직접 조작할 수 없도록 했다.
|
||||
- 손패와 덱 카드 목록이 상대에게 전달되지 않도록 서버 공개 상태를 제한했다.
|
||||
- 통합 테스트 범위를 선후공, 초기 준비, 공개 카드 비공개 필터, 턴 교대까지
|
||||
확장했다.
|
||||
- 플레이매트의 Action Point 영역을 침범하던 손패를 화면 하단 전용 트레이로
|
||||
분리했다.
|
||||
- 손패 카드를 축소된 한 줄 목록으로 표시하고 가로 스크롤을 지원한다.
|
||||
- 새로고침과 짧은 네트워크 끊김 후 같은 방·좌석·턴을 복구하는 재접속
|
||||
식별자를 추가했다.
|
||||
- 연결이 끊긴 플레이어의 자리를 60초간 보존하고 재접속 통합 테스트를
|
||||
추가했다.
|
||||
- 내 덱 순서, 손패, 카드 위치, 멀리건과 준비 상태를 플레이어별 브라우저
|
||||
저장소에 보관해 새로고침 후 복구하도록 했다.
|
||||
- 로컬에서 Vue만 실행해 웹소켓 오류가 발생하는 문제를 예방하도록
|
||||
`npm run dev:all` 통합 개발 명령을 추가했다.
|
||||
- macOS 파일 감시 한도에서 Socket.IO 개발 서버가 `EMFILE`로 종료되는
|
||||
문제를 피하도록 서버 개발 명령의 `--watch`를 제거했다.
|
||||
- 대기실과 게임 화면에 `방 나가기`를 추가하고 한쪽이 나가면 양쪽 모두
|
||||
대기실 상태로 복귀하도록 했다.
|
||||
- 먹통이 된 방을 정리하고 즉시 새 초대 코드를 만드는 버튼을 추가했다.
|
||||
- 진행 중인 게임에서 대기실로 이동하거나 브라우저를 다시 열어도 게임
|
||||
화면으로 자동 복귀하도록 재접속 응답을 보강했다.
|
||||
- 방 종료 시 재접속 세션과 플레이어별 게임 스냅샷을 삭제하도록 했다.
|
||||
|
||||
## 2026-06-22
|
||||
|
||||
- 온라인 덱 셔플을 클라이언트 난수 대신 서버의 암호학적 난수로 변경했다.
|
||||
- 서버가 플레이어별 비공개 덱 인스턴스 순서를 관리하도록 추가했다.
|
||||
- 드로우, 여러 장 공개, 라이프 배치와 덱 위·아래 이동을 서버 순서에
|
||||
연결했다.
|
||||
- 공개 카드 상태에 리비전 번호를 추가해 늦게 도착한 이전 상태를 무시하도록
|
||||
했다.
|
||||
- 서버 통합 테스트에 비공개 덱 셔플·드로우·되돌리기와 오래된 리비전 거부를
|
||||
추가했다.
|
||||
|
||||
Reference in New Issue
Block a user