Creating Probability-Based Enemy Data Generation Model

Hi guys! In this tutorial, we will demonstrate on how to create probability-based enemy data generation model so that the enemies are not too easy or too hard for everyone in PvE modes.

For best results, you must use one class support vector machine.

Initializing The Probability Model

Before we can produce ourselves a difficulty generation model, we first need to construct a model, which is shown below. Ensure that the kernel function is “RadialBasisFunction”.


 -- For this tutorial, we will assume that the player intentionally killed 90% of the enemies.

local EnemyDataGenerationModel = DataPredict.Models.OneClassSupportVectorMachine.new({maximumNumberOfIterations = 100, kernelFunction = "RadialBasisFunction", beta = 0.9})

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.

local playerCombatDataMatrix = {

  {player1MaximumHealth, player1MaximumDamage, player1CashAmount},
  {player2MaximumHealth, player2MaximumDamage, player2CashAmount},
  {player3MaximumHealth, player3MaximumDamage, player3CashAmount},

}

local enemyDataMatrix = {

  {enemy1MaximumHealth, enemy1MaximumDamage, enemy1CashAmount},
  {enemy2MaximumHealth, enemy2MaximumDamage, enemy2CashAmount},
  {enemy3MaximumHealth, enemy3MaximumDamage, enemy3CashAmount},

}

local playerCombatDataAndEnemyDataMatrix = TensorL:concatenate(playerCombatDataMatrix, enemyDataMatrix, 2)

Training Our Models

Once you created the feature matrix, you must call model’s train() function. This will generate the model parameters.


EnemyDataGenerationModel:train(playerCombatDataAndEnemyDataMatrix)

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 * probabilityToInteractFromSupportVectorMachine. Hence, bestValue = generatedValue / probabilityToInteractFromSupportVectorMachine.

    • 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

Optionally, we can also generate enemy data vector based on the model parameters.


local function generateEnemyDataVector()

 local ModelParameters = EnemyDataGenerationModel:getModelParameters()

 local enemyMaximumHealth = ModelParameters[1][1]

 local enemyMaximumDamage = ModelParameters[2][1]

 local enemyCashAmount = ModelParameters[3][1]

 local enemyMaximumHealthRandomNoise = math.random()

 local enemyMaximumDamageRandomNoise = math.random()

 local enemyCashAmountRandomNoise = math.random()

 return 

end

Case 1: Binary Generation


local playerCombatDataVector

local enemyDataVector

local playerCombatDataAndEnemyDataVector

local probabilityForPlayerToInteract

local isAcceptable = false

while true do

  if (#activeEnemyDataArray > maximumNumberOfEnemies) then continue end

  repeat

    playerCombatDataVector = getPlayerDataVector()

    enemyDataVector = generateEnemyDataVector()

    playerCombatDataAndEnemyDataVector = TensorL:concatenate(playerCombatDataVector, enemyDataVector, 2)

    probabilityForPlayerToInteract = EnemyDataGenerationModel:predict(playerCombatDataAndEnemyDataVector)[1][1]

    isAcceptable = (probabilityForPlayerToInteract >= 0.5)

  until isAcceptable

  summonEnemy(enemyDataVector)

end

Case 2: Weighted Generation


local playerCombatDataVector

local enemyDataVector

local playerCombatDataAndEnemyDataVector

local probabilityForPlayerToInteract

while true do

  if (#activeEnemyDataArray > maximumNumberOfEnemies) then continue end

 playerCombatDataVector = getPlayerDataVector()

 enemyDataVector = generateEnemyDataVector()

 playerCombatDataAndEnemyDataVector = TensorL:concatenate(playerCombatDataVector, enemyDataVector, 2)

 probabilityForPlayerToInteract = EnemyDataGenerationModel:predict(playerCombatDataAndEnemyDataVector)[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 playerCombatDataAndEnemyDataMatrix = {}

local function onEnemyKilled(Enemy, Player)

  local playerCombatDataVector = getPlayerCombatDataVector(Player)

  local enemyDataVector = getEnemyDataVector(Enemy)

  local playerCombatDataAndEnemyDataVector = TensorL:concatenate(playerCombatDataVector, enemyDataVector, 2)

  table.insert(playerCombatDataAndEnemyDataMatrix, playerCombatDataAndEnemyDataVector[1])

  removeEnemyDataFromActiveEnemyDataArray(Enemy)

end

That’s all for today!