Thủ thuật
Lập trình viên dùng Claude Code chuyển thể game Amiga năm 1993 sang Godot
(giờ Việt Nam)
Tóm tắt AI
Tác giả đã sử dụng Claude Fable 5 để chuyển đổi 34.000 dòng C++ và 72.000 dòng mã Assembly của tựa game Babylonian Twins sang Godot 4 chỉ trong một đêm, đồng thời tích hợp bản gốc vào phiên bản mới.
Bản dịch AI

Năm 1993, tại Baghdad, tôi đã xây dựng một trò chơi có tên là Babylonian Twins trên máy Amiga 500: RAM 512KB, không có ổ cứng, cắm trực tiếp vào TV. Khi đó tôi là sinh viên kỹ thuật ngoài hai mươi tuổi. Toàn bộ là mã assembly 68000 thuần túy, từng sprite và từng dòng quét (scanline) đều được làm thủ công. Murtadha Salman vẽ đồ họa và Mahir AlSalman sáng tác nhạc. Chúng tôi sống dưới lệnh cấm vận. Không internet, không tài nguyên phát triển game, chỉ có duy nhất một bản sao cuốn Amiga Hardware Reference Manual, thứ mà tôi dùng để lập trình trực tiếp cho phần cứng, cùng với điện chỉ có vài giờ mỗi ngày. Việc liên tục phải thay đĩa mềm (do bộ nhớ quá nhỏ) và những mùa hè nóng 50°C đã khiến ổ đĩa của tôi hỏng tới ba lần.
Trái: 1993, trên máy Amiga. Phải: 2026, cùng một cổng vào (gateway).
Trên Amiga, "làm thủ công" có nghĩa là trò chơi không yêu cầu hệ điều hành thực hiện bất cứ điều gì trong khi nó đang chạy. Khi khởi động, nó lưu các vector ngắt (interrupt vectors), tắt các ngắt của hệ điều hành và chiếm quyền kiểm soát toàn bộ máy:
"Lấy hệ thống từ AMIGA" là dòng chú thích của tôi từ năm 1993. Kể từ thời điểm đó, màn hình hiển thị là danh sách copper (copper list - bộ đồng xử lý video có thể lập trình của Amiga) của riêng trò chơi, được viết lại ngay lập tức cho các sprite và màu bầu trời. Các ô gạch (tiles) di chuyển bằng cách ghi trực tiếp vào các thanh ghi của blitter và chờ cờ báo hoàn thành của nó. Cần điều khiển (joystick) được đọc trực tiếp từ cổng phần cứng, và nút bắn là một chân trên chip CIA. Hệ điều hành chỉ quay lại giữa các màn chơi để tải tệp tin của màn tiếp theo từ đĩa, sau đó lại bị tắt đi.
Đây là trò chơi thương mại đầu tiên được thực hiện tại Iraq, và trong một thời gian dài, rất ít người có cơ hội chơi nó. Commodore sụp đổ và các lệnh cấm vận khiến các nhà phát hành e ngại, vì vậy trò chơi hoàn thiện cứ nằm mãi trên kệ. Một diễn đàn Amiga đã tìm thấy nó vào năm 2008 từ các video YouTube do anh trai tôi tải lên và truy tìm tôi để lấy các đĩa game; chủ đề đó vẫn còn tồn tại đến tận bây giờ.
Trò chơi đã từng được chuyển hệ (port) một lần trước đó, cũng bằng phương pháp thủ công vào năm 2010. Cùng một đội ngũ đã xây dựng lại nó cho iPhone trên một engine viết từ con số 0, với khoảng 34.000 dòng C++, mất nhiều tháng làm việc vào buổi tối và cuối tuần. Apple và Google đã giới thiệu nó, và trò chơi đạt hơn hai triệu lượt tải xuống. Câu chuyện đó nằm ở đây.
Tôi không thực hiện bản port này. Tôi yêu cầu nó, chơi thử kết quả mỗi tối, chỉ ra những điểm cảm thấy chưa ổn và đưa ra vài quyết định cần đến người đã ở đó vào năm 1993. Các định dạng tệp và việc đọc mã assembly là công việc của AI, cũng như các quyết định về cách chuyển đổi đoạn mã đã ba mươi năm tuổi, và nó diễn ra nhanh hơn cả tốc độ tôi có thể theo kịp. Bài viết này là những gì tôi phát hiện ra khi ngồi xuống vài tuần sau đó và đọc lại những gì đã được thực hiện với trò chơi của chính mình. Một vài phần trong đó bị sai, và tôi đã không nhận ra trong suốt nhiều tuần.
Tại sao tôi lại thử lần nữa
Tôi đã từng thử việc này trước đây. Khoảng một năm trước, tôi đưa cho một mô hình đời đầu cùng những tài liệu Amiga đó và yêu cầu nó giải mã các bản đồ màn chơi nhị phân của tôi. Cuối cùng nó cũng làm được, nhưng phải mất vài vòng và rất nhiều gợi ý từ tôi.
Sau đó, Claude Fable 5 ra mắt, và tôi đưa cho nó cùng những tệp tin đó.
Bài kiểm tra này là có chủ đích. Tôi đoán rằng có rất ít mã assembly Amiga trong các tập dữ liệu huấn luyện LLM. Nếu mô hình giỏi trong việc tự tìm hiểu hơn là ghi nhớ, thì đây chính là nơi nó sẽ thể hiện.
Kỳ nghỉ cuối tuần ngày 4 tháng 7 đang đến gần, vì vậy tôi lên kế hoạch ba bước, mỗi bước phụ thuộc vào việc bước trước đó thành công.
Bước một, yêu cầu an toàn: engine 2010 của chính tôi, với 34.000 dòng C++, được chuyển vào Godot 4. Đây là phần kiểm soát.
Bước hai, yêu cầu khó: 72.758 dòng mã assembly 68000 gốc, dành cho một cỗ máy đã ngừng sản xuất, không có chú thích đáng kể và không có điểm chung nào với mã C++. Xây dựng lại nó trong Godot, ở tốc độ 50 Hz gốc của Amiga.
Bước ba, yêu cầu tham vọng: đặt cái thứ hai vào bên trong cái thứ nhất, để khi mua trò chơi hiện đại, bạn sẽ nhận được bản gốc năm 1993 như một phần bổ sung có thể khởi chạy.
Cả ba bước đều thành công. Định dạng màn chơi mà một năm trước phải mất vài vòng và sự chỉnh sửa của tôi mới xong, nay đã hoàn thành chỉ trong một lần chạy mà không cần bất kỳ gợi ý nào từ tôi.
Cách nó được vận hành
Tôi chạy nó trong Claude Code, vì vậy nó có một terminal và hệ thống tệp của tôi. Nó có thể chỉnh sửa tệp, chạy trình hợp dịch (assembler), xây dựng trò chơi, khởi chạy trò chơi và đọc kết quả trả về. Khi tôi nói bên dưới rằng nó đã xây dựng lại các tệp nhị phân năm 1993 của tôi và kiểm tra chúng, nó đã làm điều đó bằng cách chạy vasm và so sánh sự khác biệt (diff) của đầu ra.
Ngay từ đầu, nó đã thêm một bộ cờ lệnh (command-line flags) vào trò chơi để nó có thể tự chơi mà không cần tôi:
Điều này biến câu hỏi "cú nhảy có cảm giác đúng không" thành thứ mà máy tính có thể đọc được:
Nó cũng có hai bài kiểm tra headless (không giao diện) mà nó có thể chạy trước khi hiển thị cho tôi bất cứ thứ gì: một bài kiểm tra biên dịch mọi script, và một bài kiểm tra xây dựng mọi màn chơi rồi báo cáo lỗi. Về phía Amiga, nó điều khiển bộ công cụ thực tế, dùng vasm để hợp dịch và FS-UAE để khởi động kết quả. Những gì chưa được tự động hóa: không có so sánh hình ảnh trên bản port hiện đại (nó chụp ảnh màn hình, tôi tự xem), và không có gì kiểm tra xem trò chơi có mang lại cảm giác đúng hay không.
Bước một: 34.000 dòng C++ trong một buổi tối
Tối thứ Tư, yêu cầu an toàn. Dấu thời gian, không chỉnh sửa:
Hai mươi mốt phút từ một dự án trống rỗng đến một nhân vật có thể điều khiển được. Mỗi dòng mã nó di chuyển đêm đó là dòng mã tôi đã viết, trong nhiều tháng, vào năm 2010. Tôi đi ngủ trong sự bối rối.
Mất khoảng ba ngày sau đó để làm cho cảm giác trở nên đúng đắn: các đường cong nhảy và thời gian của tấm bạt lò xo, cùng với tính năng phát hiện va chạm giúp ích cho việc nhấn nút liên tục, được sửa theo từng đợt vào ngày 2, 3 và 4 tháng 7.
Tôi không kiểm tra một mình. Con trai mười ba tuổi của tôi đã chơi mọi bản dựng cùng tôi. Thằng bé luôn biết tôi đã tạo ra trò chơi này, đó là một sự thật về cha nó mà nó lớn lên cùng, nhưng nó chưa bao giờ thấy tôi làm việc trên đó. Việc kiểm tra đã trở thành một hoạt động cha con mà tôi không hề lên kế hoạch trước, và đó là một trong những phần tôi yêu thích nhất của toàn bộ dự án.
Cùng đơn vị, cùng nhịp (tick)
Tất cả trạng thái trò chơi đều nằm trong các đơn vị ô gạch (1.0 = một ô 48px), và cập nhật chạy ở tốc độ cố định 60 Hz, vì bản dựng iOS năm 2010 chạy ở 60 Hz. Điều đó quan trọng vì bản gốc áp dụng lực cản theo hệ số nhân, mỗi khung hình:
Nhân với 0.85 sáu mươi lần một giây và bạn có một mức độ ma sát; nhân năm mươi lần một giây và bạn có mức khác. Chuyển nó sang một tốc độ nhịp khác và mọi đường cong gia tốc trong trò chơi đều thay đổi. Không có gì bị treo cả, chỉ là cảm giác sẽ sai lệch mãi mãi, và bạn sẽ không tìm ra lỗi đó bằng cách đọc tệp diff. Ở 60 Hz, các hằng số được chuyển nguyên văn. Đây cũng là lý do tại sao bản dựng lại năm 1993 chạy ở 50 Hz còn bản hiện đại chạy ở 60: hai bộ số được tinh chỉnh thủ công, mỗi bộ chỉ đúng ở tốc độ nhịp của riêng nó. Nó giữ cả hai đồng hồ. Tôi đã từng rất muốn gộp chúng lại thành một.
Nó không sử dụng CharacterBody2D
Godot cung cấp CharacterBody2D và move_and_slide, và mọi hướng dẫn đều bảo bạn nên sử dụng chúng. Bản port này không dùng cả hai cho nhân vật người chơi. Bản gốc có mã di chuyển tự viết riêng, và việc xây dựng lại mã đó trên hệ thống vật lý của người khác sẽ tạo ra cảm giác sai lệch theo những cách rất khó để truy vết. Người chơi là một Node2D đơn thuần, và quy trình va chạm 150 dòng đã được chuyển sang từng dòng một, bao gồm cả những con số "lấp liếm" (fudge numbers) mà tôi chọn theo cảm tính mười lăm năm trước và những dòng chú thích tôi viết cho bản thân trong tương lai:
Không có gì làm sạch con số 0.49 lẻ loi đó. Không có bài kiểm tra và không có tài liệu; những chú thích đó chính là đặc tả kỹ thuật.
Bước hai: mã assembly 68000
Đến chiều Chủ nhật, ngày 5 tháng 7, tôi bàn giao thứ mà tôi thực sự muốn kiểm tra. 72.758 dòng trên 26 tệp, được viết cho một cỗ máy có 512 KB bộ nhớ, bởi tôi, cho tôi, với thói quen chú thích của một người chưa bao giờ nghĩ rằng sẽ có người khác đọc nó. Không tài liệu. Một lần chuyển dữ liệu sang bộ nhớ hiện đại năm 2008 đã làm ngắn mọi tên tệp dài, vì vậy mọi lệnh include đều trỏ đến những cái tên không còn tồn tại. Một trong năm tệp nguồn màn chơi bị cắt ngang giữa chừng một bảng dữ liệu. Không còn bản sao nào khác.
Trước khi chuyển đổi bất cứ thứ gì, nó đã làm cho các nguồn năm 1993 có thể hợp dịch trở lại, sử dụng vasm trên máy Mac Apple Silicon, và tiếp tục cho đến khi đầu ra giống hệt từng byte với các tệp nhị phân đã phát hành.
Mười lăm phút từ một thư mục tệp tin đến bản dựng lại đầu tiên khớp với các byte đã phát hành. Tôi đã viết những thứ này trong ASM-One, phương ngữ của nó khác với vasm theo những cách làm thay đổi byte: ASM-One mã hóa cmp #4,d0 thành CMPI, vasm chọn một cách mã hóa khác, dù cũng hợp lệ, nên việc bảo nó không tối ưu hóa là cần thiết nhưng chưa đủ. Thay vì chỉnh sửa các nguồn của tôi, nó đã viết một bước tiền xử lý để bắc cầu qua năm sự khác biệt như vậy, và xây dựng lại tệp ánh xạ tên tệp bị hỏng từng tệp một.
Phần tốn kém nhất là org. Không có trình liên kết (linker) và không có tái định vị (relocation), nguồn màn chơi bố trí bộ nhớ của Amiga bằng tay, từng địa chỉ một:
Bản đồ màn một sử dụng 74.400 trong số 74.752 byte đó, với biên độ 352, và không có gì kiểm tra nó ngoại trừ tôi, vào năm 1993. (SahamR-grb gắn hành vi của một đối tượng dưới dạng một offset có tên; saham trong tiếng Ả Rập nghĩa là mũi tên.) Lệnh org của ASM-One cũng có thể di chuyển bộ đếm vị trí lùi lại, điều mà vasm không làm được. Cách giải quyết đầu tiên đã làm sai một trường hợp: một lệnh ds.b 800 bên trong một khối bị tua ngược, thứ mà ASM-One xử lý là "bỏ qua 800 byte", lại được ghi ra thành 800 byte số không. Mọi thứ sau điểm đó trong tệp, bao gồm cả danh sách copper, đều nằm cách vị trí trong tệp nhị phân gốc 944 byte. Trò chơi đã hợp dịch và khởi động được, nhưng hiển thị sai.
Ngay cả sau đó, một số đoạn vẫn không khớp, lệch khoảng 108 byte rải rác trong vùng biến. Những byte đó giải thích nguồn gốc của các tệp đã phát hành. ASM-One hợp dịch vào bộ nhớ, và trò chơi được đưa vào đĩa bằng cách lưu bộ nhớ đó ra sau khi trò chơi đã chạy. Vì vậy, các tệp đã phát hành là một bản chụp nhanh của một trò chơi đã chạy trước đó, không phải là đầu ra sạch từ trình hợp dịch. Một bản hợp dịch mới sẽ có các số không trong các biến đó, vì chưa có gì thiết lập chúng; đĩa đã phát hành chứa bất cứ thứ gì chúng giữ trên máy khi nó được lưu. Mã nguồn ghi vào chúng trước khi đọc chúng, nên các số không đó vô hại.
Bài viết được AI dịch và tổng hợp tự động từ Hacker News Nổi bật (buzzing.cc bản dịch tiếng Trung). Liên kết bài gốc ở phía trên. Dữ liệu đồng bộ qua API công khai được ghi nguồn tại AI HOT (canonical) ↗. AIHOT.vn luôn dẫn nguồn đầy đủ — nếu bạn thấy điểm cần chỉnh sửa, hãy gửi ý kiến tại trang phản hồi.