|
- import { CombatTest, Stat, Vigor, Stats, StatToVigor, VoreStats, VoreStat } from '../combat'
- import { Creature } from "../creature"
- import { LogEntry, LogLines, PropElem, LogLine, nilLog } from '../interface'
- import { Verb } from '../language'
-
- function logistic (x0: number, L: number, k: number): (x: number) => number {
- return (x: number) => {
- return L / (1 + Math.exp(-k * (x - x0)))
- }
- }
-
- /**
- * A [[Scorer]] produces a score for a creature in a certain situation.
- *
- * It takes the current score and returns a new one.
- */
- export interface Scorer {
- userScore (attacker: Creature, score: number): number;
- targetScore (defender: Creature, score: number): number;
- explain(user: Creature, target: Creature): LogEntry;
- }
-
- export class OpposedStatScorer implements Scorer {
- private maxStatVigorPenalty = 0.5
- private maxTotalVigorPenalty = 0.1
-
- constructor (private userStats: Partial<Stats & VoreStats>, private targetStats: Partial<Stats & VoreStats>) {
-
- }
-
- explain (user: Creature, target: Creature): LogEntry {
- return new LogLines(
- new LogLine(
- `Pits `,
- ...Object.entries(this.userStats).map(([stat, frac]) => {
- if (frac !== undefined) {
- return new LogLine(`${(frac * 100).toFixed(0)}% `, new PropElem(stat as Stat))
- } else {
- return nilLog
- }
- }),
- ` against `,
- ...Object.entries(this.targetStats).map(([stat, frac]) => {
- if (frac !== undefined) {
- return new LogLine(`${(frac * 100).toFixed(0)}% `, new PropElem(stat as Stat))
- } else {
- return nilLog
- }
- })
- ),
- new LogLine(
- `Score delta: ${(this.computeScore(user, this.userStats) - this.computeScore(target, this.targetStats)).toFixed(1)}`
- )
- )
- }
-
- userScore (attacker: Creature, score: number): number {
- return score + this.computeScore(attacker, this.userStats)
- }
-
- targetScore (defender: Creature, score: number): number {
- return score + this.computeScore(defender, this.targetStats)
- }
-
- private computeScore (subject: Creature, parts: Partial<Stats & VoreStats>): number {
- const total = Object.entries(parts).reduce((total: number, [stat, frac]) => {
- if (stat in Stat) {
- let value = subject.stats[stat as Stat] * (frac === undefined ? 0 : frac)
-
- const vigor = StatToVigor[stat as Stat]
- value = value * (1 - this.maxStatVigorPenalty) + value * this.maxStatVigorPenalty * subject.vigors[vigor] / subject.maxVigors[vigor]
-
- return total + value
- } else if (stat in VoreStat) {
- const value = subject.voreStats[stat as VoreStat] * (frac === undefined ? 0 : frac)
-
- return total + value
- } else {
- return total
- }
- }, 0)
-
- const modifiedTotal = Object.keys(Vigor).reduce(
- (total, vigor) => {
- const base = total * (1 - this.maxTotalVigorPenalty)
- const modified = total * this.maxTotalVigorPenalty * subject.vigors[vigor as Vigor] / subject.maxVigors[vigor as Vigor]
- return base + modified
- },
- total
- )
-
- return modifiedTotal
- }
- }
-
- // TODO this will need to be able to return a LogEntry at some point
-
- abstract class RandomTest implements CombatTest {
- constructor (public fail: (user: Creature, target: Creature) => LogEntry) {
-
- }
-
- test (user: Creature, target: Creature): boolean {
- const userFail = user.effects.map(effect => effect.failTest(user, target))
- if (userFail.some(result => result.failed)) {
- return false
- }
- const targetFail = target.effects.map(effect => effect.failTest(target, user))
- if (targetFail.some(result => result.failed)) {
- return true
- }
-
- return Math.random() < this.odds(user, target)
- }
-
- abstract odds(user: Creature, target: Creature): number
- abstract explain(user: Creature, target: Creature): LogEntry
- }
-
- export enum TestCategory {
- Attack = "Attack",
- Vore = "Vore"
- }
-
- export class CompositionTest extends RandomTest {
- private f: (x: number) => number
- private k = 0.1
-
- constructor (
- private scorers: Scorer[],
- fail: (user: Creature, target: Creature) => LogEntry,
- public category: TestCategory,
- private bias = 0
- ) {
- super(fail)
- this.f = logistic(0, 1, this.k)
- }
-
- explain (user: Creature, target: Creature): LogEntry {
- return new LogLines(
- ...this.scorers.map(scorer => scorer.explain(user, target))
- )
- }
-
- odds (user: Creature, target: Creature): number {
- const userScore = this.scorers.reduce((score, scorer) => scorer.userScore(user, score), 0)
- const targetScore = this.scorers.reduce((score, scorer) => scorer.targetScore(target, score), 0)
-
- const userMod = user.effects.reduce((score, effect) => score + effect.modTestOffense(user, target, this.category), 0)
- const targetMod = target.effects.reduce((score, effect) => score + effect.modTestDefense(target, user, this.category), 0)
-
- return this.f(userScore - targetScore + this.bias)
- }
- }
-
- export class OpposedStatTest extends RandomTest {
- private f: (x: number) => number
- private k = 0.1
-
- // how much a stat can be reduced by its corresponding vigor being low
- private maxStatVigorPenalty = 0.5
-
- // how much the total score can be reduced by each vigor being low
- private maxTotalVigorPenalty = 0.1
-
- constructor (
- public readonly userStats: Partial<Stats & VoreStats>,
- public readonly targetStats: Partial<Stats & VoreStats>,
- fail: (user: Creature, target: Creature) => LogEntry,
- public category: TestCategory,
- private bias = 0
- ) {
- super(fail)
- this.f = logistic(0, 1, this.k)
- }
-
- odds (user: Creature, target: Creature): number {
- const userScore = this.getScoreOffense(user, target, this.userStats)
- const targetScore = this.getScoreDefense(target, user, this.targetStats)
-
- return this.f(userScore - targetScore + this.bias)
- }
-
- explain (user: Creature, target: Creature): LogEntry {
- return new LogLines(
- new LogLine(
- `Pits `,
- ...Object.entries(this.userStats).map(([stat, frac]) => {
- if (frac !== undefined) {
- return new LogLine(`${(frac * 100).toFixed(0)}% of `, new PropElem(stat as Stat), `, `)
- } else {
- return nilLog
- }
- }),
- ` against `,
- ...Object.entries(this.targetStats).map(([stat, frac]) => {
- if (frac !== undefined) {
- return new LogLine(`${(frac * 100).toFixed(0)}% of `, new PropElem(stat as Stat), `, `)
- } else {
- return nilLog
- }
- })
- ),
- new LogLine(
- `${user.name.capital}: ${this.getScoreOffense(user, target, this.userStats)} // ${this.getScoreDefense(target, user, this.targetStats)} :${target.name.capital}`
- ),
- new LogLine(
- `${user.name.capital} ${user.name.conjugate(new Verb("have", "has"))} a ${(this.odds(user, target) * 100).toFixed(0)}% chance of winning this test.`
- )
- )
- }
-
- private getScoreDefense (defender: Creature, attacker: Creature, parts: Partial<Stats>): number {
- return this.getScore(defender, parts) + defender.effects.reduce((total, effect) => total + effect.modTestDefense(defender, attacker, this.category), 0)
- }
-
- private getScoreOffense (attacker: Creature, defender: Creature, parts: Partial<Stats>): number {
- return this.getScore(attacker, parts) + attacker.effects.reduce((total, effect) => total + effect.modTestOffense(attacker, defender, this.category), 0)
- }
-
- private getScore (actor: Creature, parts: Partial<Stats>): number {
- const total = Object.entries(parts).reduce((total: number, [stat, frac]) => {
- if (stat in Stat) {
- let value = actor.stats[stat as Stat] * (frac === undefined ? 0 : frac)
-
- const vigor = StatToVigor[stat as Stat]
- value = value * (1 - this.maxStatVigorPenalty) + value * this.maxStatVigorPenalty * actor.vigors[vigor] / actor.maxVigors[vigor]
-
- return total + value
- } else if (stat in VoreStat) {
- const value = actor.voreStats[stat as VoreStat] * (frac === undefined ? 0 : frac)
-
- return total + value
- } else {
- return total
- }
- }, 0)
-
- const modifiedTotal = Object.keys(Vigor).reduce(
- (total, vigor) => {
- return total * (1 - this.maxStatVigorPenalty) + total * this.maxStatVigorPenalty * actor.vigors[vigor as Vigor] / actor.maxVigors[vigor as Vigor]
- },
- total
- )
-
- return modifiedTotal
- }
- }
-
- export class StatVigorSizeTest extends RandomTest {
- private f: (x: number) => number
- private k = 0.1
-
- constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) {
- super(fail)
- this.f = logistic(0, 1, this.k)
- }
-
- odds (user: Creature, target: Creature): number {
- let userPercent = 1
- let targetPercent = 1
-
- Object.keys(Vigor).forEach(key => {
- userPercent *= user.vigors[key as Vigor] / Math.max(1, user.maxVigors[key as Vigor])
- targetPercent *= target.vigors[key as Vigor] / Math.max(1, target.maxVigors[key as Vigor])
-
- userPercent = Math.max(0, userPercent)
- targetPercent = Math.max(0, targetPercent)
- })
-
- if (userPercent === 0) {
- targetPercent *= 4
- }
-
- if (targetPercent === 0) {
- userPercent *= 4
- }
-
- const sizeOffset = Math.log2(user.voreStats.Mass / target.voreStats.Mass)
-
- return this.f(this.bias + sizeOffset * 5 + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)
- }
-
- explain (user: Creature, target: Creature): LogEntry {
- let result: LogEntry
-
- let userPercent = 1
- let targetPercent = 1
-
- Object.keys(Vigor).forEach(key => {
- userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor]
- targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor]
-
- userPercent = Math.max(0, userPercent)
- targetPercent = Math.max(0, targetPercent)
- })
-
- if (userPercent === 0) {
- targetPercent *= 4
- }
-
- if (targetPercent === 0) {
- userPercent *= 4
- }
-
- const sizeOffset = Math.log2(user.voreStats.Mass / target.voreStats.Mass)
-
- const userMod = user.stats[this.stat] * userPercent
- const targetMod = target.stats[this.stat] * targetPercent
- const delta = userMod - targetMod + sizeOffset * 5
-
- if (delta === 0) {
- result = new LogLine('You and the target have the same effective', new PropElem(this.stat), '.')
- } else if (delta < 0) {
- result = new LogLine('You effectively have ', new PropElem(this.stat, -delta), ' less than your foe.')
- } else {
- result = new LogLine('You effectively have ', new PropElem(this.stat, delta), ' more than you foe.')
- }
-
- result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%')
-
- return result
- }
- }
-
- export class StatVigorTest extends RandomTest {
- private f: (x: number) => number
- private k = 0.1
-
- constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) {
- super(fail)
- this.f = logistic(0, 1, this.k)
- }
-
- odds (user: Creature, target: Creature): number {
- let userPercent = 1
- let targetPercent = 1
-
- Object.keys(Vigor).forEach(key => {
- userPercent *= user.vigors[key as Vigor] / Math.max(1, user.maxVigors[key as Vigor])
- targetPercent *= target.vigors[key as Vigor] / Math.max(1, target.maxVigors[key as Vigor])
-
- userPercent = Math.max(0, userPercent)
- targetPercent = Math.max(0, targetPercent)
- })
-
- if (userPercent === 0) {
- targetPercent *= 4
- }
-
- if (targetPercent === 0) {
- userPercent *= 4
- }
-
- return this.f(this.bias + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)
- }
-
- explain (user: Creature, target: Creature): LogEntry {
- let result: LogEntry
-
- let userPercent = 1
- let targetPercent = 1
-
- Object.keys(Vigor).forEach(key => {
- userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor]
- targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor]
-
- userPercent = Math.max(0, userPercent)
- targetPercent = Math.max(0, targetPercent)
- })
-
- if (userPercent === 0) {
- targetPercent *= 4
- }
-
- if (targetPercent === 0) {
- userPercent *= 4
- }
- const userMod = user.stats[this.stat] * userPercent
- const targetMod = target.stats[this.stat] * targetPercent
- const delta = userMod - targetMod
-
- if (delta === 0) {
- result = new LogLine('You and the target have the same effective', new PropElem(this.stat), '.')
- } else if (delta < 0) {
- result = new LogLine('You effectively have ', new PropElem(this.stat, -delta), ' less than your foe.')
- } else {
- result = new LogLine('You effectively have ', new PropElem(this.stat, delta), ' more than you foe.')
- }
-
- result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%')
-
- return result
- }
- }
-
- export class StatTest extends RandomTest {
- private f: (x: number) => number
- private k = 0.1
-
- constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) {
- super(fail)
- this.f = logistic(0, 1, this.k)
- }
-
- odds (user: Creature, target: Creature): number {
- return this.f(this.bias + user.stats[this.stat] - target.stats[this.stat])
- }
-
- explain (user: Creature, target: Creature): LogEntry {
- const delta: number = user.stats[this.stat] - target.stats[this.stat]
- let result: LogEntry
-
- if (delta === 0) {
- result = new LogLine('You and the target have the same ', new PropElem(this.stat), '.')
- } else if (delta < 0) {
- result = new LogLine('You have ', new PropElem(this.stat, -delta), ' less than your foe.')
- } else {
- result = new LogLine('You have ', new PropElem(this.stat, delta), ' more than you foe.')
- }
-
- result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%')
-
- return result
- }
- }
-
- export class ChanceTest extends RandomTest {
- constructor (public readonly chance: number, fail: (user: Creature, target: Creature) => LogEntry) {
- super(fail)
- }
-
- odds (user: Creature, target: Creature): number {
- return this.chance
- }
-
- explain (user: Creature, target: Creature): LogEntry {
- return new LogLine('You have a flat ' + (100 * this.chance) + '% chance.')
- }
- }
|