Jump to content

What is the combat formula now?


Pelinor
 Share

Recommended Posts

Back when classes were a thing, which girls you went after mattered since harmony was related. Now the harmony stat is nothing more than a critical hit chance indicator (with RNG a poor one). The scripts have a combat simulator, but now the simulation results require a constant back and forth between the League overview page and the combat screen. I asked what the current algorithm is so I could add it to my spreadsheet.

  • Like 1
Link to comment
Share on other sites

1 hour ago, Pelinor said:

I asked what the current algorithm is so I could add it to my spreadsheet.

Was there ever a single formula possible? I mean the scripts do an iterative simulation for a reason, and do not try to calculate odds with a single analytic formula. I actually thought about develop some, which at least come close to the actual odds, but as far as I can think of it is impossible to get precise odds for a single opponent with an analytic formula, but rough averages which do not consider a specific number of rounds, of where you need to give a best guess number of rounds as input.

Just to get a better idea what you are looking for: A spreadsheet where you can enter your's and opponent's stats (AP, ego, defence, harmony, crit bonus) to get some rough win chance and/or expected points?

I also find it annoying that one must navigate back and forth to see the script results. But maybe Zoo and/or Tom will add them to the overview page/table as well. The update is pretty new and they just stated to tackle all the needed script updates to deal with the changes. What I currently do is using the new "Power" value as an idea in which area the odds are. This works quite well: Two opponents with the same power also have very similar simulation results. What I am missing is the team colour, to know whether I can raise my odds with a good counter team or not.

Link to comment
Share on other sites

1 hour ago, Horsting said:

Was there ever a single formula possible? I mean the scripts do an iterative simulation for a reason

And you did not think that this formula was actually used in this script? Because it was the script that performed the full calculations for the simulation of battles. It doesn't send thousands of requests to the server, to which it gives thousands of answers so that the script brings them together. It takes the stats of both opponents (you and your enemy) and performs thousands of calculations using a specific formula and gives you the processed results.

  • Like 4
Link to comment
Share on other sites

54 minutes ago, Master-17 said:

It takes the stats of both opponents (you and your enemy) and performs thousands of calculations using a specific formula and gives you the processed results.

Exactly (I could not have said this better) so. I don't want to reinvent the wheel, but I would like a "ballpark" formula for my spreadsheet so I don't always go for the default "attack strength" guideline on combat.

Link to comment
Share on other sites

There is a difference between a simulation, in this case one which step by steps loops through every round of every possible outcome of the fight, varying because you and your opponent do or do not crit in every round, and a single analytic formula which you could enter into a hand calculator after entering a number of variables and get a result. Excel (and alike) can deal with analytic formulas, but not with simulations. A "program" like the HH++ JavaScript tool can do simulations.

Theoretically it is possible to create analytic formulas from simulations, but those are usually too complex for anyone to deal with. Best you can practically do is to approximate simulation results with an analytic formula, but it is not trivial to find one which does this well.

Do you still have the spreadsheet you used for the old battle system, so I can get an idea how it worked?

Link to comment
Share on other sites

  • Moderator
class Simulator {
    constructor({player, opponent, logging, preSim}) {
        this.player = player
        this.opponent = opponent
        this.logging = logging
        this.preSim = preSim
    }

    run () {
        if (this.logging) {
            console.log('Running simulation against', this.opponent.name)
            console.groupCollapsed('Player')
            console.dir({...this.player})
            console.groupEnd()
            console.groupCollapsed(this.opponent.name)
            console.dir({...this.opponent})
            console.groupEnd()
        }

        const setup = x => {
            x.critMultiplier = 2 + x.bonuses.critDamage
            x.dmg = Math.max(0, x.dmg)
            x.baseAttack = {
                probability: 1 - x.critchance,
                damageAmount: Math.ceil(x.dmg),
                healAmount: Math.ceil(x.dmg * x.bonuses.healOnHit)
            }
            x.critAttack = {
                probability: x.critchance,
                damageAmount: Math.ceil(x.dmg * x.critMultiplier),
                healAmount: Math.ceil(x.dmg * x.critMultiplier * x.bonuses.healOnHit)
            }
            x.hp = Math.ceil(x.hp)
        }

        setup(this.player)
        setup(this.opponent)

        this.cache = {}
        this.runs = 0

        let ret
        try {
            // start simulation from player's turn
            ret = this.playerTurn(this.player.hp, this.opponent.hp, 0)
        } catch (error) {
            if (this.logging) console.log(`An error occurred during the simulation against ${this.opponent.name}`, error)
            return {
                points: [],
                win: Number.NaN,
                loss: Number.NaN,
                avgTurns: Number.NaN,
                scoreClass: 'minus'
            }
        }

        const sum = ret.win + ret.loss
        ret.win /= sum
        ret.loss /= sum
        ret.scoreClass = ret.win>0.9?'plus':ret.win<0.5?'minus':'close'

        if (this.logging) {console.log(`Ran ${this.runs} simulations against ${this.opponent.name}; aggregated win chance: ${ret.win * 100}%, average turns: ${ret.avgTurns}`)}

        if (this.preSim) {
            if (ret.win <= 0) ret.impossible = true
            if (ret.loss <= 0) ret.guaranteed = true
        }

        return ret
    }

