본문으로 건너뛰기
Python 기초집LESSON 24

최종 프로젝트: 게임 전투 로그 분석 및 밸런스 리포트 도구

난이도입문
예상 시간30분
선수지식이전 강의

24강. 최종 프로젝트: 게임 전투 로그 분석 및 밸런스 리포트 도구

1. 이번 강의에서 해결할 문제

앞에서 만든 데이터 모델, reader, 통계, 테스트와 CLI를 하나의 실행 흐름으로 연결합니다. 입력 형식과 출력 형식이 달라도 같은 지표를 안정적으로 계산해야 합니다.

2. 학습 목표

3. 핵심 개념

최종 구조는 입력 → 검증 모델 → 분석 → 리포트 직렬화 → CLI로 의존 방향을 한쪽으로 유지합니다. 각 계층은 Python 객체를 주고받고 파일 형식과 argparse는 경계에만 둡니다.

최종 폴더 구조
game-log-analyzer/
├─ battle_analyzer/
│ ├─ __init__.py
│ ├─ models.py
│ ├─ readers.py
│ ├─ analysis.py
│ ├─ writers.py
│ └─ cli.py
├─ data/battles.json
├─ tests/test_analyzer.py
├─ main.py
└─ requirements.txt

4. 단계별 코드 예시

파일 경로: battle_analyzer/analysis.py

battle_analyzer/analysis.py
from collections import defaultdict

from .models import BattleRecord


def metrics(records: list[BattleRecord]) -> dict[str, float | int]:
count = len(records)
wins = sum(record.result == "win" for record in records)
return {
"battle_count": count,
"average_damage": round(
sum(record.damage for record in records) / count, 2
) if count else 0.0,
"win_rate": round(wins / count, 4) if count else 0.0,
}


def group_metrics(records: list[BattleRecord], field: str) -> dict[str, dict]:
groups = defaultdict(list)
for record in records:
groups[getattr(record, field)].append(record)
return {name: metrics(group) for name, group in sorted(groups.items())}


def build_report(records: list[BattleRecord]) -> dict:
return {
"overall": metrics(records),
"players": group_metrics(records, "player"),
"monsters": group_metrics(records, "monster"),
"skills": group_metrics(records, "skill"),
}

파일 경로: battle_analyzer/writers.py

battle_analyzer/writers.py
import csv
import json
from pathlib import Path


def write_report(report: dict, path: Path, output_format: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if output_format == "json":
with path.open("w", encoding="utf-8") as file:
json.dump(report, file, ensure_ascii=False, indent=2)
return

rows = []
for category, groups in report.items():
if category == "overall":
groups = {"all": groups}
for name, values in groups.items():
rows.append({"category": category, "name": name, **values})

with path.open("w", encoding="utf-8", newline="") as file:
writer = csv.DictWriter(
file,
fieldnames=["category", "name", "battle_count", "average_damage", "win_rate"],
)
writer.writeheader()
writer.writerows(rows)

파일 경로: battle_analyzer/cli.py

battle_analyzer/cli.py
import argparse
import logging
from pathlib import Path

from .analysis import build_report
from .readers import read_csv, read_json
from .writers import write_report


def build_parser():
parser = argparse.ArgumentParser(description="게임 전투 로그 분석기")
parser.add_argument("--input", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--format", choices=("json", "csv"), default="json")
parser.add_argument("--verbose", action="store_true")
return parser


def main(argv=None):
args = build_parser().parse_args(argv)
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
try:
reader = {".json": read_json, ".csv": read_csv}[args.input.suffix.lower()]
records = reader(args.input)
report = build_report(records)
write_report(report, args.output, args.format)
except (FileNotFoundError, KeyError, TypeError, ValueError) as error:
logging.error("분석 실패: %s", error)
return 1
logging.info("%d건 분석 완료: %s", len(records), args.output)
return 0

파일 경로: tests/test_analyzer.py

tests/test_analyzer.py
import json

import pytest

from battle_analyzer.analysis import build_report, metrics
from battle_analyzer.models import BattleRecord
from battle_analyzer.readers import read_json
from battle_analyzer.writers import write_report


def record(player="Knight", monster="Slime", skill="Slash", damage=100, result="win"):
return BattleRecord(player, monster, skill, damage, result)


def test_metrics_calculates_average_and_win_rate():
result = metrics([record(damage=80), record(damage=120, result="loss")])
assert result["battle_count"] == 2
assert result["average_damage"] == pytest.approx(100.0)
assert result["win_rate"] == pytest.approx(0.5)


def test_report_groups_player_monster_and_skill():
report = build_report([record(), record(player="Mage", skill="Fireball")])
assert set(report["players"]) == {"Knight", "Mage"}
assert report["monsters"]["Slime"]["battle_count"] == 2
assert report["skills"]["Fireball"]["average_damage"] == 100


def test_json_input_and_output(tmp_path):
source = tmp_path / "battles.json"
output = tmp_path / "report.json"
source.write_text(json.dumps([{
"player": "Knight", "monster": "Slime", "skill": "Slash",
"damage": 80, "result": "win"
}]), encoding="utf-8")
report = build_report(read_json(source))
write_report(report, output, "json")
assert json.loads(output.read_text(encoding="utf-8"))["overall"]["battle_count"] == 1

파일 경로: main.py

main.py
from battle_analyzer.cli import main

if __name__ == "__main__":
raise SystemExit(main())
PowerShell · 최종 실행
Set-Location C:\dev\game-log-analyzer
.\.venv\Scripts\python.exe -m pytest -q
.\.venv\Scripts\python.exe .\main.py --input .\data\battles.json --output .\reports\balance.json --format json
.\.venv\Scripts\python.exe .\main.py --input .\data\battles.csv --output .\reports\balance.csv --format csv --verbose
Get-Content .\reports\balance.json

5. 코드가 동작하는 이유

reader는 두 입력 형식을 BattleRecord로 통일하고 analysis는 파일을 모른 채 통계를 계산합니다. getattr은 지정 필드별 그룹화를 공통화합니다. writer는 동일 리포트를 중첩 JSON 또는 평면 CSV 행으로 직렬화하며 CLI가 전체 흐름과 종료 코드를 조정합니다.

6. 자주 하는 실수와 해결법

7. 직접 실습

8. 이해 점검 질문 3개

9. 핵심 요약

MINI QUIZ

최종 프로젝트: 게임 전투 로그 분석 및 밸런스 리포트 도구 미니 퀴즈

선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.

0 / 2
  1. 문제 1“최종 프로젝트: 게임 전투 로그 분석 및 밸런스 리포트 도구” 실습 결과를 확인할 때 적용해야 할 설명은 무엇인가요?
  2. 문제 2‘테스트가 실제 reports를 덮어씀’ 실수를 판단할 때 “최종 프로젝트: 게임 전투 로그 분석 및 밸런스 리포트 도구” 강의가 제시한 기준은 무엇인가요?
LESSON STATUS

학습을 마쳤나요?

직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.

24강. 최종 프로젝트: 게임 전투 로그 분석 및 밸런스 리포트 도구 미완료 상태