Change starting algorithm: replace countdown with number-of-players based algorithm
parent
a7797b67d8
commit
00c05a8a89
|
@ -31,9 +31,3 @@ is the maximum between 3 and the number of players out of the number of maps
|
|||
are 10 players waiting). The maximum waiting time is 5 minutes, if there are not
|
||||
enough players (still more than 3) but 5 minutes were waited, a new game
|
||||
will start anyway. -->
|
||||
|
||||
The game computes a metric named the last-20 minutes average time to get 30
|
||||
players. The countdown is initially set to this value (with a maximum of 5
|
||||
minutes, which will be typically used unless there are more than 6 players a
|
||||
minute). Every time a new players connects, the countdown is decreased by
|
||||
5% of its initial value, unless the countdown is below 20% of its initial value.
|
||||
|
|
|
@ -130,7 +130,7 @@ end
|
|||
function hg_map.get_map(spawnpoints_min)
|
||||
local selectable_maps = {}
|
||||
for _, map in ipairs(hg_map.maps) do
|
||||
if not map.in_use and map.ready and map.max_players >= spawnpoints_min then
|
||||
if map.ready and map.max_players >= spawnpoints_min then
|
||||
table.insert(selectable_maps, map)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -156,10 +156,6 @@ local function coroutine_body()
|
|||
minetest.log("action", string.format("[hg_map] Finished loading of map id %d.", map.id))
|
||||
|
||||
map.ready = true
|
||||
if hg_match.waiting_map_flag then
|
||||
hg_match.waiting_map_flag = false
|
||||
hg_match.new_game()
|
||||
end
|
||||
else
|
||||
-- There is no need for a map.
|
||||
coroutine.yield()
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
hg_match.initial_countdown = 0
|
||||
hg_match.wait_countdown = 0
|
||||
hg_match.new_players_20_min = 0
|
||||
|
||||
function hg_match.init_countdown()
|
||||
-- Average time to get 20 players based on the last 20 minutes activity.
|
||||
-- Maximum 5 minutes, minimum 30 seconds
|
||||
local minutes_per_player = (20 * 60) / hg_match.new_players_20_min
|
||||
local v = math.max(math.min(20 * minutes_per_player, 300), 30)
|
||||
hg_match.initial_countdown = v
|
||||
hg_match.wait_countdown = v
|
||||
|
||||
hg_match.call_registered_on_countdown_update(v, v)
|
||||
end
|
||||
|
||||
function hg_match.update_countdown()
|
||||
if #minetest.get_connected_players() == 0 then
|
||||
-- Do not run the countdown when no player is connected,
|
||||
-- and leave it in a ready state
|
||||
hg_match.waiting_player_flag = false
|
||||
hg_match.waiting_map_flag = false
|
||||
hg_match.init_countdown()
|
||||
end
|
||||
|
||||
if hg_match.waiting_player_flag or hg_match.waiting_map_flag then
|
||||
return
|
||||
end
|
||||
|
||||
hg_match.wait_countdown = hg_match.wait_countdown - 1
|
||||
|
||||
hg_match.call_registered_on_countdown_update(hg_match.initial_countdown, hg_match.wait_countdown)
|
||||
|
||||
if hg_match.wait_countdown == 0 then
|
||||
hg_match.init_countdown()
|
||||
hg_match.new_game()
|
||||
end
|
||||
end
|
||||
|
||||
local timer = 0
|
||||
minetest.register_globalstep(function(dtime)
|
||||
timer = timer + dtime
|
||||
if timer < 1 then
|
||||
return
|
||||
end
|
||||
|
||||
timer = timer - 1
|
||||
hg_match.update_countdown()
|
||||
end)
|
||||
|
||||
hg_match.register_on_new_player(function(name)
|
||||
hg_match.new_players_20_min = hg_match.new_players_20_min + 1
|
||||
minetest.after(20 * 60, function()
|
||||
hg_match.new_players_20_min = hg_match.new_players_20_min - 1
|
||||
end)
|
||||
end)
|
||||
|
||||
hg_match.init_countdown()
|
|
@ -1 +1,2 @@
|
|||
hg_map
|
||||
hg_player
|
||||
|
|
|
@ -5,8 +5,6 @@ end
|
|||
hg_match = {
|
||||
players_waiting = {},
|
||||
max_players = 0,
|
||||
waiting_player_flag = false,
|
||||
waiting_map_flag = false,
|
||||
}
|
||||
|
||||
function debug_msg(msg)
|
||||
|
@ -15,7 +13,7 @@ function debug_msg(msg)
|
|||
end
|
||||
end
|
||||
|
||||
for _, v in ipairs({"new_game", "end_game", "countdown_update", "new_player", "killed_player"}) do
|
||||
for _, v in ipairs({"new_game", "end_game", "new_player", "killed_player"}) do
|
||||
hg_match["registered_on_" .. v] = {}
|
||||
hg_match["register_on_" .. v] = function(f)
|
||||
assert(type(f) == "function")
|
||||
|
@ -53,10 +51,6 @@ function hg_match.new_game()
|
|||
-- Check if there are enough players
|
||||
if #hg_match.players_waiting < 2 then
|
||||
debug_msg("Not enough players")
|
||||
|
||||
hg_match.waiting_player_flag = true
|
||||
hg_match.call_registered_on_countdown_update(hg_match.initial_countdown, 0,
|
||||
"The game will start as soon as at least 2 players are ready.")
|
||||
return false
|
||||
end
|
||||
|
||||
|
@ -64,26 +58,25 @@ function hg_match.new_game()
|
|||
local map = hg_map.get_map(#hg_match.players_waiting)
|
||||
if not map then
|
||||
debug_msg("No map is ready")
|
||||
hg_match.waiting_map_flag = true
|
||||
hg_match.call_registered_on_countdown_update(hg_match.initial_countdown, 0,
|
||||
"The game will start as soon as a map is ready.")
|
||||
return false
|
||||
end
|
||||
|
||||
local players = table.copy(hg_match.players_waiting)
|
||||
hg_match.players_waiting = {}
|
||||
|
||||
map.in_use = true
|
||||
map.players = players
|
||||
map.ready = false
|
||||
map.dead_players = {}
|
||||
map.build_countdown = 5*60
|
||||
map.scores = {}
|
||||
for _, name in ipairs(players) do
|
||||
table.remove(hg_match.players_waiting, 1)
|
||||
table.insert(map.players, name)
|
||||
map.scores[name] = {}
|
||||
end
|
||||
|
||||
minetest.log("action", string.format("[hg_match] Starting a new game with %d players on map %d (%s)!", #players, map.id, map.name))
|
||||
hg_match.call_registered_on_new_game(map, players)
|
||||
minetest.log("action", string.format("[hg_match] Starting a new game with %d players on map %d (%s)!", #map.players, map.id, map.name))
|
||||
hg_player.new_game(map)
|
||||
hg_match.call_registered_on_new_game(map, map.players)
|
||||
|
||||
return true
|
||||
end
|
||||
|
@ -94,7 +87,6 @@ function hg_match.end_game(map)
|
|||
minetest.log("action", string.format("[hg_match] End of game on map %d.", map.id))
|
||||
|
||||
map.in_use = false
|
||||
map.ready = false
|
||||
for i, name in ipairs(map.players) do
|
||||
table.remove(map.players, i)
|
||||
table.insert(map.dead_players, name)
|
||||
|
@ -102,12 +94,16 @@ function hg_match.end_game(map)
|
|||
hg_match.new_player(name)
|
||||
end
|
||||
|
||||
hg_match.last_game_time = os.time()
|
||||
|
||||
hg_player.end_game(map)
|
||||
hg_match.call_registered_on_end_game(map)
|
||||
end
|
||||
|
||||
function hg_match.new_player(name)
|
||||
debug_msg("Called new_player with params " .. name)
|
||||
|
||||
hg_player.new_player(name)
|
||||
hg_match.call_registered_on_new_player(name)
|
||||
|
||||
if hg_match.is_waiting(name) then
|
||||
|
@ -115,19 +111,6 @@ function hg_match.new_player(name)
|
|||
end
|
||||
|
||||
table.insert(hg_match.players_waiting, name)
|
||||
|
||||
if hg_match.waiting_player_flag then
|
||||
debug_msg("Starting a game because the waiting_player_flag is set")
|
||||
hg_match.waiting_player_flag = false
|
||||
minetest.after(2, hg_match.new_game)
|
||||
return
|
||||
end
|
||||
|
||||
if #hg_match.players_waiting >= hg_match.max_players then
|
||||
debug_msg("Starting a game because no map is able to handle more players")
|
||||
hg_match.new_game()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
function hg_match.remove_player(name)
|
||||
|
@ -173,4 +156,4 @@ function hg_match.killed_player(name, killer_name)
|
|||
end
|
||||
end
|
||||
|
||||
dofile(minetest.get_modpath("hg_match") .. "/countdown.lua")
|
||||
dofile(minetest.get_modpath("hg_match") .. "/start.lua")
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
--[[
|
||||
The code is this file is responsible for deciding when to start a game. It checks
|
||||
every second whether to do so.
|
||||
]]
|
||||
|
||||
function hg_match.starter_step()
|
||||
if hg_match.start_delay then
|
||||
-- A start is scheduled after some delay
|
||||
hg_match.start_delay = hg_match.start_delay - 1
|
||||
if hg_match.start_delay == 0 then
|
||||
hg_match.start_delay = nil
|
||||
else
|
||||
hg_player.update_text_hud_all(hg_match.players_waiting, string.format("The game will start in %d seconds...", hg_match.start_delay))
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- If there are less than two players, it's certain that we can't start a game.
|
||||
local n_players = #minetest.get_connected_players()
|
||||
if n_players < 2 then
|
||||
hg_player.update_text_hud_all(hg_match.players_waiting, "The game will start as soon as another player is ready...")
|
||||
return
|
||||
end
|
||||
|
||||
-- Read the maximum number of players to wait to start a game.
|
||||
local max_players_waited = tonumber(minetest.settings:get("hg.max_players_waited")) or hg_match.max_players
|
||||
if max_players_waited > hg_match.max_players then
|
||||
minetest.log("warning", string.format("hg.max_players_waited set to %d, but no map can handle more than %d players.",
|
||||
max_players_waited, hg_match.max_players))
|
||||
max_players_waited = hg_match.max_players
|
||||
elseif max_players_waited < 2 then
|
||||
minetest.log("warning", string.format("hg.max_players_waited must be at least 2, set to %d.",
|
||||
max_players_waited))
|
||||
max_players_waited = 2
|
||||
end
|
||||
-- Compute the number of players to wait based on the number of connected players
|
||||
local players_waited
|
||||
if n_players <= 2 then
|
||||
-- We need at least two players
|
||||
players_waited = 2
|
||||
elseif n_players <= 5 then
|
||||
-- Up to 5 players, wait all the players
|
||||
players_waited = n_players
|
||||
else
|
||||
-- Wait for the ceil of the square root of the number of players
|
||||
players_waited = math.ceil(math.sqrt(n_players))
|
||||
end
|
||||
-- But never more than max_players_waited.
|
||||
if players_waited > max_players_waited then
|
||||
players_waited = max_players_waited
|
||||
end
|
||||
|
||||
if #hg_match.players_waiting < players_waited then
|
||||
-- Not enough players
|
||||
hg_player.update_text_hud_all(hg_match.players_waiting, string.format("The game will start as soon as %d players are ready (it shouldn't take long)...",
|
||||
players_waited))
|
||||
return
|
||||
end
|
||||
|
||||
-- Also check that there is a map ready
|
||||
local map_ready = false
|
||||
for _, map in ipairs(hg_map.maps) do
|
||||
if map.ready then
|
||||
map_ready = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not map_ready then
|
||||
hg_player.update_text_hud_all(hg_match.players_waiting, "The game will start as soon as a map is ready...")
|
||||
return
|
||||
end
|
||||
|
||||
-- We can now start a game.
|
||||
if hg_match.last_game_time and os.difftime(os.time(), hg_match.last_game_time) < 15 then
|
||||
-- If a game ended less than 15 seconds ago, wait 15 seconds before starting
|
||||
-- the next game to let players respawn, say each other Good Game, and rest
|
||||
-- for a bit.
|
||||
hg_match.start_delay = 15 - os.difftime(os.time(), hg_match.last_game_time)
|
||||
else
|
||||
hg_match.new_game()
|
||||
end
|
||||
end
|
||||
|
||||
local timer = 0
|
||||
minetest.register_globalstep(function(dtime)
|
||||
timer = timer + dtime
|
||||
if timer < 1 then
|
||||
return
|
||||
end
|
||||
|
||||
timer = timer - 1
|
||||
hg_match.starter_step()
|
||||
end)
|
|
@ -1,3 +1,2 @@
|
|||
hg_match
|
||||
hg_map
|
||||
hg_hunger
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
-- Waiting HUD
|
||||
|
||||
hg_player = {
|
||||
text_huds = {},
|
||||
}
|
||||
hg_player.text_huds = {}
|
||||
|
||||
function hg_player.update_text_hud(player, text)
|
||||
local name = player:get_player_name()
|
||||
|
@ -31,6 +28,12 @@ function hg_player.update_text_hud(player, text)
|
|||
end
|
||||
end
|
||||
|
||||
function hg_player.update_text_hud_all(names, text)
|
||||
for _, name in ipairs(names) do
|
||||
hg_player.update_text_hud(minetest.get_player_by_name(name), text)
|
||||
end
|
||||
end
|
||||
|
||||
function hg_player.remove_text_hud(player)
|
||||
local name = player:get_player_name()
|
||||
local id = hg_player.text_huds[name]
|
||||
|
@ -42,20 +45,4 @@ end
|
|||
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
hg_player.remove_text_hud(player)
|
||||
hg_match.remove_player(player:get_player_name())
|
||||
end)
|
||||
|
||||
hg_match.register_on_countdown_update(function(initial, current, error)
|
||||
local str = ""
|
||||
if error then
|
||||
str = error
|
||||
else
|
||||
str = string.format("Next game starts in: %d min %d s",
|
||||
math.floor(current / 60), current % 60)
|
||||
end
|
||||
|
||||
for _, name in ipairs(hg_match.players_waiting) do
|
||||
local player = minetest.get_player_by_name(name)
|
||||
hg_player.update_text_hud(player, str)
|
||||
end
|
||||
end)
|
||||
|
|
|
@ -2,6 +2,8 @@ if minetest.is_singleplayer() then
|
|||
return
|
||||
end
|
||||
|
||||
hg_player = {}
|
||||
|
||||
dofile(minetest.get_modpath("hg_player") .. "/hud.lua")
|
||||
dofile(minetest.get_modpath("hg_player") .. "/pvp.lua")
|
||||
dofile(minetest.get_modpath("hg_player") .. "/ranking_formspec.lua")
|
||||
|
@ -26,7 +28,7 @@ local function clear_inventory(player)
|
|||
end
|
||||
end
|
||||
|
||||
hg_match.register_on_new_player(function(name)
|
||||
function hg_player.new_player(name)
|
||||
-- Revoke interact
|
||||
local privs = minetest.get_player_privs(name)
|
||||
privs.interact = nil
|
||||
|
@ -49,7 +51,7 @@ hg_match.register_on_new_player(function(name)
|
|||
player:set_clouds({
|
||||
height = hg_map.spawn.maxp.y
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
minetest.register_on_joinplayer(function(player)
|
||||
local name = player:get_player_name()
|
||||
|
@ -59,6 +61,10 @@ minetest.register_on_joinplayer(function(player)
|
|||
hg_match.new_player(name)
|
||||
end)
|
||||
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
hg_match.remove_player(player:get_player_name())
|
||||
end)
|
||||
|
||||
minetest.register_on_dieplayer(function(player)
|
||||
local name = player:get_player_name()
|
||||
|
||||
|
@ -74,6 +80,7 @@ end)
|
|||
|
||||
minetest.register_on_respawnplayer(function(player)
|
||||
local name = player:get_player_name()
|
||||
|
||||
local formspec = hg_player.respawn_ranking_formspec[name]
|
||||
if formspec then
|
||||
minetest.after(1, function()
|
||||
|
@ -87,9 +94,9 @@ minetest.register_on_respawnplayer(function(player)
|
|||
return true
|
||||
end)
|
||||
|
||||
hg_match.register_on_new_game(function(map, players)
|
||||
function hg_player.new_game(map)
|
||||
local spawnpoints = table.copy(map.hg_nodes.spawnpoint)
|
||||
for _, name in ipairs(players) do
|
||||
for _, name in ipairs(map.players) do
|
||||
local player = minetest.get_player_by_name(name)
|
||||
|
||||
-- Grant interact
|
||||
|
@ -123,12 +130,13 @@ hg_match.register_on_new_game(function(map, players)
|
|||
to_player = name,
|
||||
})
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
hg_match.register_on_end_game(function(map)
|
||||
function hg_player.end_game(map)
|
||||
for _, name in ipairs(map.dead_players) do
|
||||
minetest.sound_play({name = "hg_player_match_start"}, {
|
||||
to_player = name,
|
||||
})
|
||||
end
|
||||
end)
|
||||
hg_player.show_ranking_formspec(map)
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
hg_player.respawn_ranking_formspec = {}
|
||||
|
||||
local function show_ranking_formspec(map)
|
||||
function hg_player.show_ranking_formspec(map)
|
||||
local kill_map = {}
|
||||
local kill_number = {}
|
||||
for killer, v in pairs(map.scores) do
|
||||
|
@ -30,5 +30,3 @@ local function show_ranking_formspec(map)
|
|||
|
||||
hg_player.respawn_ranking_formspec[map.dead_players[#map.dead_players-1]] = formspec
|
||||
end
|
||||
|
||||
hg_match.register_on_end_game(show_ranking_formspec)
|
||||
|
|
Loading…
Reference in New Issue