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, private targetStats: Partial) { } 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): 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, public readonly targetStats: Partial, 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): 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): 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): 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.') } }