GLM 모델로 플레이 가능한 HTML5 미니 게임 생성하기

GLM 모델로 플레이 가능한 HTML5 미니 게임 생성하기 (Generate a playable HTML5 mini game using the GLM model via the Mistral API)

Mistral API를 통해 zai-glm-5-2(GLM) 모델을 사용해 하나의 프롬프트로 완전하고 플레이 가능한 HTML5 던전 크롤러 게임을 생성하고, 자동으로 일반적인 게임 메커니즘 문제를 리뷰·수정하는 방법을 보여주는 쿡북입니다.

출처: 문서

본문

Mistral API를 통해 zai-glm-5-2(GLM) 모델을 사용해 단일 프롬프트로 완전하고 플레이 가능한 HTML5 던전 크롤러를 생성한 뒤, 일반적인 게임 메커니즘 문제를 자동으로 리뷰하고 수정해요.

사전 요구사항 (Prerequisites)

  • Python 3.10+
  • Mistral 계정과 API 키

환경 설정 (Environment setup)

설치 (Install)

Mistral Python SDK를 설치하고 .env 파일에서 API 키를 로드하기 위한 python-dotenv를 설치해요:

pip install mistralai python-dotenv

필요한 환경 변수 (Required environment variables)

이 쿡북을 완료하려면 Mistral API 키가 필요해요. Studio의 API keys 섹션에서 새 API 키를 만드세요.

프로젝트 루트에 .env를 만들고 Mistral API 키를 추가해요:

MISTRAL_API_KEY=your-mistral-api-key

Step 1 — 클라이언트 초기화 (Initialize the client)

프로젝트 디렉토리에 generate_game.py를 만드세요:

touch generate_game.py

파일을 열고 임포트와 클라이언트 초기화를 추가해요. 클라이언트는 GLM이 1000줄 이상의 큰 코드 출력을 생성해 몇 분이 걸릴 수 있으므로 timeout_ms=600_000(10분)과 일치하는 httpx 타임아웃을 설정해요. 나머지 단계는 프롬프트, 생성, 리뷰 루프, 편집 모드, 로컬 서버를 구성해요.

"""Generate a playable HTML5 mini game using the GLM model via the Mistral API."""

import argparse
import functools
import http.server
import os
import re
import webbrowser
from pathlib import Path

from dotenv import load_dotenv

import httpx

from mistralai.client import Mistral

load_dotenv()

# Step 1 — Initialize the client
# GLM generates large code outputs that can take several minutes. The default
# httpx timeout is too short, so set it to 10 minutes to match timeout_ms.
client = Mistral(
    api_key=os.environ["MISTRAL_API_KEY"],
    timeout_ms=600_000,
    client=httpx.Client(follow_redirects=True, timeout=httpx.Timeout(600.0)),
)

# Step 2 — Craft the game prompt

# Step 3 — Generate the game

# Step 4 — Review and fix the game

# Step 5 — Edit the game

# Step 6 — Serve the game locally

def main():
    # Step 7 — Tie it all together in main
    pass

if __name__ == "__main__":
    main()

Step 2 — 게임 프롬프트 작성 (Craft the game prompt)

프롬프트는 두 부분으로 이뤄져요. 시스템 메시지는 출력 형식을 제한하고(단일 HTML 파일, 외부 의존성 없음, Canvas 렌더링), 사용자 메시지는 게임을 설명하고 구체적인 요구사항을 나열해요. 이 덕분에 모델이 시작 화면이나 게임오버 로직 같은 기능을 빠뜨리지 않아요.

"game engineering requirements" 블록이 핵심 추가 사항이에요. GLM은 때때로 메커니즘이 깨진 게임 — 처치할 수 없는 적, 빠진 충돌 감지, 벽 안에 생성되는 적 — 을 만들곤 해요. 이 요구사항이 가장 실패하기 쉬운 시스템을 어떻게 구현할지 모델에 알려줘요.

main 함수 위에 다음을 추가해요:

# Step 2 — Craft the game prompt
# Be specific about mechanics, controls, visuals, and scope.
# The more detail you provide, the better the generated game.
GAME_DESCRIPTION = (
    "A top-down dungeon crawler. The player navigates procedurally generated "
    "rooms connected by doorways. Each room contains enemies that patrol and "
    "chase the player on sight. Defeating enemies drops health potions or score "
    "pickups. The player has a melee attack (spacebar) and 3 lives. Generate at "
    "least 5 connected rooms. Show a minimap in the corner."
)

