ctf_stats: Add support for retrieving stats by rank (#454)

This commit is contained in:
ANAND 2019-11-27 11:30:39 +05:30 committed by GitHub
parent 1f10fd9e22
commit 6b33c8e9cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 197 additions and 139 deletions

View file

@ -2,47 +2,53 @@
-- Helpers -- -- Helpers --
------------- -------------
local function return_as_chat_result(to, name) local function return_as_chat_result(to, target)
local players = {} local players = ctf_stats.get_ordered_players()
for pname, pstat in pairs(ctf_stats.players) do
pstat.name = pname
pstat.color = nil
table.insert(players, pstat)
end
table.sort(players, function(one, two) local name, place, stat
return one.score > two.score if type(target) == "number" then
end) place = target
stat = players[target]
local place = -1 name = stat.name
local me = nil elseif type(target) == "string" then
-- If target is a string, search through the player stats for a match
name = target
for i = 1, #players do for i = 1, #players do
local pstat = players[i] local pstat = players[i]
if pstat.name == name then if pstat.name == name then
me = pstat stat = pstat
place = i place = i
break break
end end
end end
-- If stat does not exist yet, set place to size of players + 1
if place < 1 then if place < 1 then
place = #players + 1 place = #players + 1
end end
local you_are_in = (to == name) and "You are in " or name .. " is in " else
local result = you_are_in .. place .. " place.\n" error("Invalid type passed to return_as_chat_result!", 2)
if me then
local kd = me.kills
if me.deaths > 1 then
kd = kd / me.deaths
end end
result = result .. "Kills: " .. me.kills ..
" | Deaths: " .. me.deaths .. -- Build return string
local result = (to == name and "You are in " or name .. " is in ") ..
place .. " place.\n"
if stat then
local kd = stat.kills
if stat.deaths > 1 then
kd = kd / stat.deaths
end
result = result ..
"Kills: " .. stat.kills ..
" | Deaths: " .. stat.deaths ..
" | K/D: " .. math.floor(kd * 10) / 10 .. " | K/D: " .. math.floor(kd * 10) / 10 ..
"\nBounty kills: " .. me.bounty_kills .. "\nBounty kills: " .. stat.bounty_kills ..
" | Captures: " .. me.captures .. " | Captures: " .. stat.captures ..
" | Attempts: " .. me.attempts .. " | Attempts: " .. stat.attempts ..
"\nScore: " .. math.floor(me.score) "\nScore: " .. math.floor(stat.score)
end end
return true, result return result
end end
------------------- -------------------
@ -62,56 +68,57 @@ minetest.register_chatcommand("summary", {
}) })
minetest.register_chatcommand("r", { minetest.register_chatcommand("r", {
params = "[<name>]", params = "[<name> | <rank>]",
description = "Display rankings of yourself or another player as a chat result.", description = "Display rankings of yourself, or another player or rank, as a chat result.",
func = function(name, param) func = function(name, param)
local target local target, error = ctf_stats.get_target(name, param)
if param ~= "" then if not target then
param = param:trim() return false, error
if ctf_stats.players[param] then
target = param
minetest.log("action", name .. " ran /r " .. param)
else
return false, "Can't find player '" .. param .. "'"
end end
else
target = name minetest.log("action", name .. " runs /r " .. param)
minetest.log("action", name .. " ran /r") return true, return_as_chat_result(name, target)
end end
return return_as_chat_result(name, target) })
minetest.register_chatcommand("rn", {
params = "<rank>",
description = "Display rankings of player at the specified rank.",
func = function(name, param)
if not param or param == "" then
return false, "Empty arguments not allowed! Specify a rank."
end
param = tonumber(param)
if not param then
return false, "Argument isn't a valid number!"
elseif param <= 0 or param > #ctf_stats.get_ordered_players() or
param ~= math.floor(param) then
return false, "Invalid number or number out of bounds!"
-- TODO: This is the worst way to do it. FIX IT.
end
minetest.log("action", name .. " runs /rn " .. param)
return true, return_as_chat_result(name, param)
end end
}) })
minetest.register_chatcommand("rankings", { minetest.register_chatcommand("rankings", {
params = "[<name>]", params = "[<name> | <rank>]",
description = "Display rankings of yourself or another player.", description = "Display rankings of yourself, or another player or rank.",
func = function(name, param) func = function(name, param)
local target local target, error = ctf_stats.get_target(name, param)
if param ~= "" then if not target then
param = param:trim() return false, error
if ctf_stats.players[param] then
target = param
minetest.log("action", name .. " ran /rankings " .. param)
else
return false, "Can't find player '" .. param .. "'"
end
else
target = name
minetest.log("action", name .. " ran /rankings")
end end
minetest.log("action", name .. " runs /rankings " .. param)
if not minetest.get_player_by_name(name) then if not minetest.get_player_by_name(name) then
return return_as_chat_result(name, target) return true, return_as_chat_result(name, target)
else else
local players = {} minetest.show_formspec(name, "ctf_stats:rankings", ctf_stats.get_formspec(
for pname, pstat in pairs(ctf_stats.players) do "Player Rankings", ctf_stats.get_ordered_players(), 0, target))
pstat.name = pname return true
pstat.color = nil
table.insert(players, pstat)
end
local fs = ctf_stats.get_formspec("Player Rankings", players, 0, target)
minetest.show_formspec(name, "ctf_stats:rankings", fs)
end end
end end
}) })

