diff --git a/examples/games.cfg b/examples/games.cfg new file mode 100644 index 000000000..b0f991227 --- /dev/null +++ b/examples/games.cfg @@ -0,0 +1,14 @@ +global + default-path config + tune.lua.bool-sample-conversion normal + # load all games here + lua-load lua/trisdemo.lua + +defaults + timeout client 1h + +# map one TCP port to each game +.notice 'use "socat TCP-CONNECT:0:7001 STDIO,raw,echo=0" to start playing' +frontend trisdemo + bind :7001 + tcp-request content use-service lua.trisdemo diff --git a/examples/lua/trisdemo.lua b/examples/lua/trisdemo.lua new file mode 100644 index 000000000..7156df98d --- /dev/null +++ b/examples/lua/trisdemo.lua @@ -0,0 +1,250 @@ +-- 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)