Khám Phá ANSI Escape Code Và Cách Đóng Gói Trong Lua - nói dối e blog

Khám Phá ANSI Escape Code Và Cách Đóng Gói Trong Lua

Gần đây, tôi nảy ra một ý tưởng thú vị và quyết định xây dựng một bản prototype đơn giản. Vì dự án liên quan đến tương tác người-máy, tôi cần vẽ một số yếu tố đồ họa cơ bản trên màn hình. Dù hiện nay có rất nhiều công cụ hỗ trợ, nhưng tôi muốn thử nghiệm một cách tiếp cận hoài cổ hơn: sử dụng các ký tự ASCII trong môi trường console.

Hành trình tìm về quá khứ

Trong vài năm trở lại đây, tôi đắm chìm vào các trò chơi kiểu RogueLike. Điều này khiến tôi nhớ lại những ngày tháng tuổi thơ với chiếc máy tính Apple ][. Tôi muốn tái hiện cảm giác ấy bằng cách tạo ra các hình ảnh ASCII trong console, không dùng bất kỳ thư viện đồ họa phức tạp nào như ncurses. Mục tiêu là thiết lập môi trường phát triển chỉ trong vài phút.

Giải pháp ANSI Escape Code

Lựa chọn tối ưu chính là sử dụng ANSI Escape Code thông qua luồng xuất chuẩn. Đặc biệt, các chuỗi CSI (Control Sequence Introducer) cho phép kiểm soát vị trí con trỏ, vượt qua giới hạn xuất văn bản theo dòng truyền thống.

Các lệnh CSI phổ biến bao gồm:

  • \x1b[n;mH: Di chuyển con trỏ đến tọa độ cụ thể
  • \x1b[nJ: Xóa màn hình
  • \x1b[n(A/B/C/D): Di chuyển con trỏ theo hướng
  • \x1b[s/\x1b[u: Lưu/trả lại vị trí con trỏ

Đóng gói trong Lua

Tôi đã tạo một module Lua đơn giản với hai API chính:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
-- cursor.lua
local C = {}
local write = io.write

function C.clear()
  write "\x1b[2J"
end

local function drawline(line)
  write("\x1b[s", line, "\x1b[u\x1b[B")
  return drawline
end

-- ... (các hàm hỗ trợ khác)

return C

Ví dụ sử dụng:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local c = require "cursor"
c.clear()
c.draw(3,4)
  " XXXXXXX"
  "X    X"
  "X    X"
  "X    X"
  "X    X"
  " XXXXXXX"
-- ... (vẽ các phần khác)

Tạo hiệu ứng động

Để tạo hoạt ảnh, tôi sử dụng coroutine với framework đơn giản:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
local function run(main)
  local term = {}
  local co = coroutine.create(function()
    main()
    return term
  end)
  while true do
    c.clear()
    local ok, err = coroutine.resume(co)
    if not ok then error(err) end
    if err == term then break end
    os.execute "sleep 0.1"
  end
end

Xử lý đầu vào bàn phím

Vấn đề lớn nằm ở stdin:

  1. Chế độ canonical của terminal yêu cầu nhấn Enter mới nhận dữ liệu
  2. Không có API chuẩn để thiết lập chế độ không chặn

Giải pháp: Viết chương trình C chuyển terminal sang chế độ non-canonical, đồng thời tạo thread định kỳ gửi tín hiệu. Trên Windows cần xử lý riêng biệt do không hỗ trợ POSIX.

Cải tiến hàm run:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
local function run(main)
  local term = {}
  local co = coroutine.create(function()
    main()
    return term
  end)
  while true do
    local keys = io.read "l"
    c.clear()
    local ok, err = coroutine.resume(co, key)
    if not ok then error(err) end
    if err == term then break
  end
end

Kết luận

Với bộ công cụ nhỏ gọn này, bạn có thể thỏa sức sáng tạo các trò chơi kiểu RogueLike trên mọi nền tảng chỉ với Lua. Điều tuyệt vời nhất? Bạn không cần bất kỳ thư viện đồ họa phức tạp nào - chỉ cần nắm vững sức mạnh của ANSI Escape Code và sự linh hoạt của Lua!

0%