    mergeResult(x, xProbability, y, yProbability) {
        const points = {}
        Object.entries(x.points).map(([point, probability]) => [point, probability * xProbability])
            .concat(Object.entries(y.points).map(([point, probability]) => [point, probability * yProbability]))
            .forEach(([point, probability]) => {
                points[point] = (points[point] || 0) + probability
            })
        const merge = (x, y) =>  x * xProbability + y * yProbability
        const win = merge(x.win, y.win)
        const loss = merge(x.loss, y.loss)
        const avgTurns = merge(x.avgTurns, y.avgTurns)
        return { points, win, loss, avgTurns }
    }

    playerTurn(playerHP, opponentHP, turns) {
        turns += 1
        // avoid a stack overflow
        const maxAllowedTurns = 50
        if (turns > maxAllowedTurns) throw new Error()

        // read cache
        const cachedResult = this.cache?.[playerHP]?.[opponentHP]
        if (cachedResult) return cachedResult

        // simulate base attack and critical attack
        const baseAtk = this.player.baseAttack
        const baseAtkResult = this.playerAttack(playerHP, opponentHP, baseAtk, turns)
        const critAtk = this.player.critAttack
        const critAtkResult = this.playerAttack(playerHP, opponentHP, critAtk, turns)
        // merge result
        const mergedResult = this.mergeResult(baseAtkResult, baseAtk.probability, critAtkResult, critAtk.probability)

        // count player's turn
        mergedResult.avgTurns += 1

        // write cache
        if (!this.cache[playerHP]) this.cache[playerHP] = {}
        if (!this.cache[playerHP][opponentHP]) this.cache[playerHP][opponentHP] = {}
        this.cache[playerHP][opponentHP] = mergedResult

        return mergedResult
    }

    playerAttack(playerHP, opponentHP, attack, turns) {
        // damage
        opponentHP -= attack.damageAmount

        // heal on hit
        playerHP += attack.healAmount
        playerHP = Math.min(playerHP, this.player.hp)

        // check win
        if (opponentHP <= 0) {
            const point = 15 + Math.ceil(10 * playerHP / this.player.hp)
            this.runs += 1
            return { points: { [point]: 1 }, win: 1, loss: 0, avgTurns: 0 }
        }

        // next turn
        return this.opponentTurn(playerHP, opponentHP, turns)
    }

    opponentTurn(playerHP, opponentHP, turns) {
        // simulate base attack and critical attack
        const baseAtk = this.opponent.baseAttack
        const baseAtkResult = this.opponentAttack(playerHP, opponentHP, baseAtk, turns)
        const critAtk = this.opponent.critAttack
        const critAtkResult = this.opponentAttack(playerHP, opponentHP, critAtk, turns)
        // merge result
        return this.mergeResult(baseAtkResult, baseAtk.probability, critAtkResult, critAtk.probability)
    }

    opponentAttack(playerHP, opponentHP, attack, turns) {
        // damage
        playerHP -= attack.damageAmount

        // heal on hit
        opponentHP += attack.healAmount
        opponentHP = Math.min(opponentHP, this.opponent.hp)

        // check loss
        if (playerHP <= 0) {
            const point = 3 + Math.ceil(10 * (this.opponent.hp - opponentHP) / this.opponent.hp)
            this.runs += 1
            return { points: { [point]: 1 }, win: 0, loss: 1, avgTurns: 0 }
        }

        // next turn
        return this.playerTurn(playerHP, opponentHP, turns)
    }
}

export default Simulator
window.HHPlusPlus.Simulator = Simulator

 

Edited by Ravi-Sama
Copy/Pasted correct Sim code.
  • Thanks 1
Link to comment
Share on other sites

1 hour ago, Ravi-Sama said:

I think this is what the sim does:

This is only the code for collecting player and opponent data and calculate the final crit chance for both, not the simulation itself. Here is the simulator of Zoo's BDSM version: https://github.com/zoop0kemon/hh-plus-plus/blob/main/src/modules/BattleSimulatorModule/Simulator.js

Core part is those nested playerTurn => playerAttack (once with, once without crit, splitting the path) => opponentTurn => opponentAttack (once with, once without crit, splitting the path) iterations which build up a tree of all possible outcomes and aggregate the results along with their individual chance to happen.

  • Thanks 2
Link to comment
Share on other sites

  • Moderator
11 minutes ago, Horsting said:

This is only the code for collecting player and opponent data and calculate the final crit chance for both, not the simulation itself. Here is the simulator of Zoo's BDSM version: https://github.com/zoop0kemon/hh-plus-plus/blob/main/src/modules/BattleSimulatorModule/Simulator.js

Core part is those nested playerTurn => playerAttack (once with, once without crit, splitting the path) => opponentTurn => opponentAttack (once with, once without crit, splitting the path) iterations which build up a tree of all possible outcomes and aggregate the results along with their individual chance to happen.

Cool, makes sense.

Link to comment
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
 Share

×
×
  • Create New...