Hi, I'd like to make some contributions to examples/lua: 1. update notice message in games.cfg to report trisdemo game name 2. quick fix for trisdemo to avoid "blinking" effect 3. an "invaders" game where we have to shoot aliens falling from the sky 4. a "pong" game, which requires 2 players
Baptiste
From 94577208c1a8decf4d51b4f52a48bc21cb61dfa4 Mon Sep 17 00:00:00 2001 From: Baptiste Assmann <bassm...@haproxy.com> Date: Wed, 23 Apr 2025 11:39:17 +0200 Subject: [PATCH 4/4] NEW: examples/lua: pong like game Requires a couple of palyers to get connected on the same HAProxy and then enjoy a simple pong like game. --- examples/games.cfg | 6 ++ examples/lua/pong.lua | 235 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 examples/lua/pong.lua diff --git a/examples/games.cfg b/examples/games.cfg index 0720c6cef..73ea77f62 100644 --- a/examples/games.cfg +++ b/examples/games.cfg @@ -4,6 +4,7 @@ global # load all games here lua-load lua/trisdemo.lua lua-load lua/invaders.lua + lua-load lua/pong.lua defaults timeout client 1h @@ -18,3 +19,8 @@ frontend trisdemo frontend invaders bind :7002 tcp-request content use-service lua.invaders + +.notice 'use "socat TCP-CONNECT:0:7003 STDIO,raw,echo=0" to start playing pong (requires 2 players)' +frontend pong + bind :7003 + tcp-request content use-service lua.pong diff --git a/examples/lua/pong.lua b/examples/lua/pong.lua new file mode 100644 index 000000000..84163b322 --- /dev/null +++ b/examples/lua/pong.lua @@ -0,0 +1,235 @@ +-- Pong game in Lua for HAProxy 3.2’s TCP service + +local debug = false -- Set to true for debug output, false to disable + +local game = { + width = 40, + height = 20, + ball = { x = 40, y = 15, dx = 0.1, dy = 0.1 }, -- adjust dx / dy for ball speed + paddle1 = { height = 3 }, -- Left paddle (Player 1) + paddle2 = { height = 3 }, -- Right paddle (Player 2) + score1 = 0, + score2 = 0, + running = false, + last_input = "", + applets = {} +} + +-- ANSI escape codes +local clear_screen = "\27[2J" +local clear_endofline = "\027[0K" +local cursor_home = "\27[H" +local reset_color = "\27[0m" + +-- ANSI color codes (from trisdemo) +local color_codes = { + [1] = "\27[1;34m", -- Blue: left paddle -- FIXME: don't know why next string.sub will mess up with colors + -- basically the player2 paddle draw will mess up screen if player1 has a color + [2] = "\27[1;33m", -- Yellow: right paddle + [3] = "\27[1;31m", -- Red: ball +} + +local screen = "" -- Global screen state + +local function draw_game() + if debug then print("draw_game called") end + + -- initialize a blank screen + local screen_lines = {} + for y = 1, game.height do + screen_lines[y] = "|" .. string.rep(" ", game.width) .. "|" + end + + -- draw the walls around the game + -- horizontal lines + screen_lines[1] = "+" .. string.rep("-", game.width) .. "+" + screen_lines[game.height + 1] = "+" .. string.rep("-", game.width) .. "+" + + -- left paddle + for i = 0, game.paddle1.height - 1 do + local y = game.paddle1.y + i + if y >= 1 and y <= game.height then + -- can't change its color as the sting.sub of paddle2 seem to mess up with ANSI char + screen_lines[y] = "| ||" .. string.sub(screen_lines[y], 5) + screen_lines[y] = screen_lines[y] + end + end + -- right paddle + for i = 0, game.paddle2.height - 1 do + local y = game.paddle2.y + i + if y >= 1 and y <= game.height then + screen_lines[y] = string.sub(screen_lines[y], 1, game.width - 2) .. color_codes[2] .. "||" .. reset_color .. " |" + end + end + + -- draw ball + local ball_x = math.floor(game.ball.x + 0.5) + local ball_y = math.floor(game.ball.y + 0.5) + if ball_y >= 2 and ball_y <= game.height and + ball_x >= 2 and ball_x <= game.width - 1 then + screen_lines[ball_y] = string.sub(screen_lines[ball_y], 1, ball_x - 1) .. color_codes[3] .. "O" .. reset_color .. + string.sub(screen_lines[ball_y], ball_x + 1) + end + + screen = "Score: " .. game.score1 .. " - " .. game.score2 .. clear_endofline .. "\r\n" + screen = screen .. table.concat(screen_lines, "\r\n") .. "\r\n" + screen = screen .. "Player 1: W (up), S (down) | Player 2: I (up), K (down) | Q to quit\r\n" + if debug then screen = screen .. "Last input: " .. game.last_input .. "\r\n" end +end + +local function update_game() + if debug then print("update_game called, running: " .. tostring(game.running)) end + if not game.running then return end + + -- move the ball + game.ball.x = game.ball.x + game.ball.dx + game.ball.y = game.ball.y + game.ball.dy + -- invert ball direction when it hits a wall + if game.ball.y <= 2 then + game.ball.y = 2 + game.ball.dy = -game.ball.dy + elseif game.ball.y >= game.height then + game.ball.y = game.height + game.ball.dy = -game.ball.dy + end + + -- check if point has been scored + if game.ball.x <= 3 then + -- player2 just scored a point + game.score2 = game.score2 + 1 + game.ball.x = game.width / 2 + game.ball.y = game.height / 2 + game.ball.dx = -game.ball.dx + elseif game.ball.x > game.width - 1 then + -- player1 just scored a point + game.score1 = game.score1 + 1 + game.ball.x = game.width / 2 + game.ball.y = game.height / 2 + game.ball.dx = -game.ball.dx + elseif game.ball.x <= 5 and game.ball.y >= game.paddle1.y and game.ball.y <= game.paddle1.y + game.paddle1.height - 1 then + -- left player returned the ball + game.ball.x = 5 + game.ball.dx = -game.ball.dx + elseif game.ball.x >= game.width - 2 and game.ball.y >= game.paddle2.y and game.ball.y <= game.paddle2.y + game.paddle2.height - 1 then + -- right player returned the ball + game.ball.x = game.width - 2 + game.ball.dx = -game.ball.dx + end +end + +local function handle_input(applet, input) + if debug then print("handle_input called, applet index: " .. (function() for i, a in ipairs(game.applets) do if a == applet then return i end end return "unknown" end)() .. ", input: " .. (input or "nil")) end + if not input or input == "" then return true end + game.last_input = input + input = input:lower() + + if applet.playerid == 1 and input == "w" and game.paddle1.y > 2 then + game.paddle1.y = game.paddle1.y - 1 + elseif applet.playerid == 1 and input == "s" and game.paddle1.y + game.paddle1.height - 1 < game.height then + game.paddle1.y = game.paddle1.y + 1 + elseif applet.playerid == 2 and input == "i" and game.paddle2.y > 2 then + game.paddle2.y = game.paddle2.y - 1 + elseif applet.playerid == 2 and input == "k" and game.paddle2.y + game.paddle2.height - 1 < game.height then + game.paddle2.y = game.paddle2.y + 1 + elseif input == "q" then + for i, a in ipairs(game.applets) do + if a == applet then + if debug then print("Player quitting, removing applet " .. i) end + table.remove(game.applets, i) + break + end + end + applet:send("Game ended. Final score: " .. game.score1 .. " - " .. game.score2 .. "\r\n") + return false + end + return true +end + +local function game_task() + if debug then print("game_task started") end + + -- game initialization + game.running = false + game.width = game.width + 4 -- adjustment for walls + game.height = game.height + 2 -- adjustment for walls + game.last_input = "" + + local last_draw_time = 0 + while true do + if debug then print("game_task loop, applets: " .. #game.applets .. ", running: " .. tostring(game.running)) end + + if #game.applets == 0 then + -- no players, let's do some reset tasks + game.running = false + game.score1 = 0 + game.score2 = 0 + game.ball = { x = game.width / 2, y = game.height / 2, dx = game.ball.dx, dy = game.ball.dy } + game.score1 = 0 + game.score2 = 0 + game.paddle1 = { y = game.height / 2, height = game.paddle1.height } + game.paddle2 = { y = game.height / 2, height = game.paddle2.height } + game.last_input = "" + screen = "No players connected.\r\n" + elseif #game.applets == 1 then + game.running = false + screen = "Waiting for Player 2...\r\n" + elseif #game.applets == 2 and not game.running then + if debug then print("Starting game with 2 players") end + game.running = true + last_draw_time = (core.now().sec * 1000) + math.floor(core.now().usec / 1000) + draw_game() + end + + if game.running then + update_game() + local current_time = (core.now().sec * 1000) + math.floor(core.now().usec / 1000) + if debug then print("game_task draw check, time diff: " .. (current_time - last_draw_time)) end + if current_time - last_draw_time >= 100 then + draw_game() + last_draw_time = current_time + end + end + + core.msleep(20) + end +end + +core.register_task(game_task) + +function pong_service(applet) + if debug then print("pong_service called, adding applet, current count: " .. #game.applets) end + table.insert(game.applets, applet) + applet.playerid = #game.applets + + if #game.applets == 1 then + applet:send("Welcome to Pong! Waiting for Player 2...\r\n") + else + applet:send("Welcome to Pong! Game starting soon...\r\n") + end + + local target_cycle_time = 100 -- 100ms refresh rate + applet:send(clear_screen) + while true do + local start_time = (core.now().sec * 1000) + math.floor(core.now().usec / 1000) + + local receive_start = (core.now().sec * 1000) + math.floor(core.now().usec / 1000) + local input = applet:receive(1, 90) + local receive_end = (core.now().sec * 1000) + math.floor(core.now().usec / 1000) + if debug then print("pong_service receive waited " .. (receive_end - receive_start) .. "ms for applet " .. applet.playerid) end + + if debug then print("pong_service sending screen to applet " .. applet.playerid) end + applet:send(cursor_home .. screen) + + if not handle_input(applet, input) then break end + + local end_time = (core.now().sec * 1000) + math.floor(core.now().usec / 1000) + local elapsed = end_time - start_time + local sleep_time = target_cycle_time - elapsed + if sleep_time > 0 then + if debug then print("pong_service sleep time: " .. sleep_time .. "ms for applet " .. applet.playerid) end + core.msleep(sleep_time) + end + end +end + +core.register_service("pong", "tcp", pong_service) -- 2.43.0
From 65cd901bc78bc796239b5da13970c93519f9bb36 Mon Sep 17 00:00:00 2001 From: Baptiste Assmann <bassm...@haproxy.com> Date: Wed, 23 Apr 2025 10:36:37 +0200 Subject: [PATCH 2/4] trisdemo: avoid screen refresh effect In current version of the game, there is a "screen refresh" effect: the screen is cleared before being re-drawn. I moved the clear right after the connection is opened and removed it from rendering time. --- examples/lua/trisdemo.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/lua/trisdemo.lua b/examples/lua/trisdemo.lua index 7156df98d..4b60bb5ac 100644 --- a/examples/lua/trisdemo.lua +++ b/examples/lua/trisdemo.lua @@ -112,7 +112,7 @@ local function rotate_piece(piece, piece_id, px, py, board) end function render(applet, board, piece, piece_id, px, py, score) - local output = clear_screen .. cursor_home + local output = 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 @@ -160,6 +160,7 @@ function handler(applet) end applet:send(cursor_hide) + applet:send(clear_screen) -- fall the piece by one line every delay local function fall_piece() -- 2.43.0
From 5805c025436475976cc70b69e1d6adc04c8a252e Mon Sep 17 00:00:00 2001 From: Baptiste Assmann <bassm...@haproxy.com> Date: Tue, 22 Apr 2025 12:14:51 +0200 Subject: [PATCH 1/4] games.cfg: add game name to the notice message --- examples/games.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/games.cfg b/examples/games.cfg index b0f991227..54f7036c3 100644 --- a/examples/games.cfg +++ b/examples/games.cfg @@ -8,7 +8,7 @@ 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' +.notice 'use "socat TCP-CONNECT:0:7001 STDIO,raw,echo=0" to start playing trisdemo' frontend trisdemo bind :7001 tcp-request content use-service lua.trisdemo -- 2.43.0
From ca97c5f4618826a50acf268eb9c3599b02d0f3b2 Mon Sep 17 00:00:00 2001 From: Baptiste Assmann <bassm...@haproxy.com> Date: Tue, 22 Apr 2025 12:14:00 +0200 Subject: [PATCH 3/4] NEW: examples/lua: space invaders game Kill the invaders while the fall down from the sky. --- examples/games.cfg | 6 ++ examples/lua/invaders.lua | 213 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 examples/lua/invaders.lua diff --git a/examples/games.cfg b/examples/games.cfg index 54f7036c3..0720c6cef 100644 --- a/examples/games.cfg +++ b/examples/games.cfg @@ -3,6 +3,7 @@ global tune.lua.bool-sample-conversion normal # load all games here lua-load lua/trisdemo.lua + lua-load lua/invaders.lua defaults timeout client 1h @@ -12,3 +13,8 @@ defaults frontend trisdemo bind :7001 tcp-request content use-service lua.trisdemo + +.notice 'use "socat TCP-CONNECT:0:7002 STDIO,raw,echo=0" to start playing invaders' +frontend invaders + bind :7002 + tcp-request content use-service lua.invaders diff --git a/examples/lua/invaders.lua b/examples/lua/invaders.lua new file mode 100644 index 000000000..e278e647a --- /dev/null +++ b/examples/lua/invaders.lua @@ -0,0 +1,213 @@ +-- Space Invaders-like game in Lua for HAProxy 3.2 TCP service + +local debug = false -- Set to true for debug output, false to disable + +-- ANSI escape codes +local clear_screen = "\27[2J" +local cursor_home = "\27[H" +local reset_color = "\27[0m" + +-- ANSI color codes (from trisdemo) +local color_codes = { + [1] = "\27[1;31m", -- Red: shoot + [2] = "\27[1;34m", -- Blue: space ship + [3] = "\27[1;35m", -- Purple: invaders +} + +local function init_game() + return { + width = 40, -- wider screen is possible, but must update the init_invaders() function + height = 20, + ship = { y = 20 }, -- Ship starts in middle column, bottom row + invaders = {}, -- List of invaders {x, y} + shots = {}, -- List of player shots {x, y} + score = 0, + running = false, + last_input = "", + last_move_time = 0 + } +end + +-- delay (in ms) between 2 invaders moves +-- once all invaders are dead, level increases and delay is reduced +local invaders_move = 5000 +local game_level = 0 + +-- Initialize invaders at game start +local function init_invaders(game) + game.invaders = {} + for row = 1, 3 do + for col = 5, 35, 5 do -- should be adjusted based on width + table.insert(game.invaders, { x = row, y = col }) + end + end +end + +local function draw_game(game) + if debug then print("draw_game called") end + + -- initialize a blank screen + local screen_lines = {} + for y = 1, game.height do + screen_lines[y] = "|" .. string.rep(" ", game.width - 1) .. "|" + end + + -- draw the frame around the game + -- horizontal lines + screen_lines[1] = "+" .. string.rep("-", game.width - 1) .. "+" + screen_lines[game.height + 1] = "+" .. string.rep("-", game.width - 1) .. "+" + + -- Draw ship at bottom row + screen_lines[game.height] = string.sub(screen_lines[game.height], 1, game.ship.y - 1) .. color_codes[2] .. "^" .. reset_color .. + string.sub(screen_lines[game.height], game.ship.y + 1) + + -- Draw invaders + for _, invader in ipairs(game.invaders) do + if invader.x >= 1 and invader.x <= game.height and invader.y >= 1 and invader.y <= game.width then + screen_lines[invader.x] = string.sub(screen_lines[invader.x], 1, invader.y - 1) .. "V" .. + string.sub(screen_lines[invader.x], invader.y + 1) + end + end + + -- Draw shots + for _, shot in ipairs(game.shots) do + if shot.x >= 1 and shot.x <= game.height and shot.y >= 1 and shot.y <= game.width then + screen_lines[shot.x] = string.sub(screen_lines[shot.x], 1, shot.y - 1) .. color_codes[1] .. "|" .. reset_color .. + string.sub(screen_lines[shot.x], shot.y + 1) + end + end + + local screen = "Level: " .. game_level .. ", Score: " .. game.score .. "\r\n" + screen = screen .. table.concat(screen_lines, "\r\n") .. "\r\n" + screen = screen .. "Controls: Left and Right arrows, Space (shoot), Q (quit)\r\n" + if debug then screen = screen .. "Last input: " .. game.last_input .. "\r\n" end + return screen +end + +local function update_game(game) + if debug then print("update_game called, running: " .. tostring(game.running)) end + if not game.running then return end + + -- Move shots up every cycle (200ms) + for i = #game.shots, 1, -1 do + game.shots[i].x = game.shots[i].x - 1 + if game.shots[i].x < 1 then + table.remove(game.shots, i) + end + end + + -- Check collisions every cycle + local hits = {} + for s, shot in ipairs(game.shots) do + for i, invader in ipairs(game.invaders) do + if shot.x == invader.x and shot.y == invader.y then + if debug then print("Hit detected: Shot " .. s .. " at x=" .. shot.x .. ", y=" .. shot.y .. " vs Invader " .. i .. " at x=" .. invader.x .. ", y=" .. invader.y) end + table.insert(hits, { shot = s, invader = i }) + end + end + end + + -- Remove hits in reverse order + table.sort(hits, function(a, b) return a.invader > b.invader end) + for _, hit in ipairs(hits) do + table.remove(game.shots, hit.shot) + table.remove(game.invaders, hit.invader) + game.score = game.score + 10 + end + + -- Check for win condition + if #game.invaders == 0 then + --game.running = false + game_level = game_level + 1 + init_invaders(game) + if game_level >= 5 then + return "You Win! Level: " .. game_level .. ", Final Score: " .. game.score .. "\r\n" + end + end + + -- Move invaders down + -- Change this value to "accelerate" the game: 500ms faster for each level + local current_time = (core.now().sec * 1000) + math.floor(core.now().usec / 1000) + if current_time - game.last_move_time >= (invaders_move - game_level * 500) then + for i = #game.invaders, 1, -1 do + game.invaders[i].x = game.invaders[i].x + 1 + if game.invaders[i].x >= game.height then + game.running = false + return "Game Over! Level: " .. game_level .. ", Final Score: " .. game.score .. "\r\n" + end + end + game.last_move_time = current_time + end +end + +local function handle_input(game, applet, input) + if debug then print("handle_input called, input: " .. (input or "nil")) end + if not input or input == "" then return true end + game.last_input = input + local delay = 10 + + if input == "q" then + applet:send("Game ended. Level: " .. game_level .. ", Final score: " .. game.score .. "\r\n") + return false + elseif input == " " then -- Spacebar to shoot + table.insert(game.shots, { x = game.height - 1, y = game.ship.y }) + elseif input == "\27" then + local a = applet:receive(1, delay) + if a == "[" then + local b = applet:receive(1, delay) + if b == "C" and game.ship.y < game.width then -- Right arrow + game.ship.y = game.ship.y + 1 + elseif b == "D" and game.ship.y > 2 then -- Left arrow + game.ship.y = game.ship.y - 1 + end + end + end + return true +end + +function invaders_service(applet) + if debug then print("invaders_service called") end + + local game = init_game() + applet:send("Welcome to Space Invaders! Game starting...\r\n") + core.msleep(1000) + + game.running = true + init_invaders(game) + local target_cycle_time = 200 + + applet:send(clear_screen) + while true do + local start_time = (core.now().sec * 1000) + math.floor(core.now().usec / 1000) + + local receive_start = (core.now().sec * 1000) + math.floor(core.now().usec / 1000) + local input = applet:receive(1, 190) + local receive_end = (core.now().sec * 1000) + math.floor(core.now().usec / 1000) + if debug then + print("invaders_service receive waited " .. (receive_end - receive_start) .. "ms") + print("Input received: " .. (input or "nil")) + end + + local screen_update = update_game(game) + if screen_update then + applet:send(cursor_home .. screen_update) + break + end + + if debug then print("invaders_service sending screen") end + local screen = draw_game(game) + applet:send(cursor_home .. screen) + + if not handle_input(game, applet, input) then break end + + local end_time = (core.now().sec * 1000) + math.floor(core.now().usec / 1000) + local elapsed = end_time - start_time + local sleep_time = target_cycle_time - elapsed + if sleep_time > 0 then + if debug then print("invaders_service sleep time: " .. sleep_time .. "ms") end + core.msleep(sleep_time) + end + end +end + +core.register_service("invaders", "tcp", invaders_service) -- 2.43.0