Change starting algorithm: replace countdown with number-of-players based algorithm

master
upsilon 2019-08-17 20:55:43 +02:00
parent a7797b67d8
commit 00c05a8a89
No known key found for this signature in database
GPG Key ID: A80DAE1F266E1C3C
11 changed files with 130 additions and 128 deletions

View File

@ -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.

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -1 +1,2 @@
hg_map
hg_player

View File

@ -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")

93
mods/hg_match/start.lua Normal file
View File

@ -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)

View File

@ -1,3 +1,2 @@
hg_match
hg_map
hg_hunger

View File

@ -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)

View File

@ -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

View File

@ -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)