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

learning by 세븐핑거스 8분
ReactTypeScriptCanvas게임개발AI웹게임

삼각형 게임 완료 화면

회사에서 해양 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 렌더링 타이밍 맞추는 게 생각보다 까다롭더라구요.

렌더링 순서:

  1. 배경
  2. 삼각형 채우기 (색상 + 40% 투명도)
  3. 삼각형 번호

터치와 마우스 입력 둘 다 지원하는데, 터치 이벤트가 마우스 이벤트를 에뮬레이션하는 문제가 있어요. 그래서 터치 발생 후 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에서 직접 플레이해보세요.

← 블로그 목록으로