Skip to main content
Summary

TL;DR
SimCity(1989) C 코드를 5.3-codex 에이전트에 맡기고 4일 만에 브라우저 포팅이 나왔다.1
핵심은 "AI를 믿기"가 아니라 "원본을 오라클로 두고 property-based test로 동일성 검증"이다.1
이제 병목은 코딩 속도가 아니라, 정확한 사양(spec)과 검증 체계를 만드는 능력이다(=vibe coding의 현실).1

핵심 아이디어

에이전트 포팅의 요지는 "레거시를 이해해서 재구현"이 아니라 "레거시를 실행 가능한 참조 구현(reference)으로 만들고, 새 구현을 그 참조와 계속 맞춰가며 수렴"시키는 방식이다.1 사람은 코드 대부분을 읽지 않고, 테스트와 실패 케이스가 안내하는 방향으로 최소한만 조정(steering)한다.1

여기서 property-based test는 단순 회귀 테스트보다 강하다. 입력 공간을 넓게 흔들어 "원본 C와 TS 포트의 동작이 같은 성질(property)을 만족하는지"를 자동으로 계속 증명하려 한다. AI는 구현을 빨리 뽑아내고, 검증이 핸들(steering wheel)이 된다.1

'vibe coding'은 "대충 감으로 코딩"이 아니라 "모델이 코드를 밀어붙이게 두되, 인간은 무엇이 맞는지(사양)와 어떻게 틀렸는지(검증)를 제공하는 역할로 이동"한다는 뜻에 가깝다. 결국 인간의 병목은 구현력이 아니라 명세력이다.1

왜 가능했나(워크플로)

SimCity의 소스는 원래 어셈블리에서 C로 옮겨진 형태라 가독성이 매우 낮고(비트시프트 중심, 디컴파일에 가까운 스타일), 사람이 "읽고 이해해서" 포팅하기엔 비용이 크다.1 이때 접근을 바꾸면 된다.

먼저 원본 C를 브라우저/런타임에서 호출할 수 있게 "브리지(FFI/wasm 래퍼)"를 만든다. 이 브리지가 곧 정답 생성기(oracle)다. 그 다음 TS 포트를 만들고, property-based test로 (TS 결과 == C 결과)를 대량으로 검증한다. 실패하면 실패 케이스를 AI에게 다시 먹여 수정하고, 통과할 때까지 반복한다.1

실무 적용 체크리스트

레거시를 "오라클"로 쓸 수 있게 최소 실행 단위를 먼저 뽑아낸다(순수 함수에 가까운 API로 자르기).

브리지 계층을 먼저 만든다(C/COBOL/기타 → wasm/FFI → JS/TS). 포팅보다 "비교 가능한 호출"이 선행이다.

동일성의 정의를 문장으로 쓴다. 예: "같은 입력 상태에서 tick 결과(상태/이벤트/에러)가 동일".

property-based test를 기본 루프로 둔다. 랜덤 입력 생성 + 시드 고정 + 실패 케이스 최소화(shrinking).

결정성(determinism)을 강제한다. RNG 시드, 시간, 부동소수 처리, 플랫폼 의존 동작을 통제한다.

관찰 지점(observable)을 설계한다. 전체 메모리 덤프 대신 핵심 상태 해시/요약 구조를 만들어 비교 비용을 낮춘다.

AI에게는 "코드"보다 "테스트 실패 로그 + 명세 + 최소 재현 입력"을 준다. 수정 단위도 작게(모듈별) 끊는다.

통과 후 리팩터링은 마지막에 한다. 먼저 동치(behavioral equivalence)를 확보하고, 이후 가독성/성능을 손댄다.

간단 코드 예시(TypeScript 중심, 의사코드)

아래 흐름은 "원본 C(wasm) 호출 브리지 → TS 포트 → property-based로 동치 검증" 패턴만 보여준다.

// c_bridge.ts: 원본 C를 wasm으로 컴파일해 호출한다고 가정(의사코드)
export type SimState = { seed: number; city: Uint8Array; funds: number; /* ... */ };
export type StepResult = { next: SimState; events: string[] };

export class COracle {
  constructor(private wasm: any) {}

  step(state: SimState, action: number): StepResult {
    // 실제로는 state를 wasm 메모리에 serialize하고, C 함수 호출 후 deserialize
    // e.g. wasm.exports.sim_step(ptrState, action) -> ptrOut
    const out = this.wasm.exports.sim_step(serialize(state), action);
    return deserialize(out);
  }
}
// ts_port.ts: AI가 생성/수정해나가는 대상(의사코드)
import type { SimState, StepResult } from "./c_bridge";

export function stepTS(state: SimState, action: number): StepResult {
  // TODO: 포팅 구현(처음엔 틀려도 됨)
  // 핵심은 아래 테스트가 오라클(C)과 맞춰주도록 반복 수렴시키는 것
  return { next: { ...state }, events: [] };
}
// equivalence.test.ts: property-based test로 동일성 검증
import fc from "fast-check";
import { COracle } from "./c_bridge";
import { stepTS } from "./ts_port";

const oracle = new COracle(await loadWasm()); // 실제 프로젝트에선 setup 단계로

// 비교를 위해 관찰 가능한 요약을 만든다(전체 구조 비교가 부담이면 해시/핵심 필드만).
function fingerprint(r: { next: any; events: any }) {
  return {
    funds: r.next.funds,
    cityHash: hashBytes(r.next.city),
    events: r.events,
  };
}

test("TS port matches original C behavior (one step)", () => {
  fc.assert(
    fc.property(
      fc.record({
        seed: fc.nat(),
        funds: fc.integer({ min: 0, max: 1_000_000 }),
        city: fc.uint8Array({ minLength: 64, maxLength: 64 }), // 예시: 고정 길이로 결정성↑
      }),
      fc.integer({ min: 0, max: 255 }), // action opcode 예시
      (state, action) => {
        const c = oracle.step(state as any, action);
        const ts = stepTS(state as any, action);

        expect(fingerprint(ts)).toEqual(fingerprint(c));
      }
    ),
    { numRuns: 500, seed: 42 } // 재현 가능하게
  );
});

인용/출처

Garry's List, "AI Just Ported SimCity in 4 Days Without Reading the Code"1

참고

1AI Just Ported SimCity in 4 Days Without Reading the Code

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기