haproxy/examples/lua/trisdemo.lua
Willy Tarreau ed1d4807da EXAMPLES: add "games.cfg" and an example game in Lua
The purpose is mainly to exhibit certain limitations that come with such
less common programming models, to show users how to program interactive
tools in Lua, and how to connect interactively.

Other use cases that could be envisioned are "top" and various monitoring
utilities, with sliding graphs etc. Lua is particularly attractive for
this usage, easy to program, well known from most AI tools (including its
integration into haproxy), making such programs very quick to obtain in
their basic form, and to improve later.

A very limited example game is provided, following the principle of a
very popular one, where the player must compose lines from falling
pieces. It quickly revealed the need to the ability to enforce a timeout
to applet:receive(). Other identified limitations include the difficulty
from the Lua side to monitor multiple events at once, but it seems that
callbacks and/or event dispatchers would be useful here.

At the moment the CLI is not workable (it interactivity was broken in 2.9
when line buffering was adopted), though it was verified that it works
with older releases.

The command needed to connect to the game is displayed as a notice message
during boot.
2025-04-01 09:10:00 +02:00

251 lines
8.2 KiB
Lua

-- Example game of falling pieces for HAProxy CLI/Applet
local board_width = 10
local board_height = 20
local game_name = "Lua Tris Demo"
-- Shapes with IDs for color mapping
local pieces = {
{id = 1, shape = {{1,1,1,1}}}, -- I (Cyan)
{id = 2, shape = {{1,1},{1,1}}}, -- O (Yellow)
{id = 3, shape = {{0,1,0},{1,1,1}}}, -- T (Purple)
{id = 4, shape = {{0,1,1},{1,1,0}}}, -- S (Green)
{id = 5, shape = {{1,1,0},{0,1,1}}}, -- Z (Red)
{id = 6, shape = {{1,0,0},{1,1,1}}}, -- J (Blue)
{id = 7, shape = {{0,0,1},{1,1,1}}} -- L (Orange)
}
-- ANSI escape codes
local clear_screen = "\27[2J"
local cursor_home = "\27[H"
local cursor_hide = "\27[?25l"
local cursor_show = "\27[?25h"
local reset_color = "\27[0m"
local color_codes = {
[1] = "\27[1;36m", -- I: Cyan
[2] = "\27[1;37m", -- O: White
[3] = "\27[1;35m", -- T: Purple
[4] = "\27[1;32m", -- S: Green
[5] = "\27[1;31m", -- Z: Red
[6] = "\27[1;34m", -- J: Blue
[7] = "\27[1;33m" -- L: Yellow
}
local function init_board()
local board = {}
for y = 1, board_height do
board[y] = {}
for x = 1, board_width do
board[y][x] = 0 -- 0 for empty, piece ID for placed blocks
end
end
return board
end
local function can_place_piece(board, piece, px, py)
for y = 1, #piece do
for x = 1, #piece[1] do
if piece[y][x] == 1 then
local board_x = px + x - 1
local board_y = py + y - 1
if board_x < 1 or board_x > board_width or board_y > board_height or
(board_y >= 1 and board[board_y][board_x] ~= 0) then
return false
end
end
end
end
return true
end
local function place_piece(board, piece, piece_id, px, py)
for y = 1, #piece do
for x = 1, #piece[1] do
if piece[y][x] == 1 then
local board_x = px + x - 1
local board_y = py + y - 1
if board_y >= 1 and board_y <= board_height then
board[board_y][board_x] = piece_id -- Store piece ID for color
end
end
end
end
end
local function clear_lines(board)
local lines_cleared = 0
local y = board_height
while y >= 1 do
local full = true
for x = 1, board_width do
if board[y][x] == 0 then
full = false
break
end
end
if full then
table.remove(board, y)
table.insert(board, 1, {})
for x = 1, board_width do
board[1][x] = 0
end
lines_cleared = lines_cleared + 1
else
y = y - 1
end
end
return lines_cleared
end
local function rotate_piece(piece, piece_id, px, py, board)
local new_piece = {}
for x = 1, #piece[1] do
new_piece[x] = {}
for y = 1, #piece do
new_piece[x][#piece + 1 - y] = piece[y][x]
end
end
if can_place_piece(board, new_piece, px, py) then
return new_piece
end
return piece
end
function render(applet, board, piece, piece_id, px, py, score)
local output = clear_screen .. cursor_home
output = output .. game_name .. " - Lines: " .. score .. "\r\n"
output = output .. "+" .. string.rep("-", board_width * 2) .. "+\r\n"
for y = 1, board_height do
output = output .. "|"
for x = 1, board_width do
local char = " "
-- Current piece
for py_idx = 1, #piece do
for px_idx = 1, #piece[1] do
if piece[py_idx][px_idx] == 1 then
local board_x = px + px_idx - 1
local board_y = py + py_idx - 1
if board_x == x and board_y == y then
char = color_codes[piece_id] .. "[]" .. reset_color
end
end
end
end
-- Placed blocks
if board[y][x] ~= 0 then
char = color_codes[board[y][x]] .. "[]" .. reset_color
end
output = output .. char
end
output = output .. "|\r\n"
end
output = output .. "+" .. string.rep("-", board_width * 2) .. "+\r\n"
output = output .. "Use arrow keys to move, Up to rotate, q to quit"
applet:send(output)
end
function handler(applet)
local board = init_board()
local piece_idx = math.random(#pieces)
local current_piece = pieces[piece_idx].shape
local piece_id = pieces[piece_idx].id
local piece_x = math.floor(board_width / 2) - math.floor(#current_piece[1] / 2)
local piece_y = 1
local score = 0
local game_over = false
local delay = 500
if not can_place_piece(board, current_piece, piece_x, piece_y) then
game_over = true
end
applet:send(cursor_hide)
-- fall the piece by one line every delay
local function fall_piece()
while not game_over do
piece_y = piece_y + 1
if not can_place_piece(board, current_piece, piece_x, piece_y) then
piece_y = piece_y - 1
place_piece(board, current_piece, piece_id, piece_x, piece_y)
score = score + clear_lines(board)
piece_idx = math.random(#pieces)
current_piece = pieces[piece_idx].shape
piece_id = pieces[piece_idx].id
piece_x = math.floor(board_width / 2) - math.floor(#current_piece[1] / 2)
piece_y = 1
if not can_place_piece(board, current_piece, piece_x, piece_y) then
game_over = true
end
end
core.msleep(delay)
end
end
core.register_task(fall_piece)
local function drop_piece()
while can_place_piece(board, current_piece, piece_x, piece_y) do
piece_y = piece_y + 1
end
piece_y = piece_y - 1
place_piece(board, current_piece, piece_id, piece_x, piece_y)
score = score + clear_lines(board)
piece_idx = math.random(#pieces)
current_piece = pieces[piece_idx].shape
piece_id = pieces[piece_idx].id
piece_x = math.floor(board_width / 2) - math.floor(#current_piece[1] / 2)
piece_y = 1
if not can_place_piece(board, current_piece, piece_x, piece_y) then
game_over = true
end
render(applet, board, current_piece, piece_id, piece_x, piece_y, score)
end
while not game_over do
render(applet, board, current_piece, piece_id, piece_x, piece_y, score)
-- update the delay based on the score: 500 for 0 lines to 100ms for 100 lines.
if score >= 100 then
delay = 100
else
delay = 500 - 4*score
end
local input = applet:receive(1, delay)
if input then
if input == "q" then
game_over = true
elseif input == "\27" then
local a = applet:receive(1, delay)
if a == "[" then
local b = applet:receive(1, delay)
if b == "A" then -- Up arrow (rotate clockwise)
current_piece = rotate_piece(current_piece, piece_id, piece_x, piece_y, board)
elseif b == "B" then -- Down arrow (full drop)
drop_piece()
elseif b == "C" then -- Right arrow
piece_x = piece_x + 1
if not can_place_piece(board, current_piece, piece_x, piece_y) then
piece_x = piece_x - 1
end
elseif b == "D" then -- Left arrow
piece_x = piece_x - 1
if not can_place_piece(board, current_piece, piece_x, piece_y) then
piece_x = piece_x + 1
end
end
end
end
end
end
applet:send(clear_screen .. cursor_home .. "Game Over! Lines: " .. score .. "\r\n" .. cursor_show)
end
-- works as a TCP applet
core.register_service("trisdemo", "tcp", handler)
-- may also work on the CLI but requires an unbuffered handler
core.register_cli({"trisdemo"}, "Play a simple falling pieces game", handler)