Two stages were not much of a ladder. Add STAGE 3 · 강철 회랑: 31 steel cells the shells cannot pass, a double brick cap over the base and two narrow funnels into it, 8 enemies with 4 on the field. Getting to a later stage meant clearing the earlier ones. Read the stage from the URL (#3 or ?stage=3) at load and on hashchange, so the shell's nav chips and a plain link both land where they say. The stage buttons and key hints are built from STAGES, so a new stage needs no UI edit. Simulated 10 idle and 10 random-bot runs per stage: stage 3 leaves the base standing in 6 of 10 idle runs, and its 144 reachable cells keep the player out of a sealed pocket - an earlier layout had only 8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
128 lines
6.8 KiB
Markdown
128 lines
6.8 KiB
Markdown
# crossword
|
||
|
||
문학 가로세로 낱말풀이 — 정적 사이트. 왼쪽 탭에서 편을 골라 본 프레임에서 바로 푼다.
|
||
낱말풀이 목록 아래 **휴게실** 구획에는 곁들이로 탱크게임을 두었다.
|
||
|
||
- 공개 주소: https://crossword.plpark.ectech.co.kr
|
||
- 내부 주소: http://192.168.0.216:31380 (NodePort)
|
||
|
||
## 수록 편
|
||
|
||
| 탭 | 경로 | 격자 | 항목 |
|
||
|---|---|---|---|
|
||
| 한국 문학 | `/korean.html` | 9 × 9 · 41칸 | 15 |
|
||
| 러시아 문학 | `/russian.html` | 9 × 9 · 40칸 | 13 |
|
||
| 영국 문학 | `/british.html` | 9 × 9 · 39칸 | 12 |
|
||
| 프랑스 문학 | `/french.html` | 9 × 9 · 40칸 | 15 |
|
||
| 독일 문학 | `/german.html` | 9 × 9 · 41칸 | 14 |
|
||
| 일본 문학 | `/japanese.html` | 9 × 9 · 44칸 | 14 |
|
||
|
||
기본으로 열리는 탭은 한국 문학이다. 각 페이지는 단독으로 열어도 동작한다. 셸(`/`)의 탭은 해시로 상태를 남기므로
|
||
`/#korean`, `/#russian`, `/#british`, `/#french`, `/#german`, `/#japanese`, `/#tank` 로 바로 들어갈 수 있고, 탭을 다시 눌러도 풀던 내용은 유지된다.
|
||
휴게실 탱크게임의 단계별 진입은 `/#tank1`, `/#tank2`, `/#tank3` 이다.
|
||
|
||
## 휴게실
|
||
|
||
| 탭 | 경로 | 단계 | 적 | 특징 |
|
||
|---|---|---|---|---|
|
||
| 탱크게임 | `/tank.html` | 1단계 요새 | 4대 (동시 3) | 벽돌 밀도 25%, 넓은 기동 공간 |
|
||
| ↳ | `/tank.html#2` | 2단계 미로 | 6대 (동시 4) | 밀도 43%, 1칸 폭 통로가 얽힌 미로 |
|
||
| ↳ | `/tank.html#3` | 3단계 강철 회랑 | 8대 (동시 4) | 강철 31칸, 기지 외곽 2중 + 좁은 진입로 |
|
||
|
||
방향키 이동, 스페이스 발사. 한 단계를 돌파하면 다음 단계로 이어지고, 점수와 남은 탱크를
|
||
그대로 가져간다. **단계를 거치지 않고 바로 들어가는 방법**은 셋이다.
|
||
|
||
- 왼쪽 탭의 `휴게실 > 탱크게임` 아래 단계 칩 (셸 주소로는 `/#tank2`, `/#tank3`)
|
||
- 게임 화면 오른쪽 아래의 단계 버튼, 또는 `1` ~ `3` 키
|
||
- 주소 직접 입력 — `/tank.html#3`, `/tank.html?stage=3` (범위를 벗어난 값은 1단계)
|
||
|
||
낱말풀이와 공유하는 자산이 없는 단독 페이지다. 캔버스와 WebAudio 만 쓰므로 자산 해시
|
||
스탬프(아래 캐시 항목)의 영향도 받지 않는다. iframe 안에서는 **화면을 한 번 클릭해
|
||
포커스를 준 뒤** 키를 눌러야 한다. 셸이 iframe 의 조각(`#2`)만 바꿀 때는 문서가 다시
|
||
읽히지 않으므로, 게임 쪽에서 `hashchange` 를 받아 단계를 바꾼다.
|
||
|
||
## 구성
|
||
|
||
| 경로 | 설명 |
|
||
|---|---|
|
||
| `site/index.html` | 셸 — 왼쪽 탭(좁은 화면에서는 위쪽 탭) + 게임을 띄우는 iframe |
|
||
| `site/korean.html` | 한국 문학 편 (머리말 + 퍼즐 데이터) — 기본 탭 |
|
||
| `site/russian.html` | 러시아 문학 편 (머리말 + 퍼즐 데이터) |
|
||
| `site/british.html` | 영국 문학 편 (머리말 + 퍼즐 데이터) |
|
||
| `site/french.html` | 프랑스 문학 편 (머리말 + 퍼즐 데이터) |
|
||
| `site/german.html` | 독일 문학 편 (머리말 + 퍼즐 데이터) |
|
||
| `site/japanese.html` | 일본 문학 편 (머리말 + 퍼즐 데이터) |
|
||
| `site/tank.html` | 휴게실 · 탱크게임 (단독 페이지 — 캔버스 + WebAudio) |
|
||
| `site/assets/crossword.css` | 각 편이 공유하는 화면 양식 |
|
||
| `site/assets/crossword.js` | 각 편이 공유하는 게임 엔진 (한글 IME 입력, 검사/힌트/타이머) |
|
||
| `nginx/default.conf` | nginx 설정 (8080 리슨, `/healthz` 헬스체크) |
|
||
| `Dockerfile` | 자산 URL 에 내용 해시를 박는 단계 + nginx:1.27-alpine 정적 서빙 |
|
||
| `k8s/deployment.yaml` | Deployment + NodePort Service (31380) |
|
||
| `Jenkinsfile` | Checkout → 이미지 빌드/푸시 → kubectl apply |
|
||
|
||
Spring Boot 등 런타임은 없음. HTML/CSS/JS 만으로 동작하는 완전 정적 사이트라
|
||
nginx 로 파일만 내려주면 끝.
|
||
|
||
## 편을 추가할 때
|
||
|
||
1. `site/<편>.html` 을 기존 편 하나를 복사해 만든다. 고칠 곳은 `<title>`,
|
||
머리말(`.eyebrow` / `h1`), 그리고 `const PUZZLE = {...}` 뿐이다.
|
||
2. `site/index.html` 의 `PAGES` 객체와 `.nav` 링크에 한 줄씩 추가한다.
|
||
|
||
`PUZZLE` 규약:
|
||
|
||
- `size` — 격자 한 변 (9)
|
||
- `grid[r][c]` — 정답 음절, 빈 칸은 `null`
|
||
- `nums[r][c]` — 칸에 찍히는 번호, 없으면 `0`. 길이 2 이상 구간의 시작 칸에
|
||
좌→우 · 위→아래 순서로 붙인다
|
||
- `entries[]` — `{num, dir:"A"|"D", r, c, answer, clue, kind}`
|
||
|
||
**가로·세로로 길이 2 이상 이어지는 모든 구간은 반드시 `entries` 에 단서와 함께
|
||
등록돼 있어야 한다.** 우연히 생긴 두 글자 구간이 남아 있으면 그 칸을 채워도
|
||
완료 판정이 나지 않는 것처럼 보이므로, 격자를 새로 짜면 이 조건을 먼저 확인한다.
|
||
|
||
단서를 쓸 때는 **다른 항목의 정답을 단서 안에 그대로 적지 않는다.** 서로가 서로를
|
||
알려 주면 두 칸이 한꺼번에 풀려 버린다. 한 정답이 다른 정답에 통째로 들어가는 짝
|
||
(`소세키` / `나쓰메소세키`) 도 한 격자에 같이 두지 않는다.
|
||
|
||
낱말 풀은 작품과 작가만으로는 성글다. 특히 원어 표기의 음절이 서로 겹치지 않는
|
||
편(독일 편)에서는 **등장인물과 작품 속 핵심어까지 넣어야** 교차 지점이 생긴다.
|
||
|
||
## 캐시
|
||
|
||
프록시(NPM)가 `js` / `css` 응답에 제 캐시 헤더를 덧붙인다. 원본 nginx 가
|
||
`no-cache` 를 줘도 덮어써지므로, 브라우저가 옛 자산을 계속 쓰는 일을 막기 위해
|
||
**이미지 빌드 때 자산 URL 에 내용 해시를 박는다.**
|
||
|
||
- `Dockerfile` 의 `stamp` 단계가 `crossword.css` + `crossword.js` 의 sha256 앞
|
||
열 자리를 뽑아 각 페이지의 링크를 `assets/crossword.js?v=<해시>` 로 바꾼다
|
||
- 자산 내용이 바뀌면 해시가 바뀌고 URL 도 바뀌므로 캐시가 저절로 무효화된다
|
||
- 저장소의 HTML 은 해시 없는 상태로 두면 된다. 손으로 붙일 필요 없다
|
||
- HTML 자체는 프록시가 캐시하지 않으므로 새 해시가 바로 전달된다
|
||
|
||
## 배포
|
||
|
||
Jenkins 잡(`crossword`)을 돌리면 아래가 순서대로 진행된다.
|
||
|
||
1. gitea `plpark/crossword` main 브랜치 체크아웃
|
||
2. `gitea.plpark.ectech.co.kr/plpark/crossword:<BUILD_NUMBER>` 빌드 & 푸시
|
||
3. `plpark-int-ns` 네임스페이스에 apply, rollout 대기
|
||
|
||
Jenkins credentials 는 기존 loggen/pilot1 과 동일하게
|
||
`gitea-registry`(usernamePassword), `rancher-kubeconfig`(file) 를 사용한다.
|
||
|
||
## 프록시 (NPM)
|
||
|
||
`https://npm.ectech.co.kr` 에서 Proxy Host 추가:
|
||
|
||
- Domain Names: `crossword.plpark.ectech.co.kr`
|
||
- Scheme: `http`
|
||
- Forward Hostname / IP: `192.168.0.216`
|
||
- Forward Port: `31380`
|
||
- Block Common Exploits / Websockets Support: ON
|
||
- SSL: Let's Encrypt 발급 + Force SSL + HTTP/2
|
||
|
||
## 콘텐츠 수정
|
||
|
||
`site/` 아래만 고치고 커밋 → Jenkins 빌드. 그 외 변경 필요 없음.
|