
문제를 내는 AI
지금까지 만든 바둑·오목·오델로 AI는 사람과 두는 상대였습니다. 십자말풀이는 다릅니다. AI가 문제를 풀거나 사람과 겨루지 않고, 사람이 풀 문제를 냅니다.
웹은 다른 게임과 같은 틀로 만들었습니다.
- crossword.mariago.kr입니다. 집 맥의 3009 포트에서 돌고, IDC의 nginx가 이어 줍니다.
- 대기실에서 난이도를 고르고 게임방에 들어가면 새 판이 만들어집니다.
- 판과 입력 하나하나를 SQLite에 기보로 남깁니다.
처음 순서: 단어 → 문제 → 판
처음에는 단어 은행에서 단어를 고르고, 단어마다 문제를 붙인 다음, 그 단어들을 판 위에 이어 붙였습니다. 이미 놓인 단어와 같은 글자가 있으면 그 자리에 걸쳐 놓는 방식입니다.
나온 판은 십자말이 아니었습니다.
- 12×12 판에 문제가 7~8개뿐이었습니다. 판 대부분이 검은 칸이었습니다.
- 답이 대부분 2글자였습니다.
- 단어가 첫 글자와 끝 글자로만 이어져 비스듬한 계단처럼 늘어섰습니다. 크리스마스의 ‘스’와 알프스의 ‘스’처럼 가운데 글자로 엇갈리는 교차가 거의 없었습니다.
- 단어가 판 가운데에만 몰렸습니다.
- 어려움 판 두 판을 연달아 했더니 한 단어를 빼고 전부 이전 판에 나온 단어였습니다.
첫날 저장된 24판
기보 DB에 그날 만든 판이 24개 남아 있습니다. 모두 12×12 어려움 판입니다.
- 단어 수는 6~8개, 평균 6.8개였습니다.
- 흰 칸은 판의 평균 6.5%였습니다.
- 답 가운데 76%가 2글자였습니다.
- 180° 대칭인 판은 하나도 없었습니다.
24판 중 22판은 한 칸도 채우지 않고 다시 새 판을 눌렀습니다. 판을 보자마자 다시 만든 것입니다.
고쳐 보려던 것들
- 가로·세로 15문제 이상 나오게 했습니다. 시험에서는 통과했는데 화면에는 여전히 8문제짜리 판이 나왔습니다. 대기실이 예전에 만든 판을 계속 이어서 열고 있었습니다.
- 생성기를 직접 돌려 판 그림과 문제 목록을 글자로 찍는 텍스트 에이전트를 만들었습니다. 기보 DB의 판도 같이 검사해서 2글자 섬이면 표시합니다.
- 3글자 십자 덩어리를 반복해서 판을 채우는 방식도 나왔습니다. 위 오른쪽 그림입니다. 문제 수는 늘었지만 모든 답이 3글자이고 덩어리끼리는 이어지지 않습니다. AI다운 발상이었고, 이런 십자말풀이는 세상에 없습니다.
순서를 바꾸기로
단어부터 놓으면 판 모양은 그 결과로 따라 나올 뿐이라 손댈 수가 없습니다. 그래서 판을 먼저 만들고, 그 칸 길이에 맞는 단어를 고르고, 마지막에 문제를 내기로 했습니다.
이 단계의 실제 코드
텍스트 에이전트가 저장된 판을 검사하는 부분입니다. 평균 길이가 짧거나 가운데 교차가 없으면 새 게임이 필요하다고 찍습니다.
scripts/text-agent.ts
for (const row of rows) {
let across = 0;
let down = 0;
let mid = 0;
let avg = 0;
try {
const puzzle = JSON.parse(row.puzzle_json) as Puzzle;
across = puzzle.across?.length ?? 0;
down = puzzle.down?.length ?? 0;
mid = countMiddleCrosses(puzzle);
avg = avgWordLength(puzzle);
} catch {
/* ignore */
}
const mark = avg >= 2.7 && mid >= 1 ? "교차있음" : "2글자섬 ← 새 게임 필요";
console.log(
`${row.code} ${row.difficulty} ${row.size}×${row.size} ${row.status} 가로 ${across} 세로 ${down} 교차 ${mid} 평균 ${avg.toFixed(2)} ${mark} ${row.created_at}`,
);단어를 이어 붙이는 배치기의 규칙입니다. 새 단어는 이미 놓인 단어와 정확히 한 칸만 겹쳐야 하고, 옆 칸에 다른 글자가 붙으면 안 됩니다. 이 배치기는 지금도 예비 경로로 남아 있는데, 3편에서 다시 나옵니다.
lib/gridFill.ts
for (const [nr, nc] of neighbors) {
if (inBoard(size, nr, nc) && grid[idx(size, nr, nc)] != null) return null;
}
}
if (intersections !== 1 || newCells < 1) return null;다음 글은 ‘2. 판 먼저, 단어 다음, 문제는 마지막’입니다.
COMMENTS · CHAPTER 01
단어부터 놓았더니 십자말이 아니었습니다 댓글 0
아직 댓글이 없습니다. 읽다가 궁금한 점이나 보탤 내용을 남겨 주세요.
댓글을 등록하려면 구글로 시작하기를 눌러 닉네임과 메일 주소를 등록해 주세요.
구글로 시작하기