View file

@ -1,3 +1,6 @@
-- Number of entries to display in the player rankings table
ctf_stats.rankings_display_count = 50
-- Formspec element that governs table columns and their attributes -- Formspec element that governs table columns and their attributes
local tablecolumns = { local tablecolumns = {
"tablecolumns[color;", "tablecolumns[color;",
@ -90,7 +93,7 @@ function ctf_stats.get_formspec_match_summary(stats, winner_team, winner_player,
return ret return ret
end end
function ctf_stats.get_formspec(title, players, header, hlt_name) function ctf_stats.get_formspec(title, players, header, target)
table.sort(players, function(one, two) table.sort(players, function(one, two)
return one.score > two.score return one.score > two.score
end) end)
@ -105,14 +108,25 @@ function ctf_stats.get_formspec(title, players, header, hlt_name)
ret = ret .. "table[0.5,0;13.25,6.1;scores;" ret = ret .. "table[0.5,0;13.25,6.1;scores;"
ret = ret .. "#ffffff,,Player,Kills,Deaths,K/D,Bounty Kills,Captures,Attempts,Score" ret = ret .. "#ffffff,,Player,Kills,Deaths,K/D,Bounty Kills,Captures,Attempts,Score"
local player_in_top_50 = false local hstat, hplace
if type(target) == "number" then
hstat = players[target]
hplace = target
elseif type(target) == "string" then
for i, stat in pairs(players) do
if stat.name == target then
hplace = i
hstat = stat
break
end
end
end
for i = 1, math.min(#players, 50) do for i = 1, math.min(#players, ctf_stats.rankings_display_count) do
local pstat = players[i] local pstat = players[i]
local color local color
if hlt_name and pstat.name == hlt_name then if hplace == i then
color = "#ffff00" color = "#ffff00"
player_in_top_50 = true
else else
color = pstat.color or "#ffffff" color = pstat.color or "#ffffff"
end end
@ -134,45 +148,33 @@ function ctf_stats.get_formspec(title, players, header, hlt_name)
end end
ret = ret .. ";-1]" ret = ret .. ";-1]"
-- If hlt_name not in top 50, add a separate table -- If target not in top 50, add a separate table
-- This would result in the player's score displayed at the bottom -- This would result in the player's score displayed at the bottom
-- of the list but yet be visible without having to scroll -- of the list but yet be visible without having to scroll
if hlt_name and not player_in_top_50 then if hplace and hplace > ctf_stats.rankings_display_count then
local hlt_player, hlt_rank, hlt_kd local h_kd = hstat.kills
if hstat.deaths > 1 then
for i = 1, #players do h_kd = h_kd / hstat.deaths
if players[i].name == hlt_name then
hlt_player = players[i]
hlt_rank = i
break
end
end
if hlt_player then
hlt_kd = hlt_player.kills
if hlt_player.deaths > 1 then
hlt_kd = hlt_kd / hlt_player.deaths
end end
ret = ret .. tablecolumns ret = ret .. tablecolumns
ret = ret .. "tableoptions[highlight=#00000000]" ret = ret .. "tableoptions[highlight=#00000000]"
ret = ret .. "table[0.5,6.1;13.25,0.4;hlt_score;" ret = ret .. "table[0.5,6.1;13.25,0.4;hlt_score;"
ret = ret .. "#ffff00" .. ret = ret .. "#ffff00" ..
"," .. hlt_rank .. "," .. hplace ..
"," .. hlt_player.name .. "," .. hstat.name ..
"," .. hlt_player.kills .. "," .. hstat.kills ..
"," .. hlt_player.deaths .. "," .. hstat.deaths ..
"," .. math.floor(hlt_kd * 10) / 10 .. "," .. math.floor(h_kd * 10) / 10 ..
"," .. hlt_player.bounty_kills .. "," .. hstat.bounty_kills ..
"," .. hlt_player.captures .. "," .. hstat.captures ..
"," .. hlt_player.attempts .. "," .. hstat.attempts ..
"," .. math.floor(hlt_player.score * 10) / 10 .. ";-1]" "," .. math.floor(hstat.score * 10) / 10 .. ";-1]"
end --[[ else
-- else ret = ret .. "box[0.5,6.1;13.25,0.4;#101010]"
-- ret = ret .. "box[0.5,6.1;13.25,0.4;#101010]" Adds a box where the extra table should be, in order to make it
-- Adds a box where the extra table should be, in order to make it appear as an extension of the main table, but the color can't be
-- appear as an extension of the main table, but the color can't be matched, and looks slightly brighter or slightly darker than the table]]
-- matched, and looks slightly brighter or slightly darker than the table
end end
ret = ret .. "button_exit[10,6.5;3,1;close;Close]" ret = ret .. "button_exit[10,6.5;3,1;close;Close]"
@ -180,11 +182,8 @@ function ctf_stats.get_formspec(title, players, header, hlt_name)
return ret return ret
end end
function ctf_stats.get_html(title, players) function ctf_stats.get_html(title)
table.sort(players, function(one, two) local players = ctf_stats.get_ordered_players()
return one.score > two.score
end)
local ret = "<h1>" .. title .. "</h1>" local ret = "<h1>" .. title .. "</h1>"
ret = ret .. "<table>" .. ret = ret .. "<table>" ..
"<tr><th></th>" .. "<tr><th></th>" ..
@ -220,13 +219,6 @@ function ctf_stats.get_html(title, players)
end end
function ctf_stats.html_to_file(filepath) function ctf_stats.html_to_file(filepath)
local players = {}
for name, pstat in pairs(ctf_stats.players) do
pstat.name = name
pstat.color = nil
table.insert(players, pstat)
end
local html = ctf_stats.get_html("Player Rankings", players)
local f = io.open(filepath, "w") local f = io.open(filepath, "w")
f:write("<!doctype html>\n") f:write("<!doctype html>\n")
f:write("<html><head>\n") f:write("<html><head>\n")
@ -234,7 +226,7 @@ function ctf_stats.html_to_file(filepath)
f:write("<title>Player Rankings</title>\n") f:write("<title>Player Rankings</title>\n")
f:write("<link rel=\"stylesheet\" href=\"score_style.css\">\n") f:write("<link rel=\"stylesheet\" href=\"score_style.css\">\n")
f:write("</head><body>\n") f:write("</head><body>\n")
f:write(html) f:write(ctf_stats.get_html("Player Rankings"))
f:write("</body></html>\n") f:write("</body></html>\n")
f:close() f:close()
end end

View file

@ -137,6 +137,64 @@ function ctf_stats.player(name)
return player_stats, match_player_stats return player_stats, match_player_stats
end end
function ctf_stats.get_ordered_players()
local players = {}
-- Copy player stats into new empty table
for pname, pstat in pairs(ctf_stats.players) do
pstat.name = pname
pstat.color = nil
table.insert(players, pstat)
end
-- Sort table in the order of descending scores
table.sort(players, function(one, two)
return one.score > two.score
end)
return players
end
function ctf_stats.get_target(name, param)
param = param:trim()
-- If param is not empty, check if it's a number or a string
if param ~= "" then
-- Order of the following checks are as given below:
--
-- * `param` is returned as a string if player's stats exists
-- * If no matching stats exist, `param` is checked if it's a number
-- * If `param` isn't a number, it is assumed to be invalid, and nil is returned
-- * If `param` is a number, `param` is checked if out of bounds
-- * If `param` is not out of bounds, `param` is returned as a number, else nil
--
-- This order of checks is important because, in the case of `param` matching
-- both a number and a player name, it would be considered as a player name.
-- Check if param matches a player name
if ctf_stats.players[param] then
return param
else
-- Check if param is a number
local rank = tonumber(param)
if rank then
-- Check if param is within range
-- TODO: Fix this hack by maintaining two tables - an ordered list, and a hashmap
if rank <= 0 or rank > #ctf_stats.get_ordered_players() or
rank ~= math.floor(rank) then
return nil, "Invalid number or number out of bounds!"
else
return rank
end
else
return nil, "Invalid player name specified!"
end
end
else
return name
end
end
ctf.register_on_join_team(function(name, tname) ctf.register_on_join_team(function(name, tname)
ctf_stats.current[tname][name] = ctf_stats.current[tname][name] or { ctf_stats.current[tname][name] = ctf_stats.current[tname][name] or {
kills = 0, kills = 0,

View file

@ -69,6 +69,7 @@ function random_messages.read_messages()
"Help your team claim victory by storing extra weapons in the team chest, and never taking more than you need.", "Help your team claim victory by storing extra weapons in the team chest, and never taking more than you need.",
"Excessive spawn-killing is a direct violation of the rules - appropriate punishments will be given.", "Excessive spawn-killing is a direct violation of the rules - appropriate punishments will be given.",
"Use /r to check your score and rank, and /rankings to see the league tables.", "Use /r to check your score and rank, and /rankings to see the league tables.",
"Use /r <number> or /rn <number> to check the rankings of the player in the given rank.",
"Use bandages on team-mates to heal them by 3-4 HP if their health is below 15 HP.", "Use bandages on team-mates to heal them by 3-4 HP if their health is below 15 HP.",
"Use /m to add a team marker at pointed location, that's visible only to team-mates.", "Use /m to add a team marker at pointed location, that's visible only to team-mates.",
"Use /summary to check scores of the current match and the previous match.", "Use /summary to check scores of the current match and the previous match.",