# The system message constrains the output format (single HTML file, no
# external dependencies, Canvas rendering). The user message describes the
# game and lists concrete requirements so the model doesn't omit features.
# The "Game engineering requirements" block addresses common failure modes
# like broken collision detection, enemies that can't be killed, and missing
# spawn logic.
def build_game_prompt(game_description: str) -> tuple[str, str]:
    """Build the system and user prompts for game generation."""
    system_prompt = (
        "You are an expert game developer. You produce complete, self-contained "
        "HTML files with embedded CSS and JavaScript. Never use external CDNs, "
        "libraries, or dependencies. Use HTML5 Canvas for rendering. The game "
        "must be fully playable in any modern browser by opening the HTML file directly."
    )
    user_prompt = (
        f"Create a complete, playable game: {game_description}\n\n"
        "Requirements:\n"
        "- Single HTML file with all CSS and JS embedded\n"
        "- No external dependencies, CDNs, or imports\n"
        "- Use HTML5 Canvas for rendering\n"
        "- Keyboard controls (arrow keys or WASD)\n"
        "- Include a start screen with instructions\n"
        "- Track and display score and health/lives\n"
        "- Include game-over and restart logic\n"
        "- Use requestAnimationFrame for the game loop\n"
        "- Add colors, simple shapes, or pixel art for visuals\n\n"
        "Game engineering requirements (follow these exactly):\n"
        "- Collision detection: implement rectangle or circle collision checks. "
        "Every entity (player, enemies, projectiles, items) must have x, y, "
        "width, and height properties used in collision tests.\n"
        "- Enemy health: every enemy must have a numeric health property that "
        "decreases when the player attacks. Remove the enemy when health "
        "reaches 0.\n"
        "- Combat feedback: when the player attacks, check collision against "
        "every enemy in range. On hit, decrease enemy health and show visual "
        "feedback (flash, particle, or color change).\n"
        "- Valid spawning: enemies must spawn on valid floor positions, never "
        "inside walls or on top of the player. Validate positions before "
        "placing.\n"
        "- Game loop integrity: the update function must call enemy AI, "
        "collision detection, and rendering every frame. Never skip a step.\n"
        "- Input handling: use keydown/keyup events with a keys-pressed "
        "object (e.g., `keys = {}`) that tracks which keys are currently held. "
        "Check this object each frame in the update loop.\n\n"
        "Return the complete HTML file inside a single ```html code fence."
    )
    return system_prompt, user_prompt

엔지니어링 요구사항은 6가지 실패 모드를 다룹니다:

요구사항 방지하는 것
Collision detection 엔티티가 서로를 통과하는 것
Enemy health 처치할 수 없는 적
Combat feedback 타격을 등록하지 않는 공격
Valid spawning 벽에 끼거나 플레이어와 겹치는 적
Game loop integrity 게임 중 멈추는 시스템
Input handling 놓치는 키 입력 또는 고착된 움직임

Step 3 — 게임 생성 (Generate the game)

프롬프트를 GLM에 보내고 응답에서 HTML을 추출해요. 모델은 출력을 ```html 코드 펜스로 감싸요. extract_html 함수가 이를 파싱하고, 펜스가 없으면 DOCTYPE 기반 추출로 대체해요.

main 위에 extract_html과 generate_game 함수를 추가해요:

# Step 3 — Extract HTML from the response
# The model wraps its output in a ```html code fence. This function extracts
# the HTML content, falling back to DOCTYPE-based extraction if no fence is found.
def extract_html(text: str) -> str:
    """Extract HTML content from the model response."""
    # Try fenced code block first
    match = re.search(r"```html\s*\n(.*?)```", text, re.DOTALL)
    if match:
        return match.group(1).strip()

    # Fall back to DOCTYPE extraction
    match = re.search(r"(<!DOCTYPE.*?</html>)", text, re.DOTALL | re.IGNORECASE)
    if match:
        return match.group(1).strip()

    raise ValueError("No HTML content found in the model response.")

# Wraps the GLM call and HTML extraction into a single function.
def generate_game(system_prompt: str, user_prompt: str) -> str:
    """Call GLM to generate a game and return the extracted HTML."""
    response = client.chat.complete(
        model="zai-glm-5-2",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
    )
    return extract_html(response.choices[0].message.content)

Step 4 — 게임 리뷰·수정 (Review and fix the game)

좋은 프롬프트로도 생성된 게임에 가끔 버그가 있을 수 있어요. 이는 두 범주로 나뉘어요:

  • 런타임 에러 — 스폰 함수가 정의되지 않은 속성에 접근하거나, 초기화 코드가 인자가 빠진 함수를 호출하거나, 충돌 검사가 잘못된 객체를 참조하는 등. 게임이 TypeError로 크래시돼요.
  • 깨진 로직 — 적이 데미지를 받지만 제거되지 않거나, 공격이 연결돼도 시각적 피드백이 없거나, 게임 루프가 시스템을 건너뛰는 등. 게임은 실행되지만 제대로 플레이되지 않아요.

