1 điểm bởi GN⁺ 2023-10-09 | 1 bình luận | Chia sẻ qua WhatsApp
  • Flappy Dird là một bản triển khai thử nghiệm chạy Flappy Bird bằng cách dùng danh sách tệp trong macOS Finder làm màn hình, còn thao tác chọn và mở tệp làm đầu vào
  • Ý tưởng cốt lõi bắt đầu từ việc đọc giá trị Date Last Opened của một thư mục, vốn chỉ được cập nhật trong Finder, như một nút bấm, và đổi tên các tệp liên kết tượng trưng để vẽ màn hình
  • Cách render chỉ bằng đổi tên tệp đơn giản thì chậm và bị xé hình nặng, nên sau đó được cải thiện bằng cơ chế double buffering dùng AppleScript để đổi thư mục đích của cửa sổ Finder
  • Đầu vào bằng nhấp đúp không ổn định trên 2 FPS do giới hạn timestamp theo đơn vị giây, nên sau đó được chuyển sang cách đọc mục đang được chọn trong Finder
  • Kiến trúc cuối cùng là AppleScript chạy vòng lặp còn Python xử lý trạng thái game và render, hoạt động ở 4 FPS nhưng vẫn còn hiện tượng bỏ sót đầu vào và các giới hạn riêng của Finder

Dùng Finder làm màn hình game

  • Flappy Dird là một trò chơi chạy trên Finder, có thể xem mã nguồn và tự chạy từ kho GitHub
  • Trò chơi bao gồm hướng dẫn sử dụng, theo dõi điểm cao nhất và banner quảng cáo cuộn
  • Người dùng nhấp đúp để bắt đầu game, rồi chọn bất kỳ tệp nào trong cửa sổ Finder để làm con chim nhảy lên
  • Tốc độ chạy ở mức 4 khung hình/giây, khó có thể chạy nhanh hơn nhiều và đầu vào đôi lúc vẫn bị bỏ sót

Dùng Date Last Opened như một nút bấm

  • Điểm xuất phát là Finder có trường Date Last Opened cho thư mục
  • Có ba hành vi được quan sát
    • Đi vào thư mục bằng cd không làm timestamp được cập nhật
    • Nhấp đúp vào liên kết tượng trưng của thư mục trong Finder sẽ cập nhật timestamp
    • Có thể đọc giá trị đó bằng mdls với độ chính xác theo giây
  • Nhờ đặc tính này, có thể tạo một nút bấm bên trong Finder
    • Tạo liên kết tượng trưng button bên trong thư mục dir, trỏ tới dir
    • Khi bắt đầu, đọc thời điểm mở gần nhất của dir
    • Liên tục polling timestamp, rồi khi giá trị thay đổi thì xử lý như đầu vào
    • Mở button mà không thay đổi vị trí Finder để làm thay đổi thời điểm mở gần nhất của dir

Vẽ màn hình 15x15 bằng tên tệp emoji

  • Dù khó căn được ASCII art với phông chữ mặc định của Finder, tác giả tận dụng việc emoji trong tên tệp được hiển thị với độ rộng cố định
  • Bản prototype tạo 15 liên kết tượng trưng con trong dir, mỗi liên kết lại trỏ về dir, rồi đổi tên từng liên kết thành một hàng emoji để tạo thành màn hình
  • Hàm render nhận các giá trị bird_y_pos, pipe_locations, frame để tạo ra lưới emoji 15x15
  • Ở mỗi khung hình, tên của toàn bộ liên kết tượng trưng trong thư mục được đổi thành từng hàng của lưới emoji
  • Cũng cần một mẹo để điều chỉnh thứ tự đổi tên sao cho Finder vẫn sắp xếp theo Date Modified
  • Cách này có hoạt động, nhưng rất chậm và bị xé hình nghiêm trọng

