car_race.lua
local terminal = require "terminal"
local write = terminal.output.write
local read_key = terminal.input.readansi
local set_pos = terminal.cursor.position.set_seq
local fg = terminal.text.color.fore_seq
local bg = terminal.text.color.back_seq
local RESET_SEQ = "\27[0m"
local config = {
lane_count = 3,
lane_spacing = 4,
lane_offset = 2,
road_height = 12,
player_row = 12,
spawn_rate = 0.25,
speed_initial = 0.12,
speed_min = 0.04,
speed_decay = 0.005,
speed_interval = 40,
divider_period = 4,
divider_on = 2,
}
local colors = {
road = bg(235),
border = fg(250) .. bg(235),
divider = fg(220) .. bg(235),
obstacle = fg(196) .. bg(235),
player = fg(46) .. bg(235),
}
local ROAD_WIDTH = config.lane_count * config.lane_spacing + 1
local GAME_ROW = 2
local GAME_COL = 2
local function new_game_state()
return {
player_lane = 2,
obstacles = {},
speed = config.speed_initial,
distance = 0,
frame = 0,
running = true,
}
end
local function lane_to_column(lane)
return GAME_COL + (lane - 1) * config.lane_spacing + config.lane_offset
end
local function clamp(value, lo, hi)
return math.max(lo, math.min(hi, value))
end
local KEY_DIRECTION = {
a = -1,
d = 1,
}
local function handle_input(state)
local key = read_key(state.speed)
local dir = KEY_DIRECTION[key]
if not dir then
return
end
state.player_lane = clamp(state.player_lane + dir, 1, config.lane_count)
end
local function update_obstacles(state)
local obstacles = state.obstacles
for i = #obstacles, 1, -1 do
local o = obstacles[i]
o.row = o.row + 1
if o.row > config.road_height then
obstacles[i] = obstacles[#obstacles]
obstacles[#obstacles] = nil
end
end
end
local function spawn_obstacle(state)
if math.random() >= config.spawn_rate then
return
end
local lane = math.random(config.lane_count)
state.obstacles[#state.obstacles + 1] = {
lane = lane,
row = 1,
}
end
local function update_speed(state)
if state.distance % config.speed_interval ~= 0 then
return
end
state.speed = math.max(config.speed_min, state.speed - config.speed_decay)
end
local function update_game(state)
update_obstacles(state)
spawn_obstacle(state)
state.distance = state.distance + 1
state.frame = state.frame + 1
update_speed(state)
end
local function check_collision(state)
for _, o in ipairs(state.obstacles) do
if o.row == config.player_row and o.lane == state.player_lane then
state.running = false
return
end
end
end
local function draw_border(buf)
buf[#buf + 1] = set_pos(GAME_ROW, GAME_COL)
.. colors.border
.. "┌"
.. string.rep("─", ROAD_WIDTH - 2)
.. "┐"
.. RESET_SEQ
end
local function draw_road_row(buf, state, row)
local screen_row = GAME_ROW + row
local pos = set_pos(screen_row, GAME_COL)
buf[#buf + 1] = pos .. colors.road .. string.rep(" ", ROAD_WIDTH) .. RESET_SEQ
buf[#buf + 1] = pos .. colors.border .. "│" .. RESET_SEQ
buf[#buf + 1] = set_pos(screen_row, GAME_COL + ROAD_WIDTH - 1) .. colors.border .. "│" .. RESET_SEQ
if (state.frame + row) % config.divider_period < config.divider_on then
for lane = 1, config.lane_count - 1 do
buf[#buf + 1] = set_pos(screen_row, GAME_COL + lane * config.lane_spacing) .. colors.divider .. "┊" .. RESET_SEQ
end
end
end
local function draw_obstacles(buf, state, row)
for _, o in ipairs(state.obstacles) do
if o.row == row then
buf[#buf + 1] = set_pos(GAME_ROW + row, lane_to_column(o.lane)) .. colors.obstacle .. "🪨" .. RESET_SEQ
end
end
end
local function draw_player(buf, state, row)
if row ~= config.player_row then
return
end
buf[#buf + 1] = set_pos(GAME_ROW + row, lane_to_column(state.player_lane)) .. colors.player .. "█" .. RESET_SEQ
end
local function draw_road(buf, state)
draw_border(buf)
for row = 1, config.road_height do
draw_road_row(buf, state, row)
draw_obstacles(buf, state, row)
draw_player(buf, state, row)
end
buf[#buf + 1] = set_pos(GAME_ROW + config.road_height + 1, GAME_COL)
.. colors.border
.. "└"
.. string.rep("─", ROAD_WIDTH - 2)
.. "┘"
.. RESET_SEQ
end
local function render(state)
local buffer = {}
draw_road(buffer, state)
buffer[#buffer + 1] = set_pos(GAME_ROW + config.road_height + 3, GAME_COL)
write(table.concat(buffer))
end
local function clear_screen()
terminal.clear.screen()
terminal.cursor.position.set(1, 1)
end
local function start_screen()
clear_screen()
write("\n CAR RACE\n\n")
write(" Press [y] to start or [n] to quit\n")
return read_key(math.huge) == "y"
end
local function game_over_screen(state)
clear_screen()
write("\n GAME OVER\n\n")
write(" Distance: " .. tostring(state.distance) .. "\n\n")
write(" Press any key...\n")
read_key(math.huge)
end
terminal.initwrap(function()
terminal.cursor.visible.set(false)
while true do
if not start_screen() then
break
end
clear_screen()
local state = new_game_state()
while state.running do
handle_input(state)
update_game(state)
check_collision(state)
render(state)
end
game_over_screen(state)
end
end, { displaybackup = true })()