이 단계는 HTML을 mistral-medium-latest에 보내 두 가지를 모두 확인하는 2부 리뷰를 수행해요. 이슈가 발견되면 HTML과 이슈 설명을 GLM에 다시 보내 목표 지향 수정을 수행해요. 리뷰가 통과할 때까지 최대 5회 시도로 루프가 반복돼요.

서로 다른 강점을 위해 두 모델을 사용해요:

  • mistral-medium-latest가 코드를 리뷰해요. 런타임 에러에 대해 실행 경로를 추적하고, 게임 로직을 끝까지 따라가 정확성을 확인해요.
  • zai-glm-5-2(GLM)가 코드를 수정해요. 게임을 생성한 모델과 동일해서 코드베이스를 이해해요.

main 위에 review_game과 fix_game 함수를 추가해요:

# Step 4 — Review and fix the game
# After generation, send the HTML to mistral-medium-latest for a structured
# review. If issues are found, send the HTML and issue list back to GLM for
# a targeted fix. This loop runs up to 2 times.
def review_game(html_content: str) -> str | None:
    """Review generated HTML for common game mechanic issues.

    Returns a string describing the issues found, or None if no issues.
    """
    review_prompt = (
        "You are a game QA engineer. Review the following HTML5 game for "
        "both runtime errors and broken game logic.\n\n"
        "PART 1 — RUNTIME ERRORS\n"
        "Trace these code paths from call site to implementation. Verify "
        "that every variable and property referenced actually exists. A "
        "function that accesses undefined properties is a FAIL.\n\n"
        "1. Initialization: Trace the startup path. Does every function "
        "called during init receive the arguments it expects? Are arrays "
        "and objects initialized before being accessed?\n"
        "2. Spawning: Trace the enemy spawn function. Does it access "
        "properties (like room.x, room.width) that actually exist on the "
        "objects passed to it?\n"
        "3. Game loop: Does the update/render loop call functions with "
        "correct arguments? Does it access properties on objects that "
        "might be undefined?\n"
        "4. Room transitions: When the player moves to a new room, are "
        "all references updated correctly?\n\n"
        "PART 2 — GAME LOGIC\n"
        "Trace these mechanics end-to-end. It's not enough for the code "
        "to exist — follow the logic and confirm it produces the correct "
        "outcome.\n\n"
        "5. Enemy death: Trace from player attack to enemy removal. Does "
        "the attack decrease enemy health? When health reaches 0, is the "
        "enemy actually removed from the array/list so it stops rendering "
        "and updating? A health property that decreases but never triggers "
        "removal is a FAIL.\n"
        "6. Collision detection: Are collision checks called with the "
        "correct coordinates and dimensions? Do entities have the x, y, "
        "width, height properties the checks reference?\n"
        "7. Combat feedback: When the player attacks and hits an enemy, "
        "is there any visual feedback (flash, color change, particle)? "
        "An attack that silently reduces health with no indication is a "
        "FAIL.\n"
        "8. Input handling: Are keydown/keyup events tracked in a "
        "keys-pressed object checked each frame? A system that only uses "
        "keydown without tracking held keys will miss continuous input.\n\n"
        "If ALL checks pass, respond with exactly: PASS\n\n"
        "If any check fails, describe the specific bug: which function, "
        "which property or logic path, and what goes wrong. Do not include "
        "the game code in your response.\n\n"
        f"```html\n{html_content}\n```"
    )
    response = client.chat.complete(
        model="mistral-medium-latest",
        messages=[{"role": "user", "content": review_prompt}],
    )
    result = response.choices[0].message.content.strip()
    # The model may add preamble before "PASS" or say "PASS - all checks passed",
    # so check whether the entire response is short and contains PASS.
    if len(result) < 100 and "PASS" in result.upper():
        return None
    return result

def fix_game(html_content: str, issues: str) -> str:
    """Send the HTML and issue list back to GLM for a targeted fix."""
    fix_prompt = (
        "The following HTML5 game has specific issues that need fixing. "
        "Fix ONLY the listed issues. Keep everything else unchanged.\n\n"
        f"Issues to fix:\n{issues}\n\n"
        f"Game code:\n```html\n{html_content}\n```\n\n"
        "Return the complete fixed HTML file inside a single ```html code fence."
    )
    response = client.chat.complete(
        model="zai-glm-5-2",
        messages=[
            {
                "role": "system",
                "content": (
                    "You are an expert game developer. Fix the specific issues "
                    "listed in the game code. Return the complete, corrected "
                    "HTML file. Do not remove working features."
                ),
            },
            {"role": "user", "content": fix_prompt},
        ],
    )
    return extract_html(response.choices[0].message.content)