AppleScript và double buffering

  • Để giảm xé hình, tác giả tìm cách làm mới danh sách tệp trong Finder và dùng lệnh AppleScript tell application "Finder" to tell front window to update every item
  • Lệnh này có giúp đôi chút nhưng vẫn còn xé hình, và từ đó dẫn đến hướng điều khiển trực tiếp Finder bằng AppleScript
  • Có nhiều phương án được cân nhắc cho double buffering
    • Cách thay thế nội dung thư mục một cách nguyên tử bằng liên kết tượng trưng không phù hợp vì Finder không thể bung nội dung thư mục liên kết tượng trưng
    • Cách dùng hai thư mục trỏ lẫn nhau có thể giảm xé hình, nhưng chỉ có thể tiến khung hình khi người dùng nhấp, nên không hợp với game
    • Cần một cách đổi thư mục mà Finder đang hiển thị nhưng không làm thay đổi “thời điểm mở gần nhất”
  • Lời giải là dùng AppleScript tell application "Finder" to set target of front Finder window to ("PATH" POSIX file) để đổi thư mục đích của cửa sổ Finder
  • Khi triển khai double buffering theo cách này, game chạy mượt ở 1 FPS, nhưng tốc độ vẫn còn chậm

Chuyển cách nhập liệu: từ nhấp đúp sang chọn tệp

  • Đầu vào bằng nhấp đúp có giới hạn khi vượt quá 2 FPS vì độ chính xác timestamp chỉ ở mức giây
  • Có thể dùng AppleScript để đọc mục đang được chọn trong cửa sổ Finder, nên đầu vào được đổi thành “chọn bất kỳ tệp nào”
  • Vòng lặp game mới có cấu trúc như sau
    • Chờ cho đến khi người dùng nhấp đúp
    • Ở mỗi khung hình, dùng AppleScript để kiểm tra xem có tệp nào đang được chọn trong cửa sổ hiện tại hay không
    • Nếu có lựa chọn hoặc thời điểm mở gần nhất thay đổi thì con chim nhảy lên
    • Dùng AppleScript để đổi thư mục mà Finder đang hiển thị
    • Chờ trong khoảng thời gian còn lại để khớp với tốc độ khung hình mục tiêu
  • Cách này phần nào đạt được 2 FPS, nhưng mỗi lần gọi AppleScript mất khoảng 0,2 giây, tạo ra chi phí cố định lớn
  • Ngay cả ở 2 FPS, nếu người dùng chọn tệp sau khi kiểm tra đầu vào thì thao tác đó vẫn có thể bị bỏ lỡ

Chuyển vòng lặp chính sang AppleScript

  • Kết quả profiling cho thấy một AppleScript chỉ ghi số 1 vào log rồi thoát mất khoảng 0,14 giây
  • Sau đó có đính chính rằng phép đo này không công bằng để đo thời gian khởi động AppleScript, và với AppleScript rỗng thì có thể gần 0,06 giây hơn
  • Để tránh chi phí khởi động AppleScript ở mỗi khung hình, vòng lặp chính được chuyển sang AppleScript, còn Python phụ trách logic game và render
  • Cấu trúc cơ bản như sau
    • game.py await chờ người dùng nhấp đúp để bắt đầu
    • game.py start-frame ghi lại thời điểm bắt đầu khung hình
    • Đọc số lượng mục được chọn trong Finder rồi truyền vào game.py tick để render khung hình
    • Nhận tên thư mục mà tick đã chuẩn bị và đổi đích cửa sổ Finder sang thư mục đó
    • Dùng game.py sleep để khớp tốc độ khung hình mục tiêu, rồi lặp lại nếu người chơi chưa thua
  • Sau đó start-framesleep có thể được gộp lại, đồng thời thêm vòng lặp để khởi động lại sau khi thua

Những chi tiết cần thiết để hoàn thiện trò chơi

  • Trạng thái game được lưu bằng cách tạo tệp state.json và đọc ghi nó ở mỗi khung hình
  • Văn bản dùng các ký tự có độ rộng hẹp hơn emoji, nên cần rất nhiều khoảng cách hardcode để căn chỉnh
  • Phần chữ banner cuộn ở phía trên đặc biệt khó xử lý, nên tác giả tạo hàm read_n_ad_chars để ước lượng số ký tự có thể hiển thị cùng lúc
  • Thay vì truyền thư mục làm việc cho AppleScript, các biến mẫu được thay thế trong lời gọi Python first-time-setup
  • AppleScript nuốt mất giá trị output của Python, nên log được append vào tệp rồi kiểm tra sau bằng cat
  • Để cảm giác chơi tốt hơn, nếu bấm ở hai khung hình liên tiếp thì con chim sẽ nhảy cao thêm một hàng

