Rewrite scoring algorithm
This commit is contained in:
parent
496ef9c30c
commit
2234a59c61
4 changed files with 139 additions and 66 deletions
14
mods/ctf_stats/README.md
Normal file
14
mods/ctf_stats/README.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
## Score
|
||||||
|
|
||||||
|
* +5 for picking up a flag.
|
||||||
|
* +20 for then capturing the flag.
|
||||||
|
* +X for killing someone, where X is:
|
||||||
|
* +5 for every kill they've made since last death in this match.
|
||||||
|
* `15 * kd` ratio, with variable cap based on player's score:
|
||||||
|
* capped at X * 10 for < X * 100 player score
|
||||||
|
(eg 10 for 100, 20 for 200, 40 for 400+).
|
||||||
|
* capped to 40.
|
||||||
|
* deaths > kills gives 0 score
|
||||||
|
* Limited to 0 ≤ X ≤ 100
|
||||||
|
* If they don't have a good weapon: half given score.
|
||||||
|
A good weapon is a non-default one which is not a pistol, stone sword, or crossbow.
|
|
@ -15,36 +15,10 @@ function ctf_stats.get_formspec_match_summary(stats)
|
||||||
return ret
|
return ret
|
||||||
end
|
end
|
||||||
|
|
||||||
local function calc_scores(players)
|
function ctf_stats.get_formspec(title, players)
|
||||||
for i = 1, #players do
|
|
||||||
local pstat = players[i]
|
|
||||||
pstat.kills = pstat.kills or 0
|
|
||||||
pstat.deaths = pstat.deaths or 0
|
|
||||||
pstat.captures = pstat.captures or 0
|
|
||||||
pstat.attempts = pstat.attempts or 0
|
|
||||||
local kd = pstat.kills
|
|
||||||
if pstat.deaths > 0 then
|
|
||||||
kd = kd / pstat.deaths
|
|
||||||
end
|
|
||||||
--[[local killbonus = 0
|
|
||||||
if pstat.kills > 50 and pstat.kills < 200 then
|
|
||||||
killbonus = pstat.kills - 50
|
|
||||||
elseif pstat.kills >= 200 then
|
|
||||||
killbonus = 150
|
|
||||||
end]]--
|
|
||||||
pstat.score = --killbonus +
|
|
||||||
50 * pstat.captures +
|
|
||||||
8 * pstat.attempts +
|
|
||||||
6 * kd
|
|
||||||
end
|
|
||||||
table.sort(players, function(one, two)
|
table.sort(players, function(one, two)
|
||||||
return one.score > two.score
|
return one.score > two.score
|
||||||
end)
|
end)
|
||||||
end
|
|
||||||
ctf_stats.calc_scores = calc_scores
|
|
||||||
|
|
||||||
function ctf_stats.get_formspec(title, players)
|
|
||||||
calc_scores(players)
|
|
||||||
|
|
||||||
local ret = "size[12,6.5]"
|
local ret = "size[12,6.5]"
|
||||||
ret = ret .. "vertlabel[0,0;" .. title .. "]"
|
ret = ret .. "vertlabel[0,0;" .. title .. "]"
|
||||||
|
@ -81,7 +55,9 @@ function ctf_stats.get_formspec(title, players)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ctf_stats.get_html(title, players)
|
function ctf_stats.get_html(title, players)
|
||||||
calc_scores(players)
|
table.sort(players, function(one, two)
|
||||||
|
return one.score > two.score
|
||||||
|
end)
|
||||||
|
|
||||||
local ret = "<h1>" .. title .. "</h1>"
|
local ret = "<h1>" .. title .. "</h1>"
|
||||||
ret = ret .. "<table>" ..
|
ret = ret .. "<table>" ..
|
||||||
|
@ -148,7 +124,11 @@ minetest.register_chatcommand("rankings", {
|
||||||
pstat.color = nil
|
pstat.color = nil
|
||||||
table.insert(players, pstat)
|
table.insert(players, pstat)
|
||||||
end
|
end
|
||||||
calc_scores(players)
|
|
||||||
|
table.sort(players, function(one, two)
|
||||||
|
return one.score > two.score
|
||||||
|
end)
|
||||||
|
|
||||||
local place = -1
|
local place = -1
|
||||||
local me = nil
|
local me = nil
|
||||||
for i = 1, #players do
|
for i = 1, #players do
|
||||||
|
|
|
@ -1,76 +1,95 @@
|
||||||
ctf_stats = {}
|
ctf_stats = {}
|
||||||
|
|
||||||
|
local storage = minetest.get_mod_storage()
|
||||||
|
local data_to_persist = { "matches", "players" }
|
||||||
|
|
||||||
function ctf_stats.load()
|
function ctf_stats.load()
|
||||||
local file = io.open(minetest.get_worldpath().."/ctf_stats.txt", "r")
|
local file = io.open(minetest.get_worldpath() .. "/ctf_stats.txt", "r")
|
||||||
if file then
|
if file then
|
||||||
local table = minetest.deserialize(file:read("*all"))
|
local table = minetest.deserialize(file:read("*all"))
|
||||||
|
file:close()
|
||||||
if type(table) == "table" then
|
if type(table) == "table" then
|
||||||
|
ctf.log("ctf_stats", "Migrating stats...")
|
||||||
ctf_stats.matches = table.matches
|
ctf_stats.matches = table.matches
|
||||||
ctf_stats.current = table.current
|
|
||||||
ctf_stats.players = table.players
|
ctf_stats.players = table.players
|
||||||
return
|
|
||||||
|
for name, player_stats in pairs(ctf_stats.players) do
|
||||||
|
if not player_stats.score or player_stats.score < 0 then
|
||||||
|
player_stats.score = 0
|
||||||
|
end
|
||||||
|
if player_stats.score > 300 then
|
||||||
|
player_stats.score = (player_stats.score - 300) / 30 + 300
|
||||||
|
end
|
||||||
|
if player_stats.score > 800 then
|
||||||
|
player_stats.score = 800
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ctf.needs_save = true
|
||||||
|
end
|
||||||
|
os.remove(minetest.get_worldpath() .. "/ctf_stats.txt")
|
||||||
|
else
|
||||||
|
for _, key in pairs(data_to_persist) do
|
||||||
|
ctf_stats[key] = minetest.parse_json(storage:get_string(key))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
ctf_stats.matches = {
|
ctf_stats.matches = ctf_stats.matches or {
|
||||||
blue_wins = 0,
|
wins = {
|
||||||
red_wins = 0,
|
blue = 0,
|
||||||
|
red = 0,
|
||||||
|
},
|
||||||
skipped = 0
|
skipped = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
ctf_stats.current = {
|
ctf_stats.current = ctf_stats.current or {
|
||||||
red = {},
|
red = {},
|
||||||
blue = {}
|
blue = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctf_stats.players = {}
|
ctf_stats.players = ctf_stats.players or {}
|
||||||
end
|
end
|
||||||
|
|
||||||
ctf.register_on_save(function()
|
ctf.register_on_save(function()
|
||||||
local file = io.open(minetest.get_worldpath().."/ctf_stats.txt", "w")
|
for _, key in pairs(data_to_persist) do
|
||||||
if file then
|
storage:set_string(key, minetest.write_json(ctf_stats[key]))
|
||||||
file:write(minetest.serialize({
|
|
||||||
matches = ctf_stats.matches,
|
|
||||||
current = ctf_stats.current,
|
|
||||||
players = ctf_stats.players
|
|
||||||
}))
|
|
||||||
file:close()
|
|
||||||
else
|
|
||||||
ctf.error("io", "CTF file failed to save!")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
-- Returns a tuple: `player_stats`, `match_player_stats`
|
||||||
function ctf_stats.player(name)
|
function ctf_stats.player(name)
|
||||||
local tplayer = ctf.player(name)
|
local player_stats = ctf_stats.players[name]
|
||||||
local player = ctf_stats.players[name]
|
if not player_stats then
|
||||||
if not player then
|
player_stats = {
|
||||||
player = {
|
|
||||||
name = name,
|
name = name,
|
||||||
red_wins = 0,
|
wins = {
|
||||||
blue_wins = 0,
|
red = 0,
|
||||||
|
blue = 0,
|
||||||
|
},
|
||||||
kills = 0,
|
kills = 0,
|
||||||
deaths = 0,
|
deaths = 0,
|
||||||
captures = 0,
|
captures = 0,
|
||||||
attempts = 0,
|
attempts = 0,
|
||||||
score = -1,
|
score = 0,
|
||||||
}
|
}
|
||||||
ctf_stats.players[name] = player
|
ctf_stats.players[name] = player_stats
|
||||||
end
|
end
|
||||||
|
|
||||||
local mplayer = ctf_stats.current.red[name] or
|
local match_player_stats =
|
||||||
ctf_stats.current.blue[name]
|
ctf_stats.current.red[name] or ctf_stats.current.blue[name]
|
||||||
|
|
||||||
return player, mplayer
|
return player_stats, match_player_stats
|
||||||
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] = ctf_stats.current[tname][name] or {
|
||||||
kills = 0,
|
kills = 0,
|
||||||
|
kills_since_death = 0,
|
||||||
deaths = 0,
|
deaths = 0,
|
||||||
attempts = 0,
|
attempts = 0,
|
||||||
captures = 0
|
captures = 0,
|
||||||
|
score = 0,
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@ -82,15 +101,17 @@ end)
|
||||||
ctf_flag.register_on_capture(function(name, flag)
|
ctf_flag.register_on_capture(function(name, flag)
|
||||||
local main, match = ctf_stats.player(name)
|
local main, match = ctf_stats.player(name)
|
||||||
if main and match then
|
if main and match then
|
||||||
main.captures = main.captures + 1
|
main.captures = main.captures + 1
|
||||||
|
main.score = main.score + 20
|
||||||
match.captures = match.captures + 1
|
match.captures = match.captures + 1
|
||||||
|
match.score = match.score + 20
|
||||||
ctf.needs_save = true
|
ctf.needs_save = true
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ctf_match.register_on_winner(function(winner)
|
ctf_match.register_on_winner(function(winner)
|
||||||
ctf.needs_save = true
|
ctf.needs_save = true
|
||||||
ctf_stats.matches[winner .. "_wins"] = ctf_stats.matches[winner .. "_wins"] + 1
|
ctf_stats.matches.wins[winner] = ctf_stats.matches.wins[winner] + 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ctf_match.register_on_new_match(function()
|
ctf_match.register_on_new_match(function()
|
||||||
|
@ -110,8 +131,10 @@ end)
|
||||||
ctf_flag.register_on_pick_up(function(name, flag)
|
ctf_flag.register_on_pick_up(function(name, flag)
|
||||||
local main, match = ctf_stats.player(name)
|
local main, match = ctf_stats.player(name)
|
||||||
if main and match then
|
if main and match then
|
||||||
main.attempts = main.attempts + 1
|
main.attempts = main.attempts + 1
|
||||||
|
main.score = main.score + 5
|
||||||
match.attempts = match.attempts + 1
|
match.attempts = match.attempts + 1
|
||||||
|
match.score = match.score + 5
|
||||||
ctf.needs_save = true
|
ctf.needs_save = true
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
@ -120,17 +143,73 @@ ctf_flag.register_on_precapture(function(name, flag)
|
||||||
local tplayer = ctf.player(name)
|
local tplayer = ctf.player(name)
|
||||||
local main, match = ctf_stats.player(name)
|
local main, match = ctf_stats.player(name)
|
||||||
if main then
|
if main then
|
||||||
main[tplayer.team .. "_wins"] = main[tplayer.team .. "_wins"] + 1
|
main.wins[tplayer.team] = main.wins[tplayer.team] + 1
|
||||||
ctf.needs_save = true
|
ctf.needs_save = true
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
local good_weapons = {
|
||||||
|
"default:sword_steel",
|
||||||
|
"shooter:grenade",
|
||||||
|
"shooter:shotgun",
|
||||||
|
"shooter:rifle",
|
||||||
|
"shooter:machine_gun",
|
||||||
|
}
|
||||||
|
local function invHasGoodWeapons(inv)
|
||||||
|
for _, weapon in pairs(good_weapons) do
|
||||||
|
if inv:contains_item("main", weapon) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function calculateKillReward(victim, killer)
|
||||||
|
local vmain, victim_match = ctf_stats.player(victim)
|
||||||
|
|
||||||
|
-- +5 for every kill they've made since last death in this match.
|
||||||
|
local reward = victim_match.kills_since_death * 5
|
||||||
|
ctf.log("ctf_stats", "Player " .. victim .. " has made " .. reward .. " kills since last death")
|
||||||
|
|
||||||
|
-- 15 * kd ration, with variable based on player's score
|
||||||
|
local kdreward = 15 * vmain.kills / (vmain.deaths + 1)
|
||||||
|
local max = vmain.score / 10
|
||||||
|
if kdreward > max then
|
||||||
|
kdreward = max
|
||||||
|
end
|
||||||
|
if kdreward > 40 then
|
||||||
|
kdreward = 40
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Limited to 0 <= X <= 100
|
||||||
|
if reward > 100 then
|
||||||
|
reward = 100
|
||||||
|
elseif reward < 0 then
|
||||||
|
reward = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Half if no good weapons
|
||||||
|
local inv = minetest.get_inventory({ type="player", name = victim })
|
||||||
|
if not invHasGoodWeapons(inv) then
|
||||||
|
ctf.log("ctf_stats", "Player " .. victim .. " has no good weapons")
|
||||||
|
reward = reward * 0.5
|
||||||
|
else
|
||||||
|
ctf.log("ctf_stats", "Player " .. victim .. " has good weapons")
|
||||||
|
end
|
||||||
|
|
||||||
|
return reward
|
||||||
|
end
|
||||||
|
|
||||||
ctf.register_on_killedplayer(function(victim, killer)
|
ctf.register_on_killedplayer(function(victim, killer)
|
||||||
local main, match = ctf_stats.player(killer)
|
local main, match = ctf_stats.player(killer)
|
||||||
if main and match then
|
if main and match then
|
||||||
main.kills = main.kills + 1
|
local reward = calculateKillReward(victim, killer)
|
||||||
|
main.kills = main.kills + 1
|
||||||
|
main.score = main.score + reward
|
||||||
match.kills = match.kills + 1
|
match.kills = match.kills + 1
|
||||||
|
match.score = match.score + reward
|
||||||
|
match.kills_since_death = match.kills_since_death + 1
|
||||||
ctf.needs_save = true
|
ctf.needs_save = true
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
@ -140,6 +219,7 @@ minetest.register_on_dieplayer(function(player)
|
||||||
if main and match then
|
if main and match then
|
||||||
main.deaths = main.deaths + 1
|
main.deaths = main.deaths + 1
|
||||||
match.deaths = match.deaths + 1
|
match.deaths = match.deaths + 1
|
||||||
|
match.kills_since_death = 0
|
||||||
ctf.needs_save = true
|
ctf.needs_save = true
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -42,7 +42,6 @@ local function get_is_player_pro(player)
|
||||||
pstat.color = nil
|
pstat.color = nil
|
||||||
table.insert(players, pstat)
|
table.insert(players, pstat)
|
||||||
end
|
end
|
||||||
ctf_stats.calc_scores(players)
|
|
||||||
return ctf_stats.player(player:get_player_name()).score > 0
|
return ctf_stats.player(player:get_player_name()).score > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue