From 5df582851a64ac8cc7545dab243ec6935c495605 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 26 Jan 2024 16:32:23 +0100 Subject: [PATCH] Allow AI to have some definition of rating (#5855) --- lua/system/repr.lua | 16 +-- lua/ui/lobby/aitypes.lua | 211 +++++++++++++++++++++++++++---- lua/ui/lobby/data/playerdata.lua | 1 + lua/ui/lobby/lobby.lua | 90 +++++++++++-- 4 files changed, 279 insertions(+), 39 deletions(-) diff --git a/lua/system/repr.lua b/lua/system/repr.lua index 7303b34685..7c42fa1e9a 100644 --- a/lua/system/repr.lua +++ b/lua/system/repr.lua @@ -19,7 +19,7 @@ local skip = { } local function IsState(t) - return (t.__State and true) or false + return (rawget(t, "__State") and true) or false end local function IsVector(t) @@ -27,31 +27,31 @@ local function IsVector(t) end local function IsUnit(t) - return (t.AddCommandCap and true) or false + return (rawget(t, "AddCommandCap") and true) or false end local function IsProp(t) - return (t.AddPropCallback and true) or false + return (rawget(t, "AddPropCallback") and true) or false end local function IsProjectile(t) - return (t.ChangeDetonateBelowHeight and true) or false + return (rawget(t, "ChangeDetonateBelowHeight") and true) or false end local function IsBrain(t) - return (t.AssignThreatAtPosition and true) or false + return (rawget(t, "AssignThreatAtPosition") and true) or false end local function IsWeapon(t) - return (t.WeaponHasTarget and true) or false + return (rawget(t, "WeaponHasTarget") and true) or false end local function IsTrashbag(t) - return (t.Add and t.Destroy and t.Empty and true) or false + return (rawget(t, "Add") and rawget(t, "Destroy") and rawget(t, "Empty") and true) or false end local function IsLazyVar(t) - return (t.Set and t.SetFunction and t.SetValue and true) or false + return (rawget(t, "Set") and rawget(t, "SetFunction") and rawget(t, "SetValue") and true) or false end local function _FormatHeader(t) diff --git a/lua/ui/lobby/aitypes.lua b/lua/ui/lobby/aitypes.lua index d8565f5f4e..284de7a44f 100644 --- a/lua/ui/lobby/aitypes.lua +++ b/lua/ui/lobby/aitypes.lua @@ -6,50 +6,130 @@ --* Copyright © 2006 Gas Powered Games, Inc. All rights reserved. --***************************************************************************** +---@class AILobbyProperties +---@field key string +---@field name string +---@field rating? number +---@field ratingCheatMultiplier? number +---@field ratingBuildMultiplier? number +---@field ratingMapMultiplier? number[] +---@field ratingOmniBonus? number +---@field ratingNegativeThreshold? number + function GetAItypes() --Table of AI Names to return local aitypes = { { key = 'easy', name = "AI: Easy", - requiresNavMesh = true, - baseAI = true, + + rating = 200, + ratingCheatMultiplier = 0.0, + ratingBuildMultiplier = 0.0, + ratingOmniBonus = 0.0, + ratingMapMultiplier = { + [256] = 1.0, -- 5x5 + [512] = 1.0, -- 10x10 + [1024] = 0.9, -- 20x20 + [2048] = 0.75, -- 40x40 + [4096] = 0.6, -- 80x80 + } }, { key = 'medium', name = "AI: Normal", - requiresNavMesh = true, - baseAI = true, + + rating = 300, + ratingCheatMultiplier = 0.0, + ratingBuildMultiplier = 0.0, + ratingOmniBonus = 0.0, + ratingMapMultiplier = { + [256] = 1.0, -- 5x5 + [512] = 1.0, -- 10x10 + [1024] = 0.9, -- 20x20 + [2048] = 0.75, -- 40x40 + [4096] = 0.6, -- 80x80 + } }, { key = 'adaptive', name = "AI: Adaptive", - requiresNavMesh = true, - baseAI = true, + + rating = 450, + ratingCheatMultiplier = 0.0, + ratingBuildMultiplier = 0.0, + ratingOmniBonus = 0.0, + ratingMapMultiplier = { + [256] = 1.0, -- 5x5 + [512] = 1.0, -- 10x10 + [1024] = 0.9, -- 20x20 + [2048] = 0.75, -- 40x40 + [4096] = 0.6, -- 80x80 + } }, { key = 'rush', name = "AI: Rush", - requiresNavMesh = true, - baseAI = true, + + rating = 450, + ratingCheatMultiplier = 0.0, + ratingBuildMultiplier = 0.0, + ratingOmniBonus = 0.0, + ratingMapMultiplier = { + [256] = 1.0, -- 5x5 + [512] = 1.0, -- 10x10 + [1024] = 0.9, -- 20x20 + [2048] = 0.75, -- 40x40 + [4096] = 0.6, -- 80x80 + } }, { key = 'turtle', name = "AI: Turtle", - requiresNavMesh = true, - baseAI = true, + + rating = 450, + ratingCheatMultiplier = 0.0, + ratingBuildMultiplier = 0.0, + ratingOmniBonus = 0.0, + ratingMapMultiplier = { + [256] = 1.0, -- 5x5 + [512] = 1.0, -- 10x10 + [1024] = 0.9, -- 20x20 + [2048] = 0.75, -- 40x40 + [4096] = 0.6, -- 80x80 + } }, { key = 'tech', name = "AI: Tech", - requiresNavMesh = true, - baseAI = true, + + rating = 450, + ratingCheatMultiplier = 0.0, + ratingBuildMultiplier = 0.0, + ratingOmniBonus = 0.0, + ratingMapMultiplier = { + [256] = 1.0, -- 5x5 + [512] = 1.0, -- 10x10 + [1024] = 0.9, -- 20x20 + [2048] = 0.75, -- 40x40 + [4096] = 0.6, -- 80x80 + } }, { key = 'random', name = "AI: Random", - requiresNavMesh = true, - baseAI = true, + + rating = 500, + ratingCheatMultiplier = 0.0, + ratingBuildMultiplier = 0.0, + ratingOmniBonus = 0.0, + ratingMapMultiplier = { + [256] = 1.0, -- 5x5 + [512] = 1.0, -- 10x10 + [1024] = 0.9, -- 20x20 + [2048] = 0.75, -- 40x40 + [4096] = 0.6, -- 80x80 + } } } @@ -84,9 +164,9 @@ function GetAItypes() -- loop over all installed mods for Index, ModData in simMods do -- check if we have a CustomAIs_v2 folder (then we have an AI mod) - if exists(ModData.location..'/lua/AI/CustomAIs_v2') then + if exists(ModData.location .. '/lua/AI/CustomAIs_v2') then -- get all AI files from CustomAIs_v2 folder - ModAIFiles = DiskFindFiles(ModData.location..'/lua/AI/CustomAIs_v2', '*.lua') + ModAIFiles = DiskFindFiles(ModData.location .. '/lua/AI/CustomAIs_v2', '*.lua') -- check, if we have found at least 1 file if ModAIFiles[1] then -- loop over all AI files @@ -106,11 +186,96 @@ function GetAItypes() end --Default GPG Cheating AIs - table.insert(aitypes, { key = 'adaptivecheat', name = "AIx: Adaptive", requiresNavMesh = true, baseAI = true }) - table.insert(aitypes, { key = 'rushcheat', name = "AIx: Rush", requiresNavMesh = true, baseAI = true }) - table.insert(aitypes, { key = 'turtlecheat', name = "AIx: Turtle", requiresNavMesh = true, baseAI = true}) - table.insert(aitypes, { key = 'techcheat', name = "AIx: Tech", requiresNavMesh = true, baseAI = true }) - table.insert(aitypes, { key = 'randomcheat', name = "AIx: Random", requiresNavMesh = true, baseAI = true }) + table.insert(aitypes, + { + key = 'adaptivecheat', + name = "AIx: Adaptive", + + rating = 450, + ratingCheatMultiplier = 150.0, + ratingBuildMultiplier = 150.0, + ratingOmniBonus = 200, + ratingNegativeThreshold = -50, + ratingMapMultiplier = { + [256] = 1.0, -- 5x5 + [512] = 1.0, -- 10x10 + [1024] = 0.9, -- 20x20 + [2048] = 0.75, -- 40x40 + [4096] = 0.6, -- 80x80 + } + }) + table.insert(aitypes, + { + key = 'rushcheat', + name = "AIx: Rush", + + rating = 450, + ratingCheatMultiplier = 150.0, + ratingBuildMultiplier = 150.0, + ratingOmniBonus = 200, + ratingNegativeThreshold = -50, + ratingMapMultiplier = { + [256] = 1.0, -- 5x5 + [512] = 1.0, -- 10x10 + [1024] = 0.9, -- 20x20 + [2048] = 0.75, -- 40x40 + [4096] = 0.6, -- 80x80 + } + }) + table.insert(aitypes, + { + key = 'turtlecheat', + name = "AIx: Turtle", + + rating = 450, + ratingCheatMultiplier = 150.0, + ratingBuildMultiplier = 150.0, + ratingOmniBonus = 200, + ratingNegativeThreshold = -50, + ratingMapMultiplier = { + [256] = 1.0, -- 5x5 + [512] = 1.0, -- 10x10 + [1024] = 0.9, -- 20x20 + [2048] = 0.75, -- 40x40 + [4096] = 0.6, -- 80x80 + } + }) + table.insert(aitypes, + { + key = 'techcheat', + name = "AIx: Tech", + + rating = 450, + ratingCheatMultiplier = 150.0, + ratingBuildMultiplier = 150.0, + ratingOmniBonus = 200, + ratingNegativeThreshold = -50, + ratingMapMultiplier = { + [256] = 1.0, -- 5x5 + [512] = 1.0, -- 10x10 + [1024] = 0.9, -- 20x20 + [2048] = 0.75, -- 40x40 + [4096] = 0.6, -- 80x80 + } + }) + table.insert(aitypes, + { + key = 'randomcheat', + name = "AIx: Random", + + rating = 550, + ratingCheatMultiplier = 150.0, + ratingBuildMultiplier = 150.0, + ratingOmniBonus = 200, + ratingNegativeThreshold = -50, + ratingMapMultiplier = { + [256] = 1.0, -- 5x5 + [512] = 1.0, -- 10x10 + [1024] = 0.9, -- 20x20 + [2048] = 0.75, -- 40x40 + [4096] = 0.6, -- 80x80 + } + }) --Load Custom Cheating AIs - old style for i, v in AIFilesold do @@ -135,9 +300,9 @@ function GetAItypes() -- loop over all installed mods for Index, ModData in simMods do -- check if we have a CustomAIs_v2 folder (then we have an AI mod) - if exists(ModData.location..'/lua/AI/CustomAIs_v2') then + if exists(ModData.location .. '/lua/AI/CustomAIs_v2') then -- get all AI files from CustomAIs_v2 folder - ModAIFiles = DiskFindFiles(ModData.location..'/lua/AI/CustomAIs_v2', '*.lua') + ModAIFiles = DiskFindFiles(ModData.location .. '/lua/AI/CustomAIs_v2', '*.lua') -- check, if we have found at least 1 file if ModAIFiles[1] then -- loop over all AI files diff --git a/lua/ui/lobby/data/playerdata.lua b/lua/ui/lobby/data/playerdata.lua index ca6dbd45ad..2c9aecfab5 100644 --- a/lua/ui/lobby/data/playerdata.lua +++ b/lua/ui/lobby/data/playerdata.lua @@ -18,6 +18,7 @@ ---@field Ready boolean ---@field StartSpot number ---@field Team number +---@field AILobbyProperties? AILobbyProperties local WatchedValueTable = import("/lua/ui/lobby/data/watchedvalue/watchedvaluetable.lua").WatchedValueTable diff --git a/lua/ui/lobby/lobby.lua b/lua/ui/lobby/lobby.lua index cc77f9de70..ffaf12f159 100644 --- a/lua/ui/lobby/lobby.lua +++ b/lua/ui/lobby/lobby.lua @@ -456,6 +456,66 @@ function GetLocalPlayerData() ) end +--- Compute an estimation of the rating of the given AI. The values originate from 'aitypes.lua' +---@param gameOptions table +---@param aiLobbyProperties AILobbyProperties +---@return number +function ComputeAIRating(gameOptions, aiLobbyProperties) + + if not aiLobbyProperties then + return 0 + end + + if not aiLobbyProperties.rating then + return 0 + end + + if not gameInfo.GameOptions.ScenarioFile then + return 0 + end + + -- try and take into account map + local scenarioInfo = MapUtil.LoadScenario(gameInfo.GameOptions.ScenarioFile) + if not (scenarioInfo and scenarioInfo.size and scenarioInfo.size[1] and scenarioInfo.size[2]) then + return 0 + end + + -- clamp the value + local maparea = math.max(scenarioInfo.size[1], scenarioInfo.size[2]) + if maparea < 256 then + maparea = 256 + elseif maparea > 4096 then + maparea = 4096 + end + + -- process various multipliers to determine rating + local mapMultiplier = aiLobbyProperties.ratingMapMultiplier[maparea] or 1.0 + local cheatBuildMultiplier = (tonumber(gameOptions.BuildMult) or 1.0) - 1.0 + local cheatResourceMultiplier = (tonumber(gameOptions.CheatMult) or 1.0) - 1.0 + + -- if they're smaller than 1.0 then the AI doesn't get better; it gets worse! + if cheatBuildMultiplier < 0 then + cheatBuildMultiplier = 1 / cheatBuildMultiplier + end + + if cheatResourceMultiplier < 0 then + cheatResourceMultiplier = 1 / cheatResourceMultiplier + end + + -- compute the rating + local cheatBuildValue = (aiLobbyProperties.ratingBuildMultiplier or 0.0) * cheatBuildMultiplier + local cheatResourceValue = (aiLobbyProperties.ratingCheatMultiplier or 0.0) * cheatResourceMultiplier + local cheatOmniValue = (gameOptions.OmniCheat == 'on' and aiLobbyProperties.ratingOmniBonus) or 0.0 + local rating = mapMultiplier * (aiLobbyProperties.rating + cheatBuildValue + cheatResourceValue + cheatOmniValue) + + -- prevent very low numbers + if rating < aiLobbyProperties.ratingNegativeThreshold then + rating = aiLobbyProperties.ratingNegativeThreshold + (rating - aiLobbyProperties.ratingNegativeThreshold) * 0.2 + end + + return math.floor(rating) +end + function GetAIPlayerData(name, AIPersonality, slot) local AIColor -- gets the color of the player/AI occupying the slot directly prior if available @@ -472,12 +532,11 @@ function GetAIPlayerData(name, AIPersonality, slot) end -- retrieve properties from AI table - local baseAI = false - local requiresNavMesh = false + ---@type AILobbyProperties | nil + local aiLobbyProperties = nil for k, entry in aitypes do if entry.key == AIPersonality then - requiresNavMesh = requiresNavMesh or entry.requiresNavMesh - baseAI = baseAI or entry.baseAI + aiLobbyProperties = entry end end @@ -491,9 +550,10 @@ function GetAIPlayerData(name, AIPersonality, slot) PlayerColor = AIColor, ArmyColor = AIColor, - -- properties from AI table - RequiresNavMesh = requiresNavMesh, - BaseAI = baseAI + PL = ComputeAIRating(gameInfo.GameOptions, aiLobbyProperties), + + -- keep track of the AI lobby properties for easier access + AILobbyProperties = aiLobbyProperties, } ) end @@ -2157,9 +2217,13 @@ local function TryLaunch(skipNoObserversCheck) local allRatings = {} local clanTags = {} for k, player in gameInfo.PlayerOptions do - if player.Human and player.PL then + if player.PL then allRatings[player.PlayerName] = player.PL clanTags[player.PlayerName] = player.PlayerClan + + if not player.Human then + allRatings[player.PlayerName] = ComputeAIRating(gameInfo.GameOptions, player.AILobbyProperties) + end end if player.OwnerID == localPlayerID then @@ -2256,6 +2320,16 @@ local function UpdateGame() if gameInfo.GameOptions.ScenarioFile and (gameInfo.GameOptions.ScenarioFile ~= "") then scenarioInfo = MapUtil.LoadScenario(gameInfo.GameOptions.ScenarioFile) + -- update AI rating as game settings change + for k = 1, 16 do + local playerOptions = gameInfo.PlayerOptions[k] + if playerOptions then + if not playerOptions.Human then + playerOptions.PL = ComputeAIRating(gameInfo.GameOptions, playerOptions.AILobbyProperties); + end + end + end + if scenarioInfo and scenarioInfo.map and scenarioInfo.map ~= '' then GUI.mapView:SetScenario(scenarioInfo) ShowMapPositions(GUI.mapView, scenarioInfo)