리뷰-수정 루프가 main에 어떻게 통합되는지는 Step 7에서 볼 수 있어요. main이 review_game과 fix_game을 리뷰가 통과할 때까지(최대 5회) 루프로 호출해요.

Step 5 — 게임 편집 (Edit the game)

생성된 게임이 대부분 맞지만 특정 문제가 한 개 있을 수 있어요 — 적이 데미지를 받지 않거나, 미니맵이 없거나, 움직임이 이상한 경우처럼요. 처음부터 다시 생성하는 대신 --edit 플래그로 무엇이 문제인지 설명하면 목표 지향 수정을 받을 수 있어요.

main 위에 edit_game 함수를 추가해요:

# Step 5 — Edit the game
# The --edit flag lets users describe what's wrong with an existing game
# and get a targeted fix without regenerating from scratch.
def edit_game(html_content: str, user_feedback: str) -> str:
    """Send existing game HTML and user feedback to GLM for a targeted fix."""
    edit_prompt = (
        "The following HTML5 game needs changes based on user feedback. "
        "Apply the requested changes while keeping everything else intact.\n\n"
        f"User feedback: {user_feedback}\n\n"
        f"Current game code:\n```html\n{html_content}\n```\n\n"
        "Return the complete updated HTML file inside a single ```html code fence."
    )
    response = client.chat.complete(
        model="zai-glm-5-2",
        messages=[
            {
                "role": "system",
                "content": (
                    "You are an expert game developer. Apply the user's "
                    "requested changes to the game code. Return the complete, "
                    "updated HTML file. Do not remove working features."
                ),
            },
            {"role": "user", "content": edit_prompt},
        ],
    )
    return extract_html(response.choices[0].message.content)

사용법:

python generate_game.py --edit "enemies don't take damage when I attack them"

이 명령은 기존 game.html을 읽고, 피드백과 함께 GLM에 보내고, 수정된 파일로 덮어써요.

Step 6 — 게임 저장·서빙 (Save and serve the game)

브라우저에서 file:// URL을 여는 것은 JavaScript 실행을 깨뜨릴 수 있는 보안 제한을 트리거해요. 로컬 HTTP 서버가 이를 완전히 피해요.

main 위에 serve_and_open 함수를 추가해요:

# Step 6 — Serve the game locally
# Opening file:// URLs triggers browser security restrictions. A local HTTP
# server avoids this and lets the game run without issues.
def serve_and_open(directory: Path, filename: str, port: int = 8000):
    """Start a local HTTP server and open the game in the default browser."""
    handler = functools.partial(
        http.server.SimpleHTTPRequestHandler, directory=str(directory)
    )
    server = http.server.HTTPServer(("localhost", port), handler)
    url = f"http://localhost:{port}/{filename}"

    print(f"Serving game at {url}")
    print("Press Ctrl+C to stop the server.")
    webbrowser.open(url)
    server.serve_forever()

Step 7 — main에서 모두 묶기 (Tie it all together in main)

Step 1의 main 자리 표시자를 전체 오케스트레이션으로 교체해요. 이 함수는 이전 단계를 모두 묶어요 — 인자를 파싱하고, 생성·리뷰 함수를 호출하고, 편집 모드를 처리하고, 결과를 서빙해요:

def main():
    # Step 7 — Tie it all together in main
    parser = argparse.ArgumentParser(description="Generate or edit an HTML5 game.")
    parser.add_argument(
        "--edit",
        type=str,
        help="Edit an existing game.html. Describe what to fix.",
    )
    args = parser.parse_args()

    output = Path("game.html")

    if args.edit:
        # Step 5 — Edit mode: read existing game and apply fixes
        if not output.exists():
            print(f"Error: {output} not found. Generate a game first.")
            return
        print(f"Editing game: {args.edit}")
        html_content = output.read_text(encoding="utf-8")
        html_content = edit_game(html_content, args.edit)
    else:
        # Step 2 — Craft the prompt
        system_prompt, user_prompt = build_game_prompt(GAME_DESCRIPTION)

        # Step 3 — Generate the game
        print(f"Generating game: {GAME_DESCRIPTION}")
        print("This may take a few minutes...")
        html_content = generate_game(system_prompt, user_prompt)

        # Step 4 — Review and fix
        max_attempts = 5
        for attempt in range(max_attempts):
            print(f"Reviewing game (attempt {attempt + 1}/{max_attempts})...")
            issues = review_game(html_content)
            if issues is None:
                print("Review passed.")
                break
            print(f"Issues found:\n{issues}")
            print("Fixing issues...")
            html_content = fix_game(html_content, issues)
        else:
            print(f"Applied {max_attempts} rounds of fixes. Saving best result.")

    output.write_text(html_content, encoding="utf-8")
    print(f"Game saved to {output.resolve()}")

    # Step 6 — Serve the game locally
    serve_and_open(output.resolve().parent, output.name)

