Creating Conditional-Diversity-Based Enemy Data Generation Model
Hi guys! In this tutorial, we will demonstrate on how to create diversity-based enemy data generation model so that the enemies are not too easy or too hard for everyone in PvE modes.
Designing The Feature Matrix
Before we can train and generate our models, we first need to design our featureMatrix.
--[[
Techincally, the player combat data information is not quite necessary unless these values changes a lot or you're using it as part of enemy data generation.
Otherwise, "Unconditional-Diversity-Based Enemy Data Generation Model" is more suited here.
--]]
-- A row of 1 is added here for "bias".
local playerCombatDataMatrix = {
{1, player1MaximumHealth, player1MaximumDamage, player1CashAmount},
{1, player2MaximumHealth, player2MaximumDamage, player2CashAmount},
{1, player3MaximumHealth, player3MaximumDamage, player3CashAmount},
}
local enemyDataMatrix = {
{enemy1MaximumHealth, enemy1MaximumDamage, enemy1CashAmount},
{enemy2MaximumHealth, enemy2MaximumDamage, enemy2CashAmount},
{enemy3MaximumHealth, enemy3MaximumDamage, enemy3CashAmount},
}
local noiseMatrix = TensorL:createRandomUniformTensor({3, 1}) -- Single point of variation.
Constructing Our Model
Before we start training our model, we first need to build our model. We have split this to multiple subsections to make it easy to follow through.
Constructing Our Neural Network
local GeneratorNeuralNetwork = DataPredict.Model.NeuralNetwork.new({maximumNumberOfIterations = 1})
GeneratorNeuralNetwork:addLayer(4, true) -- Three player data features, one noise feature and one bias.
GeneratorNeuralNetwork:addLayer(3, false) -- We're outputing three enemy data features and is without bias.
local DiscriminatorNeuralNetwork = DataPredict.Model.NeuralNetwork.new({maximumNumberOfIterations = 1})
DiscriminatorNeuralNetwork:addLayer(6, true) -- Three player data features, three enemy features and one bias.
DiscriminatorNeuralNetwork:addLayer(1, false) -- Discriminator only outputs 1 value.
Constructing Our Deep Reinforcement Learning Model
-- You can use CGAN here. However, for more "stable" model, stick with CWGAN.
local EnemyDataGenerationModel = DataPredict.Model.ConditionalWassersteinGenerativeAdversarialNetwork.new()
-- Inserting our generator and discriminator Neural Networks here.
EnemyDataGenerationModel:setGeneratorModel(GeneratorNeuralNetwork)
EnemyDataGenerationModel:setDiscriminatorModel(DiscriminatorNeuralNetwork)
Training Our Models
Once you created the feature matrix, you must call model’s train() function. This will generate the model parameters.
EnemyDataGenerationModel:train(enemyDataMatrix, noiseDataMatrix, playerCombatDataMatrix)
Generating The Enemy Data
Multiple cases can be done here.
-
Case 1: Binary Generation.
- For a given set of generated enemy data values, the model determines the probability that the player will interact with it. This is then used to spawn or reject the enemy with the generated data values.
-
Case 2: Weighted Generation (Not Recommended)
-
For a given set of generated enemy data values, the model outputs a probability that can be used to modify the generated enemy data.
-
General formula: generatedValue = bestValue * probabilityToInteract. Hence, bestValue = generatedValue / probabilityToInteract.
-
Once bestValue is calculated, spawn an enemy with this best value data.
-
But first, let initialize an array so that we can control how many enemies we should generate.
local activeEnemyDataArray = {}
local maximumNumberOfEnemies = 10
Case 1: Binary Generation
local noiseVector
local playerCombatDataVector
local enemyDataVector
local playerCombatDataAndEnemyDataVector
local probabilityForPlayerToInteract
local isAcceptable = false
while true do
if (#activeEnemyDataArray > maximumNumberOfEnemies) then continue end
repeat
noiseVector =
playerCombatDataVector = getPlayerDataVector()
enemyDataVector = EnemyDataGenerationModel:generate(noiseVector, playerCombatDataVector)
probabilityForPlayerToInteract = EnemyDataGenerationModel:evaluate(enemyDataVector)[1][1]
isAcceptable = (probabilityForPlayerToInteract >= 0.5)
until isAcceptable
summonEnemy(enemyDataVector)
end
Case 2: Weighted Generation
local noiseVector
local playerCombatDataVector
local enemyDataVector
local playerCombatDataAndEnemyDataVector
local probabilityForPlayerToInteract
while true do
if (#activeEnemyDataArray > maximumNumberOfEnemies) then continue end
noiseVector =
playerCombatDataVector = getPlayerDataVector()
enemyDataVector = EnemyDataGenerationModel:generate(noiseVector, playerCombatDataVector)
probabilityForPlayerToInteract = EnemyDataGenerationModel:evaluate(enemyDataVector)[1][1]
enemyDataVector = TensorL:divide(enemyDataVector, probabilityForPlayerToInteract)
summonEnemy(enemyDataVector)
end
Upon Player Interaction With Enemy.
--[[
You can keep all the data or periodically clear it upon model training.
I recommend the latter because it makes sure we don't include old data that might not be relevant to the current session.
Additionally, using the whole data is computationally expensive and may impact players' gameplay experience.
--]]
local playerCombatDataMatrix = {}
local playerEnemyDataMatrix = {}
local function onEnemyKilled(Enemy, Player)
local playerCombatDataVector = getPlayerCombatDataVector(Player)
local enemyDataVector = getEnemyDataVector(Enemy)
table.insert(playerCombatDataMatrix, playerCombatDataVector[1])
table.insert(playerEnemyDataMatrix, enemyDataVector[1])
removeEnemyDataFromActiveEnemyDataArray(Enemy)
end
That’s all for today!