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 --
-------------
local function return_as_chat_result(to, name)
local players = {}
for pname, pstat in pairs(ctf_stats.players) do
pstat.name = pname
pstat.color = nil
table.insert(players, pstat)
local function return_as_chat_result(to, target)
local players = ctf_stats.get_ordered_players()
local name, place, stat
if type(target) == "number" then
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
table.sort(players, function(one, two)
return one.score > two.score
end)
-- Build return string
local result = (to == name and "You are in " or name .. " is in ") ..
place .. " place.\n"
local place = -1
local me = nil
for i = 1, #players do
local pstat = players[i]
if pstat.name == name then
me = pstat
place = i
break
if stat then
local kd = stat.kills
if stat.deaths > 1 then
kd = kd / stat.deaths
end
end
if place < 1 then
place = #players + 1
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 ..
result = result ..
"Kills: " .. stat.kills ..
" | Deaths: " .. stat.deaths ..
" | K/D: " .. math.floor(kd * 10) / 10 ..
"\nBounty kills: " .. me.bounty_kills ..
" | Captures: " .. me.captures ..
" | Attempts: " .. me.attempts ..
"\nScore: " .. math.floor(me.score)
"\nBounty kills: " .. stat.bounty_kills ..
" | Captures: " .. stat.captures ..
" | Attempts: " .. stat.attempts ..
"\nScore: " .. math.floor(stat.score)
end
return true, result
return result
end
-------------------
@ -62,56 +68,57 @@ minetest.register_chatcommand("summary", {
})
minetest.register_chatcommand("r", {
params = "[<name>]",
description = "Display rankings of yourself or another player as a chat result.",
params = "[<name> | <rank>]",
description = "Display rankings of yourself, or another player or rank, as a chat result.",
func = function(name, param)
local target
if param ~= "" then
param = param:trim()
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")
local target, error = ctf_stats.get_target(name, param)
if not target then
return false, error
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
})
minetest.register_chatcommand("rankings", {
params = "[<name>]",
description = "Display rankings of yourself or another player.",
params = "[<name> | <rank>]",
description = "Display rankings of yourself, or another player or rank.",
func = function(name, param)
local target
if param ~= "" then
param = param:trim()
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")
local target, error = ctf_stats.get_target(name, param)
if not target then
return false, error
end
minetest.log("action", name .. " runs /rankings " .. param)
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
local players = {}
for pname, pstat in pairs(ctf_stats.players) do
pstat.name = pname
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)
minetest.show_formspec(name, "ctf_stats:rankings", ctf_stats.get_formspec(
"Player Rankings", ctf_stats.get_ordered_players(), 0, target))
return true
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
local tablecolumns = {
"tablecolumns[color;",
@ -20,8 +23,8 @@ local function render_team_stats(red, blue, stat, round)
blue_stat = math.floor(blue_stat * 10) / 10
end
return red_stat + blue_stat .. " (" ..
minetest.colorize(red.color, tostring(red_stat)) .. " - " ..
minetest.colorize(blue.color, tostring(blue_stat)) .. ")"
minetest.colorize(red.color, tostring(red_stat)) .. " - " ..
minetest.colorize(blue.color, tostring(blue_stat)) .. ")"
end
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
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)
return one.score > two.score
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 .. "#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 color
if hlt_name and pstat.name == hlt_name then
if hplace == i then
color = "#ffff00"
player_in_top_50 = true
else
color = pstat.color or "#ffffff"
end
@ -121,58 +135,46 @@ function ctf_stats.get_formspec(title, players, header, hlt_name)
kd = kd / pstat.deaths
end
ret = ret ..
"," .. string.gsub(color, "0x", "#") ..
"," .. i ..
"," .. pstat.name ..
"," .. pstat.kills ..
"," .. pstat.deaths ..
"," .. math.floor(kd * 10) / 10 ..
"," .. pstat.bounty_kills ..
"," .. pstat.captures ..
"," .. pstat.attempts ..
"," .. math.floor(pstat.score * 10) / 10
"," .. string.gsub(color, "0x", "#") ..
"," .. i ..
"," .. pstat.name ..
"," .. pstat.kills ..
"," .. pstat.deaths ..
"," .. math.floor(kd * 10) / 10 ..
"," .. pstat.bounty_kills ..
"," .. pstat.captures ..
"," .. pstat.attempts ..
"," .. math.floor(pstat.score * 10) / 10
end
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
-- of the list but yet be visible without having to scroll
if hlt_name and not player_in_top_50 then
local hlt_player, hlt_rank, hlt_kd
for i = 1, #players do
if players[i].name == hlt_name then
hlt_player = players[i]
hlt_rank = i
break
end
if hplace and hplace > ctf_stats.rankings_display_count then
local h_kd = hstat.kills
if hstat.deaths > 1 then
h_kd = h_kd / hstat.deaths
end
if hlt_player then
hlt_kd = hlt_player.kills
if hlt_player.deaths > 1 then
hlt_kd = hlt_kd / hlt_player.deaths
end
ret = ret .. tablecolumns
ret = ret .. "tableoptions[highlight=#00000000]"
ret = ret .. "table[0.5,6.1;13.25,0.4;hlt_score;"
ret = ret .. "#ffff00" ..
"," .. hlt_rank ..
"," .. hlt_player.name ..
"," .. hlt_player.kills ..
"," .. hlt_player.deaths ..
"," .. math.floor(hlt_kd * 10) / 10 ..
"," .. hlt_player.bounty_kills ..
"," .. hlt_player.captures ..
"," .. hlt_player.attempts ..
"," .. 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
ret = ret .. tablecolumns
ret = ret .. "tableoptions[highlight=#00000000]"
ret = ret .. "table[0.5,6.1;13.25,0.4;hlt_score;"
ret = ret .. "#ffff00" ..
"," .. hplace ..
"," .. hstat.name ..
"," .. hstat.kills ..
"," .. hstat.deaths ..
"," .. math.floor(h_kd * 10) / 10 ..
"," .. hstat.bounty_kills ..
"," .. hstat.captures ..
"," .. hstat.attempts ..
"," .. math.floor(hstat.score * 10) / 10 .. ";-1]"
--[[ 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
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
end
function ctf_stats.get_html(title, players)
table.sort(players, function(one, two)
return one.score > two.score
end)
function ctf_stats.get_html(title)
local players = ctf_stats.get_ordered_players()
local ret = "<h1>" .. title .. "</h1>"
ret = ret .. "<table>" ..
"<tr><th></th>" ..
@ -220,13 +219,6 @@ function ctf_stats.get_html(title, players)
end
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")
f:write("<!doctype html>\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("<link rel=\"stylesheet\" href=\"score_style.css\">\n")
f:write("</head><body>\n")
f:write(html)
f:write(ctf_stats.get_html("Player Rankings"))
f:write("</body></html>\n")
f:close()
end

View file

@ -137,6 +137,64 @@ function ctf_stats.player(name)
return player_stats, match_player_stats
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_stats.current[tname][name] = ctf_stats.current[tname][name] or {
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.",
"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 <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 /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.",