Các giới hạn của game Finder làm không cần engine

  • Bản prototype dài khoảng 90 dòng, còn mã cuối cùng khoảng 550 dòng, nhưng khoảng một phần ba trong số đó là boilerplate hoặc hằng số
  • Việc không dùng game engine mà chỉ lưu trạng thái khung hình trong một mảng 2D nhỏ giúp dễ giữ toàn bộ game trong đầu
  • 4 FPS và đầu vào hạn chế là những ràng buộc lớn, nhưng có vẻ vẫn có thể làm các game khác theo cùng cách này
  • Cũng đã có ý tưởng làm Tetris trong Finder, và nó vẫn được xem là một ví dụ khó nhưng có thể thực hiện được

1 bình luận

 
GN⁺ 2023-10-09
Ý kiến trên Hacker News
  • Thật đáng kinh ngạc đến mức cạn lời; dù tôi vẫn tự coi mình là người biết nghĩ vượt ra ngoài khuôn khổ, nếu trước khi đọc bài này thì chắc tôi đã nói là không thể
    Thật tuyệt khi thấy tác giả tự đặt thử thách cuối tuần rồi đẩy nó đến mức chứng minh được là thực sự khả thi, và bài viết cũng rất thú vị

    • Vì thế trong phần mềm, tôi không bao giờ nói không thể. Chỉ cần có thời gian và óc sáng tạo
  • Bob vội vã lấy áo khoác và ba lô để theo cả nhóm đi uống bia chiều thứ Sáu, mà không nhận ra mình đã để Flappy Bird chạy trên MacOS
    Trong suốt kỳ nghỉ cuối tuần có ngày lễ ngân hàng, tuổi thọ ghi của SSD laptop bị tiêu hao với tốc độ 4 khung hình mỗi giây, và đến thứ Ba khi quay lại, Bob thấy SSD không còn hoạt động nữa

    • Tôi tự hỏi liệu một người đến mức chạy Flappy Dird ở công ty có thật sự có khả năng vội vã như vậy không
    • Dù vậy, với mức đó thì tôi vẫn kỳ vọng ổ đĩa có thể chạy vài năm mà không chết
  • Tôi lại thích cách “Đây là ảnh. Giờ tôi không biết nhúng tweet thế nào nữa” hơn nhiều

    • Làm vậy thì bài viết được lưu giữ lâu hơn. Nội dung nhúng có thể biến mất hoặc hỏng bất cứ lúc nào
    • Đồng ý, từ khi chặn Twitter, tôi thường thấy chỗ nhúng chẳng hiện gì, hoặc chỉ render mỗi emoji ở giữa một vùng trống
  • Tôi tò mò liệu OP có thể giải quyết vấn đề tốc độ khởi động AppleScript bằng cách viết lại bằng JavaScript không
    Từ vài năm trước, framework mặc định và công cụ dòng lệnh osascript đã hỗ trợ AppleScript và JavaScript gần như ngang nhau, và trong JavaScript có khá nhiều thứ nằm trong đối tượng $

    • Tôi hoàn toàn không biết chuyện này, phải thử mới được. Trong vòng 5 phút mà nhận được tới hai bình luận rất hữu ích về AppleScript, tuyệt thật
  • Nếu muốn gọi sự kiện AppleScript một cách native từ Python, cũng có thể thử py-appscript
    https://github.com/hhas/appscript/tree/master/py-appscript
    Tôi mới chỉ dùng bản Appscript cho Ruby, nhưng nếu bản Python hoạt động tương tự thì nó có vẻ là lựa chọn lý tưởng đến mức không cần chính AppleScript nữa

    • Tôi thậm chí còn không nghĩ đến việc kiểm tra xem có thứ như vậy không, nhất định sẽ xem thử. Dù vậy, cũng có chút thú vị từ việc vòng lặp cốt lõi là AppleScript
  • Nếu bạn thích kiểu game ở những nơi lẽ ra không nên có game, còn có những dự án vô lý hơn nữa
    Fontemon: game trong phông chữ https://www.coderelay.io/fontemon.html
    Dungeons & Directories: text adventure trong trình duyệt tệp https://wheybags.com/dungeons_and_directories/ — cái này do tôi làm

    • Cảm ơn cả hai link. Việc chọn hệ thống tệp cũng thú vị, và khi quay lại máy Windows sau vài tuần nữa tôi muốn xem thử
      Tôi cũng đã nghịch một chút với game trong phông chữ, và để xem cảm giác viết một phông chữ như thế nào, tôi đã làm Hexagone[1]. Tôi chưa chắc nên làm game gì, nhưng thật vui khi có được kiểu cảm hứng như vậy
      [1] http://eieio.games/nonsense/hexagone-converting-hex-to-rgb-w...
    • Còn có Defender of the Favicon: http://www.p01.org/defender_of_the_favicon/
    • Cách triển khai thứ hai thật tài tình. Nhưng tôi tò mò liệu bạn có cân nhắc triển khai bằng FUSE không
    • Dành cho dân mê dòng lệnh thì còn có rpg-cli
      https://github.com/facundoolano/rpg-cli
    • Tôi không biết đây là thứ ma thuật từ dị giới nào nữa
  • Tôi rất thích tinh thần “có thể làm tốt hơn nữa” xuyên suốt bài viết. Không biết sẽ thế nào nếu mọi lập trình viên đều có tâm thế này
    Tất nhiên nếu ai cũng vậy thì vì lúc nào cũng có thể làm tốt hơn, sẽ chẳng có gì hoàn thành cả; đôi khi cần có người nói “thế này là đủ rồi”

    • Đúng vậy, cân bằng chuyện đó khá khó
      Với những dự án sở thích như thế này, giữ phía “có thể làm tốt hơn nữa” dễ hơn. Trong công việc, thường phải cân nhắc liệu bỏ thêm một ngày để trau chuốt có đáng hơn mọi việc khác mà chúng ta muốn làm không
      Nhưng với Flappy Dird, tôi muốn tiếp tục cho đến khi hài lòng với dự án, hoặc đến khi tin rằng mình không thể đẩy trò chơi đi xa hơn nữa
  • Nhìn tiêu đề, ban đầu tôi tưởng sẽ là cách dùng AppleScript để di chuyển các icon như sprite trong chế độ xem biểu tượng của cửa sổ Finder
    Hóa ra lại triển khai trong chế độ xem danh sách, và ban đầu là bằng cách polling “ngày mở lần cuối” của thư mục, nên tôi khá bất ngờ; đúng là một cách tiếp cận sáng tạo

  • Tôi rất thích tinh thần của bài này: đưa một game nhỏ vào mọi nơi có thể
    Nó làm tôi nhớ đến widget trên thanh tác vụ Fortune Teller fish từng dùng được trong GNOME ngày xưa
    Hoàn toàn ngoài lề, khi thấy cách viết hoa “MacOS Finder”, ban đầu tôi tưởng đây là một extension cho MacOS 8 hoặc 9, nhưng kết quả thực tế còn thú vị hơn nhiều

  • Thử thách tiếp theo là chạy Game of Life trong Finder, rồi trong đó giả lập một máy Mac chạy Finder

    • Nếu bỏ phần giả lập MacOS và chỉ làm Game of Life, tôi tự hỏi liệu có thể làm bằng tệp DS_Store không
      Ban đầu tạo các thư mục theo mẫu rồi nhấp chuột phải để sắp xếp, chúng sẽ được đặt trên lưới. Sau đó script đọc vị trí thư mục từ tệp DS_Store, cập nhật sang vị trí mới của khung hình tiếp theo, hoặc tạo và xóa thư mục khi cần