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 local name, place, stat
pstat.color = nil if type(target) == "number" then
table.insert(players, pstat) place = target
stat = players[target]
name = stat.name
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
local pstat = players[i]
if pstat.name == name then
stat = pstat
place = i
break
end
end
-- If stat does not exist yet, set place to size of players + 1
if place < 1 then
place = #players + 1
end
else
error("Invalid type passed to return_as_chat_result!", 2)
end end
table.sort(players, function(one, two) -- Build return string
return one.score > two.score local result = (to == name and "You are in " or name .. " is in ") ..
end) place .. " place.\n"
local place = -1 if stat then
local me = nil local kd = stat.kills
for i = 1, #players do if stat.deaths > 1 then
local pstat = players[i] kd = kd / stat.deaths
if pstat.name == name then
me = pstat
place = i
break
end end
end result = result ..
if place < 1 then "Kills: " .. stat.kills ..
place = #players + 1 " | Deaths: " .. stat.deaths ..
end
local you_are_in = (to == name) and "You are in " or name .. " is in "
local result = you_are_in .. place .. " place.\n"
if me then
local kd = me.kills
if me.deaths > 1 then
kd = kd / me.deaths
end
result = result .. "Kills: " .. me.kills ..
" | Deaths: " .. me.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
else
target = name
minetest.log("action", name .. " ran /r")
end end
return return_as_chat_result(name, target)
minetest.log("action", name .. " runs /r " .. param)
return true, return_as_chat_result(name, target)
end
})
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;",
@ -20,8 +23,8 @@ local function render_team_stats(red, blue, stat, round)
blue_stat = math.floor(blue_stat * 10) / 10 blue_stat = math.floor(blue_stat * 10) / 10
end end
return red_stat + blue_stat .. " (" .. return red_stat + blue_stat .. " (" ..
minetest.colorize(red.color, tostring(red_stat)) .. " - " .. minetest.colorize(red.color, tostring(red_stat)) .. " - " ..
minetest.colorize(blue.color, tostring(blue_stat)) .. ")" minetest.colorize(blue.color, tostring(blue_stat)) .. ")"
end end
function ctf_stats.get_formspec_match_summary(stats, winner_team, winner_player, time) function ctf_stats.get_formspec_match_summary(stats, winner_team, winner_player, time)
@ -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
@ -121,58 +135,46 @@ function ctf_stats.get_formspec(title, players, header, hlt_name)
kd = kd / pstat.deaths kd = kd / pstat.deaths
end end
ret = ret .. ret = ret ..
"," .. string.gsub(color, "0x", "#") .. "," .. string.gsub(color, "0x", "#") ..
"," .. i .. "," .. i ..
"," .. pstat.name .. "," .. pstat.name ..
"," .. pstat.kills .. "," .. pstat.kills ..
"," .. pstat.deaths .. "," .. pstat.deaths ..
"," .. math.floor(kd * 10) / 10 .. "," .. math.floor(kd * 10) / 10 ..
"," .. pstat.bounty_kills .. "," .. pstat.bounty_kills ..
"," .. pstat.captures .. "," .. pstat.captures ..
"," .. pstat.attempts .. "," .. pstat.attempts ..
"," .. math.floor(pstat.score * 10) / 10 "," .. math.floor(pstat.score * 10) / 10
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 end
if hlt_player then ret = ret .. tablecolumns
hlt_kd = hlt_player.kills ret = ret .. "tableoptions[highlight=#00000000]"
if hlt_player.deaths > 1 then ret = ret .. "table[0.5,6.1;13.25,0.4;hlt_score;"
hlt_kd = hlt_kd / hlt_player.deaths ret = ret .. "#ffff00" ..
end "," .. hplace ..
"," .. hstat.name ..
ret = ret .. tablecolumns "," .. hstat.kills ..
ret = ret .. "tableoptions[highlight=#00000000]" "," .. hstat.deaths ..
ret = ret .. "table[0.5,6.1;13.25,0.4;hlt_score;" "," .. math.floor(h_kd * 10) / 10 ..
ret = ret .. "#ffff00" .. "," .. hstat.bounty_kills ..
"," .. hlt_rank .. "," .. hstat.captures ..
"," .. hlt_player.name .. "," .. hstat.attempts ..
"," .. hlt_player.kills .. "," .. math.floor(hstat.score * 10) / 10 .. ";-1]"
"," .. hlt_player.deaths .. --[[ else
"," .. math.floor(hlt_kd * 10) / 10 .. ret = ret .. "box[0.5,6.1;13.25,0.4;#101010]"
"," .. hlt_player.bounty_kills .. Adds a box where the extra table should be, in order to make it
"," .. hlt_player.captures .. appear as an extension of the main table, but the color can't be
"," .. hlt_player.attempts .. matched, and looks slightly brighter or slightly darker than the table]]
"," .. math.floor(hlt_player.score * 10) / 10 .. ";-1]"
end
-- else
-- 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
-- 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
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.",