Feast 2.0!
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

441 lines
14 KiB

  1. import { CombatTest, Stat, Vigor, Stats, StatToVigor, VoreStats, VoreStat } from '../combat'
  2. import { Creature } from "../creature"
  3. import { LogEntry, LogLines, PropElem, LogLine, nilLog } from '../interface'
  4. import { Verb } from '../language'
  5. function logistic (x0: number, L: number, k: number): (x: number) => number {
  6. return (x: number) => {
  7. return L / (1 + Math.exp(-k * (x - x0)))
  8. }
  9. }
  10. /**
  11. * A [[Scorer]] produces a score for a creature in a certain situation.
  12. *
  13. * It takes the current score and returns a new one.
  14. */
  15. export interface Scorer {
  16. userScore (attacker: Creature, score: number): number;
  17. targetScore (defender: Creature, score: number): number;
  18. explain(user: Creature, target: Creature): LogEntry;
  19. }
  20. export class OpposedStatScorer implements Scorer {
  21. private maxStatVigorPenalty = 0.5
  22. private maxTotalVigorPenalty = 0.1
  23. constructor (private userStats: Partial<Stats & VoreStats>, private targetStats: Partial<Stats & VoreStats>) {
  24. }
  25. explain (user: Creature, target: Creature): LogEntry {
  26. return new LogLines(
  27. new LogLine(
  28. `Pits `,
  29. ...Object.entries(this.userStats).map(([stat, frac]) => {
  30. if (frac !== undefined) {
  31. return new LogLine(`${(frac * 100).toFixed(0)}% `, new PropElem(stat as Stat))
  32. } else {
  33. return nilLog
  34. }
  35. }),
  36. ` against `,
  37. ...Object.entries(this.targetStats).map(([stat, frac]) => {
  38. if (frac !== undefined) {
  39. return new LogLine(`${(frac * 100).toFixed(0)}% `, new PropElem(stat as Stat))
  40. } else {
  41. return nilLog
  42. }
  43. })
  44. ),
  45. new LogLine(
  46. `Score delta: ${(this.computeScore(user, this.userStats) - this.computeScore(target, this.targetStats)).toFixed(1)}`
  47. )
  48. )
  49. }
  50. userScore (attacker: Creature, score: number): number {
  51. return score + this.computeScore(attacker, this.userStats)
  52. }
  53. targetScore (defender: Creature, score: number): number {
  54. return score + this.computeScore(defender, this.targetStats)
  55. }
  56. private computeScore (subject: Creature, parts: Partial<Stats & VoreStats>): number {
  57. const total = Object.entries(parts).reduce((total: number, [stat, frac]) => {
  58. if (stat in Stat) {
  59. let value = subject.stats[stat as Stat] * (frac === undefined ? 0 : frac)
  60. const vigor = StatToVigor[stat as Stat]
  61. value = value * (1 - this.maxStatVigorPenalty) + value * this.maxStatVigorPenalty * subject.vigors[vigor] / subject.maxVigors[vigor]
  62. return total + value
  63. } else if (stat in VoreStat) {
  64. const value = subject.voreStats[stat as VoreStat] * (frac === undefined ? 0 : frac)
  65. return total + value
  66. } else {
  67. return total
  68. }
  69. }, 0)
  70. const modifiedTotal = Object.keys(Vigor).reduce(
  71. (total, vigor) => {
  72. const base = total * (1 - this.maxTotalVigorPenalty)
  73. const modified = total * this.maxTotalVigorPenalty * subject.vigors[vigor as Vigor] / subject.maxVigors[vigor as Vigor]
  74. return base + modified
  75. },
  76. total
  77. )
  78. return modifiedTotal
  79. }
  80. }
  81. // TODO this will need to be able to return a LogEntry at some point
  82. abstract class RandomTest implements CombatTest {
  83. constructor (public fail: (user: Creature, target: Creature) => LogEntry) {
  84. }
  85. test (user: Creature, target: Creature): boolean {
  86. const userFail = user.effects.map(effect => effect.failTest(user, target))
  87. if (userFail.some(result => result.failed)) {
  88. return false
  89. }
  90. const targetFail = target.effects.map(effect => effect.failTest(target, user))
  91. if (targetFail.some(result => result.failed)) {
  92. return true
  93. }
  94. return Math.random() < this.odds(user, target)
  95. }
  96. abstract odds(user: Creature, target: Creature): number
  97. abstract explain(user: Creature, target: Creature): LogEntry
  98. }
  99. export enum TestCategory {
  100. Attack = "Attack",
  101. Vore = "Vore"
  102. }
  103. export class CompositionTest extends RandomTest {
  104. private f: (x: number) => number
  105. private k = 0.1
  106. constructor (
  107. private scorers: Scorer[],
  108. fail: (user: Creature, target: Creature) => LogEntry,
  109. public category: TestCategory,
  110. private bias = 0
  111. ) {
  112. super(fail)
  113. this.f = logistic(0, 1, this.k)
  114. }
  115. explain (user: Creature, target: Creature): LogEntry {
  116. return new LogLines(
  117. ...this.scorers.map(scorer => scorer.explain(user, target))
  118. )
  119. }
  120. odds (user: Creature, target: Creature): number {
  121. const userScore = this.scorers.reduce((score, scorer) => scorer.userScore(user, score), 0)
  122. const targetScore = this.scorers.reduce((score, scorer) => scorer.targetScore(target, score), 0)
  123. const userMod = user.effects.reduce((score, effect) => score + effect.modTestOffense(user, target, this.category), 0)
  124. const targetMod = target.effects.reduce((score, effect) => score + effect.modTestDefense(target, user, this.category), 0)
  125. return this.f(userScore - targetScore + this.bias)
  126. }
  127. }
  128. export class OpposedStatTest extends RandomTest {
  129. private f: (x: number) => number
  130. private k = 0.1
  131. // how much a stat can be reduced by its corresponding vigor being low
  132. private maxStatVigorPenalty = 0.5
  133. // how much the total score can be reduced by each vigor being low
  134. private maxTotalVigorPenalty = 0.1
  135. constructor (
  136. public readonly userStats: Partial<Stats & VoreStats>,
  137. public readonly targetStats: Partial<Stats & VoreStats>,
  138. fail: (user: Creature, target: Creature) => LogEntry,
  139. public category: TestCategory,
  140. private bias = 0
  141. ) {
  142. super(fail)
  143. this.f = logistic(0, 1, this.k)
  144. }
  145. odds (user: Creature, target: Creature): number {
  146. const userScore = this.getScoreOffense(user, target, this.userStats)
  147. const targetScore = this.getScoreDefense(target, user, this.targetStats)
  148. return this.f(userScore - targetScore + this.bias)
  149. }
  150. explain (user: Creature, target: Creature): LogEntry {
  151. return new LogLines(
  152. new LogLine(
  153. `Pits `,
  154. ...Object.entries(this.userStats).map(([stat, frac]) => {
  155. if (frac !== undefined) {
  156. return new LogLine(`${(frac * 100).toFixed(0)}% of `, new PropElem(stat as Stat), `, `)
  157. } else {
  158. return nilLog
  159. }
  160. }),
  161. ` against `,
  162. ...Object.entries(this.targetStats).map(([stat, frac]) => {
  163. if (frac !== undefined) {
  164. return new LogLine(`${(frac * 100).toFixed(0)}% of `, new PropElem(stat as Stat), `, `)
  165. } else {
  166. return nilLog
  167. }
  168. })
  169. ),
  170. new LogLine(
  171. `${user.name.capital}: ${this.getScoreOffense(user, target, this.userStats)} // ${this.getScoreDefense(target, user, this.targetStats)} :${target.name.capital}`
  172. ),
  173. new LogLine(
  174. `${user.name.capital} ${user.name.conjugate(new Verb("have", "has"))} a ${(this.odds(user, target) * 100).toFixed(0)}% chance of winning this test.`
  175. )
  176. )
  177. }
  178. private getScoreDefense (defender: Creature, attacker: Creature, parts: Partial<Stats>): number {
  179. return this.getScore(defender, parts) + defender.effects.reduce((total, effect) => total + effect.modTestDefense(defender, attacker, this.category), 0)
  180. }
  181. private getScoreOffense (attacker: Creature, defender: Creature, parts: Partial<Stats>): number {
  182. return this.getScore(attacker, parts) + attacker.effects.reduce((total, effect) => total + effect.modTestOffense(attacker, defender, this.category), 0)
  183. }
  184. private getScore (actor: Creature, parts: Partial<Stats>): number {
  185. const total = Object.entries(parts).reduce((total: number, [stat, frac]) => {
  186. if (stat in Stat) {
  187. let value = actor.stats[stat as Stat] * (frac === undefined ? 0 : frac)
  188. const vigor = StatToVigor[stat as Stat]
  189. value = value * (1 - this.maxStatVigorPenalty) + value * this.maxStatVigorPenalty * actor.vigors[vigor] / actor.maxVigors[vigor]
  190. return total + value
  191. } else if (stat in VoreStat) {
  192. const value = actor.voreStats[stat as VoreStat] * (frac === undefined ? 0 : frac)
  193. return total + value
  194. } else {
  195. return total
  196. }
  197. }, 0)
  198. const modifiedTotal = Object.keys(Vigor).reduce(
  199. (total, vigor) => {
  200. return total * (1 - this.maxStatVigorPenalty) + total * this.maxStatVigorPenalty * actor.vigors[vigor as Vigor] / actor.maxVigors[vigor as Vigor]
  201. },
  202. total
  203. )
  204. return modifiedTotal
  205. }
  206. }
  207. export class StatVigorSizeTest extends RandomTest {
  208. private f: (x: number) => number
  209. private k = 0.1
  210. constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) {
  211. super(fail)
  212. this.f = logistic(0, 1, this.k)
  213. }
  214. odds (user: Creature, target: Creature): number {
  215. let userPercent = 1
  216. let targetPercent = 1
  217. Object.keys(Vigor).forEach(key => {
  218. userPercent *= user.vigors[key as Vigor] / Math.max(1, user.maxVigors[key as Vigor])
  219. targetPercent *= target.vigors[key as Vigor] / Math.max(1, target.maxVigors[key as Vigor])
  220. userPercent = Math.max(0, userPercent)
  221. targetPercent = Math.max(0, targetPercent)
  222. })
  223. if (userPercent === 0) {
  224. targetPercent *= 4
  225. }
  226. if (targetPercent === 0) {
  227. userPercent *= 4
  228. }
  229. const sizeOffset = Math.log2(user.voreStats.Mass / target.voreStats.Mass)
  230. return this.f(this.bias + sizeOffset * 5 + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)
  231. }
  232. explain (user: Creature, target: Creature): LogEntry {
  233. let result: LogEntry
  234. let userPercent = 1
  235. let targetPercent = 1
  236. Object.keys(Vigor).forEach(key => {
  237. userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor]
  238. targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor]
  239. userPercent = Math.max(0, userPercent)
  240. targetPercent = Math.max(0, targetPercent)
  241. })
  242. if (userPercent === 0) {
  243. targetPercent *= 4
  244. }
  245. if (targetPercent === 0) {
  246. userPercent *= 4
  247. }
  248. const sizeOffset = Math.log2(user.voreStats.Mass / target.voreStats.Mass)
  249. const userMod = user.stats[this.stat] * userPercent
  250. const targetMod = target.stats[this.stat] * targetPercent
  251. const delta = userMod - targetMod + sizeOffset * 5
  252. if (delta === 0) {
  253. result = new LogLine('You and the target have the same effective', new PropElem(this.stat), '.')
  254. } else if (delta < 0) {
  255. result = new LogLine('You effectively have ', new PropElem(this.stat, -delta), ' less than your foe.')
  256. } else {
  257. result = new LogLine('You effectively have ', new PropElem(this.stat, delta), ' more than you foe.')
  258. }
  259. result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%')
  260. return result
  261. }
  262. }
  263. export class StatVigorTest extends RandomTest {
  264. private f: (x: number) => number
  265. private k = 0.1
  266. constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) {
  267. super(fail)
  268. this.f = logistic(0, 1, this.k)
  269. }
  270. odds (user: Creature, target: Creature): number {
  271. let userPercent = 1
  272. let targetPercent = 1
  273. Object.keys(Vigor).forEach(key => {
  274. userPercent *= user.vigors[key as Vigor] / Math.max(1, user.maxVigors[key as Vigor])
  275. targetPercent *= target.vigors[key as Vigor] / Math.max(1, target.maxVigors[key as Vigor])
  276. userPercent = Math.max(0, userPercent)
  277. targetPercent = Math.max(0, targetPercent)
  278. })
  279. if (userPercent === 0) {
  280. targetPercent *= 4
  281. }
  282. if (targetPercent === 0) {
  283. userPercent *= 4
  284. }
  285. return this.f(this.bias + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)
  286. }
  287. explain (user: Creature, target: Creature): LogEntry {
  288. let result: LogEntry
  289. let userPercent = 1
  290. let targetPercent = 1
  291. Object.keys(Vigor).forEach(key => {
  292. userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor]
  293. targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor]
  294. userPercent = Math.max(0, userPercent)
  295. targetPercent = Math.max(0, targetPercent)
  296. })
  297. if (userPercent === 0) {
  298. targetPercent *= 4
  299. }
  300. if (targetPercent === 0) {
  301. userPercent *= 4
  302. }
  303. const userMod = user.stats[this.stat] * userPercent
  304. const targetMod = target.stats[this.stat] * targetPercent
  305. const delta = userMod - targetMod
  306. if (delta === 0) {
  307. result = new LogLine('You and the target have the same effective', new PropElem(this.stat), '.')
  308. } else if (delta < 0) {
  309. result = new LogLine('You effectively have ', new PropElem(this.stat, -delta), ' less than your foe.')
  310. } else {
  311. result = new LogLine('You effectively have ', new PropElem(this.stat, delta), ' more than you foe.')
  312. }
  313. result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%')
  314. return result
  315. }
  316. }
  317. export class StatTest extends RandomTest {
  318. private f: (x: number) => number
  319. private k = 0.1
  320. constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) {
  321. super(fail)
  322. this.f = logistic(0, 1, this.k)
  323. }
  324. odds (user: Creature, target: Creature): number {
  325. return this.f(this.bias + user.stats[this.stat] - target.stats[this.stat])
  326. }
  327. explain (user: Creature, target: Creature): LogEntry {
  328. const delta: number = user.stats[this.stat] - target.stats[this.stat]
  329. let result: LogEntry
  330. if (delta === 0) {
  331. result = new LogLine('You and the target have the same ', new PropElem(this.stat), '.')
  332. } else if (delta < 0) {
  333. result = new LogLine('You have ', new PropElem(this.stat, -delta), ' less than your foe.')
  334. } else {
  335. result = new LogLine('You have ', new PropElem(this.stat, delta), ' more than you foe.')
  336. }
  337. result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%')
  338. return result
  339. }
  340. }
  341. export class ChanceTest extends RandomTest {
  342. constructor (public readonly chance: number, fail: (user: Creature, target: Creature) => LogEntry) {
  343. super(fail)
  344. }
  345. odds (user: Creature, target: Creature): number {
  346. return this.chance
  347. }
  348. explain (user: Creature, target: Creature): LogEntry {
  349. return new LogLine('You have a flat ' + (100 * this.chance) + '% chance.')
  350. }
  351. }