실행 (Run)

스크립트를 실행해요:

python generate_game.py

스크립트가 GLM을 호출하고, 깨진 메커니즘에 대해 게임을 리뷰하며, 발견한 이슈를 수정하고(최대 5라운드), HTML을 game.html에 저장하고 브라우저에서 열어요.

예시 출력:

Generating game: A top-down dungeon crawler. The player navigates procedurally generated rooms...
This may take a few minutes...
Reviewing game (attempt 1/5)...
Issues found:
3. Combat: The attack function does not check collision against enemies. Pressing spacebar sets an attack flag but no damage is applied.
4. Spawning: Enemies are placed at random positions without checking for wall overlap.
Fixing issues...
Reviewing game (attempt 2/5)...
Review passed.
Game saved to /Users/you/glm_game_generator/game.html
Serving game at http://localhost:8000/game.html
Press Ctrl+C to stop the server.

기존 게임의 특정 문제를 수정하려면:

python generate_game.py --edit "the minimap doesn't update when I move to a new room"

다른 게임 시도 (Try different games)

GAME_DESCRIPTION을 바꿔 다른 게임을 생성할 수 있어요. 몇 가지 아이디어:

Space shooter:

GAME_DESCRIPTION = (
    "A vertical-scrolling space shooter. The player controls a ship at the bottom "
    "of the screen, moves left/right with arrow keys, and shoots with spacebar. "
    "Waves of enemy ships descend from the top with different movement patterns. "
    "Power-ups drop from destroyed enemies: rapid fire, shield, triple shot. "
    "Track score and display a high-score counter."
)

Breakout clone:

GAME_DESCRIPTION = (
    "A Breakout/Arkanoid clone. The player controls a paddle at the bottom with "
    "left/right arrow keys. A ball bounces around the screen destroying colored "
    "bricks. Different brick colors take different numbers of hits. Some bricks "
    "drop power-ups: wider paddle, multi-ball, sticky paddle. Include 3 levels "
    "with different brick layouts."
)

Tower defense:

GAME_DESCRIPTION = (
    "A tower defense game. Enemies follow a winding path from the top-left to "
    "the bottom-right. Click on empty tiles adjacent to the path to place towers. "
    "Three tower types: arrow (fast, low damage), cannon (slow, splash damage), "
    "and ice (slows enemies). Earn gold from defeated enemies to buy more towers. "
    "Survive 10 waves with increasing difficulty."
)

요약 (Summary)

이 쿡북은 자동 품질 검사가 있는 코드 생성 엔진으로 GLM을 사용하는 방법을 보여줬어요 — 상세한 게임 설명을 보내고, 깨진 메커니즘에 대해 출력을 리뷰하고, 이슈를 자동으로 수정하며, 목표 지향 편집으로 기존 게임을 반복 개선해요.

만든 것:

  • 텍스트 설명을 플레이 가능한 HTML5 Canvas 게임으로 바꾸는 게임 생성 스크립트
  • 일반적인 실패 모드(깨진 충돌, 처치 불가능한 적, 잘못된 스폰)를 방지하는 엔지니어링 요구사항이 있는 프롬프트 구조
  • mistral-medium-latest가 게임을 QA하고 GLM이 이슈를 패치하는 자동 리뷰-수정 루프
  • 처음부터 다시 생성하지 않고 특정 문제를 고치는 편집 모드(--edit)
  • 브라우저 보안 제한 없이 생성된 게임을 서빙하는 로컬 HTTP 서버

사용한 Mistral 기능:

  • 코드 생성을 위한 zai-glm-5-2 모델의 Chat completions API
  • 코드 리뷰를 위한 mistral-medium-latest의 Chat completions API
  • 구조화된 프롬프팅을 위한 시스템·사용자 메시지 역할
  • 긴 코드 생성을 위한 확장 타임아웃(timeout_ms 및 httpx.Timeout)

더 알아보기 (Learn more)