삼각형 게임 만들기 - 2,760줄로 구현한 전략 게임

회사에서 해양 GIS 데이터만 만지다가 뭔가 다른 거 해보고 싶더라구요. 그래서 만든 게 Triangle Game이에요.
점을 연결해서 삼각형을 만드는 단순한 룰인데, 막상 구현하려니 생각보다 복잡하더라구요. AI 상대 만들고, 선 교차 검사하고, 삼각형 자동 인식하고… 결국 2,760줄짜리 프로젝트가 됐어요.
게임 규칙은 단순해요
기본 룰:
- 차례대로 점 두 개를 선으로 연결
- 선이 서로 교차하면 안 됨
- 삼각형을 완성하면 영역 차지 + 보너스 턴
- 가장 많은 삼각형을 만든 사람이 승리
룰은 단순한데, 여기에 AI 전략, 타임아웃 시스템, 페널티까지 붙이다 보니 코드가 길어졌어요.
구현에서 까다로웠던 부분
1. 선 교차 검사
선이 교차하는지 검사하는 알고리즘이 필요했어요. CCW (Counter-Clockwise) 방식 썼는데, 두 선분의 방향성을 계산해서 교차 여부를 판단합니다.
function doLinesIntersect(p1, p2, p3, p4): boolean {
const ccw = (A, B, C) =>
(C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x);
// 끝점이 같으면 무시
if (p1.id === p3.id || p1.id === p4.id || p2.id === p3.id || p2.id === p4.id)
return false;
return (
ccw(p1, p3, p4) !== ccw(p2, p3, p4) && ccw(p1, p2, p3) !== ccw(p1, p2, p4)
);
}
처음엔 이게 왜 작동하는지 이해 못 했는데, 기하학적으로 두 점이 선분의 양쪽에 있는지 확인하는 원리더라구요.
2. 삼각형 자동 인식
선을 그을 때마다 삼각형이 완성됐는지 체크해야 해요. 모든 점을 순회하면서 세 점이 모두 연결되어 있는지 확인합니다.
function findNewTriangles(
newLine: Line,
lines: Line[],
points: Point[]
): [number, number, number][] {
const newTriangles = [];
for (const point of points) {
if (point.id === from || point.id === to) continue;
// 세 점이 모두 선으로 연결되어 있는지 확인
if (isTriangle(from, to, point.id, allLines)) {
const sorted = [from, to, point.id].sort();
// 중복 체크
if (!alreadyExists(sorted, existingTriangles)) {
newTriangles.push(sorted);
}
}
}
return newTriangles;
}
O(n³) 복잡도라 처음엔 성능 걱정했는데, 점이 최대 20-30개 정도라 실제로는 문제없더라구요.
3. AI 전략 구현
AI가 단순히 랜덤으로 두면 재미없잖아요. 그래서 3단계 우선순위로 작동하게 만들었어요.
- 우선순위 1: 삼각형 완성할 수 있으면 무조건 완성
- 우선순위 2: 안전한 수 (상대에게 기회 안 줌)
- 우선순위 3: 위험한 수 (상대에게 삼각형 완성 기회를 줄 수 있음)
여기에 AI는 0-3개의 랜덤 “스킵 턴”을 가지고 있어서, 위험한 수를 두느니 차라리 턴을 넘기는 전략도 씁니다.
// 우선순위 3: 위험한 수
if (riskyMoves.length > 0) {
// AI가 스킵 턴 남아있고, 30% 확률로 스킵 선택
if (isRisky && aiSkipsRemaining > 0 && random < 0.3) {
return SKIP_TURN;
}
return bestRiskyMove;
}
실제로 해보면 AI가 꽤 전략적으로 플레이해요.
재미 요소 추가
AI 도발 시스템
AI가 수를 두면서 간간이 도발 메시지를 날려요. 10개 카테고리로 나눠서 상황에 맞게 나오도록 했습니다.
- 시간 부족: “똑딱똑딱… 고민 많으시네? 🤔”
- AI 득점: “꿀꺽! 삼각형 하나 먹었다~ 🔺”
- 플레이어 득점: “운이 좋으셨네요 🙄”
- 플레이어 뒤처짐: “저 점수 보이세요? 😏”
확률로 발동해서 매번 나오진 않고, 한 번에 하나씩만 떠요. 처음엔 너무 자주 떠서 스팸처럼 느껴졌는데, 확률 조정하니까 적당히 재미있어졌어요.
턴 타이머 & 페널티
턴당 10초 제한 걸어뒀어요. 3초 남으면 빨간색으로 깜빡이면서 경고하고, 시간 다 되면 자동 스킵됩니다.
스킵 횟수는 최종 점수에서 차감되는 페널티로 작동해요.
최종 점수 = 완성한 삼각형 - 스킵 횟수
이거 안 넣었을 때는 게임 끝날 때까지 고민만 하는 사람 있었거든요 ㅋㅋ
운영자 턴 (비상 메커니즘)
모든 플레이어가 연속으로 스킵하면 무한 루프 빠질 수 있어요. 그래서 “운영자 턴”이라는 걸 만들었어요.
모두가 스킵하면 운영자(playerId: -1)가 랜덤으로 유효한 선 하나를 그어주고, 다음 플레이어를 랜덤으로 선택합니다. 점수는 안 주니까 게임 밸런스엔 영향 없어요.
캔버스 렌더링과 입력 처리
Canvas API로 직접 그렸어요. React 상태와 Canvas 렌더링 타이밍 맞추는 게 생각보다 까다롭더라구요.
렌더링 순서:
- 배경
- 삼각형 채우기 (색상 + 40% 투명도)
- 삼각형 번호
- 선
- 점
터치와 마우스 입력 둘 다 지원하는데, 터치 이벤트가 마우스 이벤트를 에뮬레이션하는 문제가 있어요. 그래서 터치 발생 후 500ms 동안은 마우스 이벤트 무시하게 만들었어요.
handleTouchStart(e) {
lastTouchTime = Date.now();
e.preventDefault();
// ... 터치 처리
}
handleMouseClick(e) {
// 터치 후 500ms 이내 마우스 이벤트 무시
if (Date.now() - lastTouchTime < 500) return;
// ... 마우스 처리
}
모바일에서도 문제없이 작동합니다.
소셜 공유 기능
게임 끝나면 결과를 이미지로 만들어서 공유할 수 있어요. html2canvas 라이브러리 써서 게임 캔버스랑 결과 화면을 합성했습니다.
async handleShare() {
// 게임 캔버스 캡처
const gameCanvas = canvasRef.current;
// 결과 오버레이 캡처
const resultHTML = await html2canvas(gameOverRef.current);
// 합성 이미지 생성 (그라데이션 배경 + 캔버스 + 결과)
// ...
// Web Share API 시도, 실패 시 다운로드
if (navigator.canShare({ files: [image] })) {
await navigator.share({ files: [image] });
} else {
downloadImage("triangle-game-result.png");
}
}
워터마크로 “sevenfingersstudio.com” 넣어놨어요.
운영자 실전 노트
세븐핑거스가 실제 프로젝트 진행하면서 겪은 문제들입니다.
Canvas 렌더링 타이밍
- React 상태 업데이트 후 useEffect에서 렌더링해야 정상 작동함
터치 vs 마우스 이벤트 충돌
- lastTouchTime으로 500ms 가드 걸어서 해결
AI 도발 스팸 문제
- 확률 기반 발동 + 한 번에 하나만 표시로 해결
선 교차 검사 false positive
- 끝점 공유 케이스 별도 처리 필요
삼각형 중복 인식
- 점 ID 정렬 후 비교해서 중복 제거
이 경험을 통해 알게 된 점
- 기하학 알고리즘(CCW)은 개념만 이해하면 구현은 단순함
- Canvas API는 성능 좋지만 상태 관리가 React와 다른 패러다임 — useEffect 활용 필수
- AI 전략은 3단계 우선순위만으로도 충분히 도전적인 상대 구현 가능
- 게임 룰이 단순해도 예외 케이스(무한 스킵 등) 처리가 프로젝트 크기를 2배로 늘림
- 모바일 입력 처리는 터치 에뮬레이션 방지가 핵심
FAQ
Q. 전체 코드량이 2,760줄이면 많은 편인가요?
게임 로직(1,362줄), 스타일(657줄), 설정 화면, AI 시스템 다 합쳐서 그 정도예요. 외부 라이브러리 거의 안 쓰고 순수 React + Canvas로 만들다 보니 길어졌습니다. 실제 핵심 알고리즘은 200줄 정도고, 나머지는 UI와 예외 처리예요.
Q. AI 난이도 조절은 어떻게 하나요?
현재는 난이도 옵션 없고, AI가 랜덤으로 0-3개 스킵 턴을 가지고 시작해요. 이게 운적 요소로 작용해서 매번 난이도가 조금씩 달라집니다. 나중에 Easy/Normal/Hard 모드 추가할 예정이에요.
마치며
단순한 게임인 줄 알았는데 생각보다 신경 쓸 게 많았어요. 선 교차 검사, 삼각형 인식, AI 전략, 모바일 입력 처리까지… 하나하나 해결하다 보니 어느새 2,760줄이 됐네요.
관심 있으시면 Triangle Game에서 직접 플레이해보세요.
← 블로그 목록으로