mirror of
https://github.com/wesnoth/wesnoth
synced 2025-05-03 23:29:59 +00:00
1055 lines
29 KiB
C++
1055 lines
29 KiB
C++
/* $Id$ */
|
|
/*
|
|
Copyright (C) 2006 - 2008 by Rusty Russell <rusty@rustcorp.com.au>
|
|
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License version 2
|
|
or at your option any later version.
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY.
|
|
|
|
See the COPYING file for more details.
|
|
|
|
Full algorithm by Yogin. Typing and optimization by Rusty.
|
|
|
|
This code has lots of debugging. It is there for a reason:
|
|
this code is kinda tricky. Do not remove it.
|
|
*/
|
|
|
|
/**
|
|
* @file attack_prediction.cpp
|
|
* Simulate combat to calculate attacks. Standalone program, benchmark.
|
|
*/
|
|
|
|
#include "attack_prediction.hpp"
|
|
|
|
#include <cassert>
|
|
#include <cstring> // For memset
|
|
#include <vector>
|
|
|
|
|
|
// Compile with -O3 -DBENCHMARK for speed testing,
|
|
// -DCHECK for testing correctness
|
|
// (run tools/wesnoth-attack-sim.c --check on output)
|
|
#if !defined(BENCHMARK) && !defined(CHECK)
|
|
#include "util.hpp"
|
|
#else
|
|
#include <time.h>
|
|
#include <sys/time.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#define maximum(a,b) ((a) > (b) ? (a) : (b))
|
|
#define minimum(a,b) ((a) < (b) ? (a) : (b))
|
|
#endif
|
|
|
|
#ifdef ATTACK_PREDICTION_DEBUG
|
|
#define debug(x) printf x
|
|
#else
|
|
#define debug(x)
|
|
#endif
|
|
|
|
namespace
|
|
{
|
|
/** A matrix of A's hitpoints vs B's hitpoints. */
|
|
struct prob_matrix
|
|
{
|
|
// Simple matrix, both known HP.
|
|
prob_matrix(unsigned int a_max_hp, unsigned int b_max_hp,
|
|
bool a_slows, bool b_slows,
|
|
unsigned int a_hp, unsigned int b_hp,
|
|
const std::vector<double> a_summary[2],
|
|
const std::vector<double> b_summary[2]);
|
|
|
|
~prob_matrix();
|
|
|
|
// A hits B.
|
|
void receive_blow_b(unsigned damage, unsigned slow_damage, double hit_chance,
|
|
bool a_slows, bool a_drains);
|
|
|
|
// B hits A. Why can't they just get along?
|
|
void receive_blow_a(unsigned damage, unsigned slow_damage, double hit_chance,
|
|
bool b_slows, bool b_drains);
|
|
|
|
// We lied: actually did less damage, adjust matrix.
|
|
void remove_stone_distortion_a(unsigned damage, unsigned slow_damage, unsigned b_hp);
|
|
void remove_stone_distortion_b(unsigned damage, unsigned slow_damage, unsigned a_hp);
|
|
|
|
// Its over, and here's the bill.
|
|
void extract_results(std::vector<double> summary_a[2],
|
|
std::vector<double> summary_b[2]);
|
|
|
|
// What's the chance one is dead?
|
|
double dead_prob() const;
|
|
|
|
void dump() const;
|
|
|
|
// We need four matrices, or "planes", reflecting the possible
|
|
// "slowed" states (neither slowed, A slowed, B slowed, both slowed).
|
|
enum {
|
|
NEITHER_SLOWED,
|
|
A_SLOWED,
|
|
B_SLOWED,
|
|
BOTH_SLOWED
|
|
};
|
|
|
|
private:
|
|
// This gives me 10% speed improvement over std::vector<> (g++4.0.3 x86)
|
|
double *new_arr(unsigned int size);
|
|
|
|
double &val(unsigned plane, unsigned row, unsigned col);
|
|
const double &val(unsigned plane, unsigned row, unsigned col) const;
|
|
|
|
// Move this much from src to dst. Returns true if anything transferred.
|
|
void xfer(unsigned dst_plane, unsigned src_plane,
|
|
unsigned row_dst, unsigned col_dst,
|
|
unsigned row_src, unsigned col_src,
|
|
double prob);
|
|
|
|
// Shift columns on this plane (b taking damage). Returns min col.
|
|
void shift_cols(unsigned dst, unsigned src,
|
|
unsigned damage, double prob, bool drain);
|
|
|
|
// Shift rows on this plane (a taking damage). Returns new min row.
|
|
void shift_rows(unsigned dst, unsigned src,
|
|
unsigned damage, double prob, bool drain);
|
|
|
|
/** @todo FIXME: rename using _ at end. */
|
|
unsigned int rows_, cols_;
|
|
double *plane_[4];
|
|
|
|
// For optimization, we keep track of the lower row/col we need to consider
|
|
unsigned int min_row_[4], min_col_[4];
|
|
};
|
|
|
|
prob_matrix::prob_matrix(unsigned int a_max_hp, unsigned int b_max_hp,
|
|
bool a_slows, bool b_slows,
|
|
unsigned int a_hp, unsigned int b_hp,
|
|
const std::vector<double> a_summary[2],
|
|
const std::vector<double> b_summary[2])
|
|
: rows_(a_max_hp+1), cols_(b_max_hp+1)
|
|
{
|
|
if (!a_summary[0].empty()) {
|
|
// A has fought before. Do we need a slow plane for it?
|
|
if (!a_summary[1].empty())
|
|
b_slows = true;
|
|
// Don't handle both being reused.
|
|
assert(b_summary[0].empty());
|
|
}
|
|
if (!b_summary[0].empty()) {
|
|
// B has fought before. Do we need a slow plane for it?
|
|
if (!b_summary[1].empty())
|
|
a_slows = true;
|
|
}
|
|
|
|
plane_[NEITHER_SLOWED] = new_arr(rows_*cols_);
|
|
if (b_slows)
|
|
plane_[A_SLOWED] = new_arr(rows_*cols_);
|
|
else
|
|
plane_[A_SLOWED] = NULL;
|
|
if (a_slows)
|
|
plane_[B_SLOWED] = new_arr(rows_*cols_);
|
|
else
|
|
plane_[B_SLOWED] = NULL;
|
|
if (a_slows && b_slows)
|
|
plane_[BOTH_SLOWED] = new_arr(rows_*cols_);
|
|
else
|
|
plane_[BOTH_SLOWED] = NULL;
|
|
|
|
min_row_[NEITHER_SLOWED] = a_hp - 1;
|
|
min_col_[NEITHER_SLOWED] = b_hp - 1;
|
|
min_row_[A_SLOWED] = min_row_[B_SLOWED] = min_row_[BOTH_SLOWED] = rows_;
|
|
min_col_[A_SLOWED] = min_col_[B_SLOWED] = min_col_[BOTH_SLOWED] = cols_;
|
|
|
|
// Transfer HP distribution from A?
|
|
if (!a_summary[0].empty()) {
|
|
// @todo FIXME: Can optimize here.
|
|
min_row_[NEITHER_SLOWED] = 0;
|
|
min_row_[A_SLOWED] = 0;
|
|
min_col_[A_SLOWED] = b_hp - 1;
|
|
for (unsigned int row = 0; row < a_summary[0].size(); row++)
|
|
val(NEITHER_SLOWED, row, b_hp) = a_summary[0][row];
|
|
if (!a_summary[1].empty()) {
|
|
for (unsigned int row = 0; row < a_summary[1].size(); row++)
|
|
val(A_SLOWED, row, b_hp) = a_summary[1][row];
|
|
}
|
|
debug(("A has fought before\n"));
|
|
dump();
|
|
} else if (!b_summary[0].empty()) {
|
|
min_col_[NEITHER_SLOWED] = 0;
|
|
min_col_[B_SLOWED] = 0;
|
|
min_row_[B_SLOWED] = a_hp - 1;
|
|
for (unsigned int col = 0; col < b_summary[0].size(); col++)
|
|
val(NEITHER_SLOWED, a_hp, col) = b_summary[0][col];
|
|
if (!b_summary[1].empty()) {
|
|
for (unsigned int col = 0; col < b_summary[1].size(); col++)
|
|
val(B_SLOWED, a_hp, col) = b_summary[1][col];
|
|
}
|
|
debug(("B has fought before\n"));
|
|
dump();
|
|
} else {
|
|
// If a unit has drain it might end with more HP than before.
|
|
// Make sure we don't access the matrix in invalid positions.
|
|
a_hp = minimum<unsigned int>(a_hp, rows_ - 1);
|
|
b_hp = minimum<unsigned int>(b_hp, cols_ - 1);
|
|
val(NEITHER_SLOWED, a_hp, b_hp) = 1.0;
|
|
}
|
|
}
|
|
|
|
prob_matrix::~prob_matrix()
|
|
{
|
|
delete[] plane_[NEITHER_SLOWED];
|
|
delete[] plane_[A_SLOWED];
|
|
delete[] plane_[B_SLOWED];
|
|
delete[] plane_[BOTH_SLOWED];
|
|
}
|
|
|
|
// Allocate a new probability array, initialized to 0.
|
|
double *prob_matrix::new_arr(unsigned int size)
|
|
{
|
|
double *arr = new double[size];
|
|
memset(arr, 0, sizeof(double) * size);
|
|
return arr;
|
|
}
|
|
|
|
double &prob_matrix::val(unsigned p, unsigned row, unsigned col)
|
|
{
|
|
assert(row < rows_);
|
|
assert(col < cols_);
|
|
return plane_[p][row * cols_ + col];
|
|
}
|
|
|
|
const double &prob_matrix::val(unsigned p, unsigned row, unsigned col) const
|
|
{
|
|
assert(row < rows_);
|
|
assert(col < cols_);
|
|
return plane_[p][row * cols_ + col];
|
|
}
|
|
|
|
#ifdef CHECK
|
|
void prob_matrix::dump() const
|
|
{
|
|
unsigned int row, col, m;
|
|
const char *names[]
|
|
= { "NEITHER_SLOWED", "A_SLOWED", "B_SLOWED", "BOTH_SLOWED" };
|
|
|
|
for (m = 0; m < 4; m++) {
|
|
if (!plane_[m])
|
|
continue;
|
|
debug(("%s:\n", names[m]));
|
|
for (row = 0; row < rows_; row++) {
|
|
debug((" "));
|
|
for (col = 0; col < cols_; col++)
|
|
debug(("%4.3g ", val(m, row, col)*100));
|
|
debug(("\n"));
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
void prob_matrix::dump() const
|
|
{
|
|
}
|
|
#endif
|
|
|
|
// xfer, shift_cols and shift_rows use up most of our time. Careful!
|
|
void prob_matrix::xfer(unsigned dst_plane, unsigned src_plane,
|
|
unsigned row_dst, unsigned col_dst,
|
|
unsigned row_src, unsigned col_src,
|
|
double prob)
|
|
{
|
|
double &src = val(src_plane, row_src, col_src);
|
|
if (src != 0.0) {
|
|
double diff = src * prob;
|
|
src -= diff;
|
|
|
|
// This is here for drain.
|
|
if (col_dst >= cols_)
|
|
col_dst = cols_ - 1;
|
|
if (row_dst >= rows_)
|
|
row_dst = rows_ - 1;
|
|
|
|
val(dst_plane, row_dst, col_dst) += diff;
|
|
|
|
debug(("Shifted %4.3g from %s(%u,%u) to %s(%u,%u)\n",
|
|
diff, src_plane == NEITHER_SLOWED ? ""
|
|
: src_plane == A_SLOWED ? "[A_SLOWED]"
|
|
: src_plane == B_SLOWED ? "[B_SLOWED]"
|
|
: src_plane == BOTH_SLOWED ? "[BOTH_SLOWED]" : "INVALID",
|
|
row_src, col_src,
|
|
dst_plane == NEITHER_SLOWED ? ""
|
|
: dst_plane == A_SLOWED ? "[A_SLOWED]"
|
|
: dst_plane == B_SLOWED ? "[B_SLOWED]"
|
|
: dst_plane == BOTH_SLOWED ? "[BOTH_SLOWED]" : "INVALID",
|
|
row_dst, col_dst));
|
|
}
|
|
}
|
|
|
|
void prob_matrix::shift_cols(unsigned dst, unsigned src,
|
|
unsigned damage, double prob, bool drain)
|
|
{
|
|
unsigned int row, col;
|
|
unsigned int shift = drain ? 1 : 31; // Avoids a branch.
|
|
|
|
if (damage >= cols_)
|
|
damage = cols_ - 1;
|
|
|
|
// Loop backwards so we write drain behind us, for when src == dst.
|
|
for (row = rows_ - 1; row > min_row_[src]; row--) {
|
|
// These are all going to die (move to col 0).
|
|
for (col = 1; col <= damage; col++)
|
|
xfer(dst, src, row+(col>>shift), 0, row, col, prob);
|
|
for (col = damage+1; col < cols_; col++)
|
|
xfer(dst, src, row+(damage>>shift), col - damage, row, col, prob);
|
|
}
|
|
}
|
|
|
|
void prob_matrix::shift_rows(unsigned dst, unsigned src,
|
|
unsigned damage, double prob, bool drain)
|
|
{
|
|
unsigned int row, col;
|
|
unsigned int shift = drain ? 1 : 31; // Avoids a branch.
|
|
|
|
if (damage >= rows_)
|
|
damage = rows_ - 1;
|
|
|
|
// Loop downwards so if we drain, we write behind us.
|
|
for (col = cols_ - 1; col > min_col_[src]; col--) {
|
|
// These are all going to die (move to row 0).
|
|
for (row = 1; row <= damage; row++)
|
|
xfer(dst, src, 0, col+(row>>shift), row, col, prob);
|
|
for (row = damage+1; row < rows_; row++)
|
|
xfer(dst, src, row - damage, col+(damage>>shift), row, col, prob);
|
|
}
|
|
}
|
|
|
|
// Shift prob_matrix to reflect probability 'hit_chance'
|
|
// that damage (up to) 'damage' is done to 'b'.
|
|
void prob_matrix::receive_blow_b(unsigned damage, unsigned slow_damage, double hit_chance,
|
|
bool a_slows, bool a_drains)
|
|
{
|
|
int src, dst;
|
|
|
|
// Walk backwards so we don't copy already-copied matrix planes.
|
|
for (src = 3; src >=0; src--) {
|
|
unsigned int actual_damage;
|
|
|
|
if (!plane_[src])
|
|
continue;
|
|
|
|
// If A slows us, we go from 0=>2, 1=>3, 2=>2 3=>3.
|
|
if (a_slows)
|
|
dst = (src|2);
|
|
else
|
|
dst = src;
|
|
|
|
// A is slow in planes 1 and 3.
|
|
if (src & 1)
|
|
actual_damage = slow_damage;
|
|
else
|
|
actual_damage = damage;
|
|
|
|
shift_cols(dst, src, actual_damage, hit_chance, a_drains);
|
|
if (min_col_[src] < damage)
|
|
min_col_[dst] = 0;
|
|
else if (min_col_[src] - damage < min_col_[dst])
|
|
min_col_[dst] = min_col_[src] - damage;
|
|
if (min_row_[src] < min_row_[dst])
|
|
min_row_[dst] = min_row_[src];
|
|
}
|
|
}
|
|
|
|
// We lied: actually did less damage, adjust matrix.
|
|
void prob_matrix::remove_stone_distortion_a(unsigned damage, unsigned slow_damage,
|
|
unsigned b_hp)
|
|
{
|
|
for (int p = 0; p < 4; p++) {
|
|
if (!plane_[p])
|
|
continue;
|
|
|
|
// A is slow in planes 1 and 3.
|
|
if (p & 1) {
|
|
if (b_hp > slow_damage)
|
|
for (unsigned int row = 0; row < rows_; row++)
|
|
xfer(p, p, row, b_hp - slow_damage, row, 0, 1.0);
|
|
} else {
|
|
if (b_hp > damage)
|
|
for (unsigned int row = 0; row < rows_; row++)
|
|
xfer(p, p, row, b_hp - damage, row, 0, 1.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void prob_matrix::remove_stone_distortion_b(unsigned damage, unsigned slow_damage,
|
|
unsigned a_hp)
|
|
{
|
|
for (int p = 0; p < 4; p++) {
|
|
if (!plane_[p])
|
|
continue;
|
|
|
|
// B is slow in planes 2 and 3.
|
|
if (p & 2) {
|
|
if (a_hp > slow_damage)
|
|
for (unsigned int col = 0; col < cols_; col++)
|
|
xfer(p, p, a_hp - slow_damage, col, 0, col, 1.0);
|
|
} else {
|
|
if (a_hp > damage)
|
|
for (unsigned int col = 0; col < cols_; col++)
|
|
xfer(p, p, a_hp - damage, col, 0, col, 1.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void prob_matrix::extract_results(std::vector<double> summary_a[2],
|
|
std::vector<double> summary_b[2])
|
|
{
|
|
unsigned int p, row, col;
|
|
|
|
summary_a[0] = std::vector<double>(rows_);
|
|
summary_b[0] = std::vector<double>(cols_);
|
|
|
|
if (plane_[A_SLOWED])
|
|
summary_a[1] = std::vector<double>(rows_);
|
|
if (plane_[B_SLOWED])
|
|
summary_b[1] = std::vector<double>(cols_);
|
|
|
|
for (p = 0; p < 4; p++) {
|
|
int dst_a, dst_b;
|
|
if (!plane_[p])
|
|
continue;
|
|
|
|
// A is slow in planes 1 and 3.
|
|
dst_a = (p & 1);
|
|
// B is slow in planes 2 and 3.
|
|
dst_b = !!(p & 2);
|
|
for (row = 0; row < rows_; row++) {
|
|
for (col = 0; col < cols_; col++) {
|
|
summary_a[dst_a][row] += val(p, row, col);
|
|
summary_b[dst_b][col] += val(p, row, col);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// What's the chance one is dead?
|
|
double prob_matrix::dead_prob() const
|
|
{
|
|
unsigned int p, row, col;
|
|
double prob = 0.0;
|
|
|
|
for (p = 0; p < 4; p++) {
|
|
if (!plane_[p])
|
|
continue;
|
|
// We might count 0,0 twice, but that is always 0 anyway.
|
|
for (row = min_row_[p]; row < rows_; row++)
|
|
prob += val(p, row, 0);
|
|
for (col = min_col_[p]; col < cols_; col++)
|
|
prob += val(p, 0, col);
|
|
}
|
|
return prob;
|
|
}
|
|
|
|
// Shift matrix to reflect probability 'hit_chance'
|
|
// that damage (up to) 'damage' is done to 'a'.
|
|
void prob_matrix::receive_blow_a(unsigned damage, unsigned slow_damage, double hit_chance,
|
|
bool b_slows, bool b_drains)
|
|
{
|
|
int src, dst;
|
|
|
|
// Walk backwards so we don't copy already-copied matrix planes.
|
|
for (src = 3; src >=0; src--) {
|
|
unsigned actual_damage;
|
|
|
|
if (!plane_[src])
|
|
continue;
|
|
|
|
// If B slows us, we go from 0=>1, 1=>1, 2=>3 3=>3.
|
|
if (b_slows)
|
|
dst = (src|1);
|
|
else
|
|
dst = src;
|
|
|
|
// B is slow in planes 2 and 3.
|
|
if (src & 2)
|
|
actual_damage = slow_damage;
|
|
else
|
|
actual_damage = damage;
|
|
|
|
shift_rows(dst, src, actual_damage, hit_chance, b_drains);
|
|
if (min_row_[src] < damage)
|
|
min_row_[dst] = 0;
|
|
else if (min_row_[src] - damage < min_row_[dst])
|
|
min_row_[dst] = min_row_[src] - damage;
|
|
if (min_col_[src] < min_col_[dst])
|
|
min_col_[dst] = min_col_[src];
|
|
}
|
|
}
|
|
|
|
} // end anon namespace
|
|
|
|
unsigned combatant::hp_dist_size(const battle_context::unit_stats &u, const combatant *prev)
|
|
{
|
|
// Our summary must be as big as previous one.
|
|
if (prev) {
|
|
return prev->hp_dist.size();
|
|
}
|
|
|
|
// If this unit drains, HP can increase, so alloc full array.
|
|
if (u.drains) {
|
|
return u.max_hp + 1;
|
|
}
|
|
return u.hp+1;
|
|
}
|
|
|
|
combatant::combatant(const battle_context::unit_stats &u, const combatant *prev)
|
|
: hp_dist(hp_dist_size(u, prev)),
|
|
u_(u),
|
|
hit_chances_(u.num_blows, u.chance_to_hit / 100.0)
|
|
{
|
|
// We inherit current state from previous combatant.
|
|
if (prev) {
|
|
summary[0] = prev->summary[0];
|
|
summary[1] = prev->summary[1];
|
|
poisoned = prev->poisoned;
|
|
untouched = prev->untouched;
|
|
slowed = prev->slowed;
|
|
} else {
|
|
untouched = 1.0;
|
|
poisoned = u.is_poisoned ? 1.0 : 0.0;
|
|
slowed = u.is_slowed ? 1.0 : 0.0;
|
|
}
|
|
}
|
|
|
|
// Copy constructor (except use this copy of unit_stats)
|
|
combatant::combatant(const combatant &that, const battle_context::unit_stats &u)
|
|
: hp_dist(that.hp_dist), untouched(that.untouched), poisoned(that.poisoned), slowed(that.slowed), u_(u), hit_chances_(that.hit_chances_)
|
|
{
|
|
summary[0] = that.summary[0];
|
|
summary[1] = that.summary[1];
|
|
}
|
|
|
|
|
|
|
|
// For swarm, whether we get an attack depends on HP distribution
|
|
// from previous combat. So we roll this into our P(hitting),
|
|
// since no attack is equivalent to missing.
|
|
void combatant::adjust_hitchance()
|
|
{
|
|
if (summary[0].empty() || u_.swarm_min == u_.swarm_max)
|
|
return;
|
|
|
|
hit_chances_ = std::vector<double>(u_.swarm_max);
|
|
double alive_prob;
|
|
|
|
if (summary[1].empty())
|
|
alive_prob = 1 - summary[0][0];
|
|
else
|
|
alive_prob = 1 - summary[0][0] - summary[1][0];
|
|
|
|
unsigned int i;
|
|
for (i = 1; i <= u_.max_hp; i++) {
|
|
double prob = 0.0;
|
|
if(i < summary[0].size()) {
|
|
prob = summary[0][i];
|
|
}
|
|
if (!summary[1].empty())
|
|
prob += summary[1][i];
|
|
for (unsigned int j = 0; j < u_.swarm_min + (u_.swarm_max -
|
|
static_cast<double>(u_.swarm_min)) * u_.hp / u_.max_hp; j++)
|
|
|
|
hit_chances_[j] += prob * u_.chance_to_hit / 100.0 / alive_prob;
|
|
}
|
|
|
|
debug(("\nhit_chances_ (base %u%%):", u_.chance_to_hit));
|
|
for (i = 0; i < u_.swarm_max; i++)
|
|
debug((" %.2f", hit_chances_[i] * 100.0 + 0.5));
|
|
debug(("\n"));
|
|
}
|
|
|
|
// Minimum HP we could possibly have.
|
|
unsigned combatant::min_hp() const
|
|
{
|
|
if (summary[0].empty())
|
|
return u_.hp;
|
|
|
|
// We don't handle this (yet).
|
|
assert(summary[1].empty());
|
|
|
|
unsigned int i;
|
|
for (i = 0; summary[0][i] == 0; i++) {};
|
|
return i;
|
|
}
|
|
|
|
// Combat without chance of death, berserk, slow or drain is simple.
|
|
void combatant::no_death_fight(combatant &opp)
|
|
{
|
|
if (summary[0].empty()) {
|
|
// Starts with a known HP, so Pascal's triangle.
|
|
summary[0] = std::vector<double>(u_.hp+1);
|
|
summary[0][u_.hp] = 1.0;
|
|
for (unsigned int i = 0; i < opp.hit_chances_.size(); i++) {
|
|
for (int j = i; j >= 0; j--) {
|
|
double move = summary[0][u_.hp - j * opp.u_.damage] * opp.hit_chances_[i];
|
|
summary[0][u_.hp - j * opp.u_.damage] -= move;
|
|
summary[0][u_.hp - (j+1) * opp.u_.damage] += move;
|
|
}
|
|
}
|
|
} else {
|
|
// HP could be spread anywhere, iterate through whole thing.
|
|
for (unsigned int i = 0; i < opp.hit_chances_.size(); i++) {
|
|
for (unsigned int j = opp.u_.damage; j <= u_.hp; j++) {
|
|
double move = summary[0][j] * opp.hit_chances_[i];
|
|
summary[0][j] -= move;
|
|
summary[0][j - opp.u_.damage] += move;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (opp.summary[0].empty()) {
|
|
// Starts with a known HP, so Pascal's triangle.
|
|
opp.summary[0] = std::vector<double>(opp.u_.hp+1);
|
|
opp.summary[0][opp.u_.hp] = 1.0;
|
|
for (unsigned int i = 0; i < hit_chances_.size(); i++) {
|
|
for (int j = i; j >= 0; j--) {
|
|
double move = opp.summary[0][opp.u_.hp - j * u_.damage] * hit_chances_[i];
|
|
opp.summary[0][opp.u_.hp - j * u_.damage] -= move;
|
|
opp.summary[0][opp.u_.hp - (j+1) * u_.damage] += move;
|
|
}
|
|
}
|
|
} else {
|
|
// HP could be spread anywhere, iterate through whole thing.
|
|
for (unsigned int i = 0; i < hit_chances_.size(); i++) {
|
|
for (unsigned int j = u_.damage; j <= opp.u_.hp; j++) {
|
|
double move = opp.summary[0][j] * hit_chances_[i];
|
|
opp.summary[0][j] -= move;
|
|
opp.summary[0][j - u_.damage] += move;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Combat with <= 1 strike each is simple, too.
|
|
void combatant::one_strike_fight(combatant &opp)
|
|
{
|
|
if (opp.summary[0].empty()) {
|
|
opp.summary[0] = std::vector<double>(opp.u_.hp+1);
|
|
if (hit_chances_.size() == 1) {
|
|
opp.summary[0][opp.u_.hp] = 1.0 - hit_chances_[0];
|
|
opp.summary[0][maximum<int>(opp.u_.hp - u_.damage, 0)] = hit_chances_[0];
|
|
} else {
|
|
assert(hit_chances_.size() == 0);
|
|
opp.summary[0][opp.u_.hp] = 1.0;
|
|
}
|
|
} else {
|
|
if (hit_chances_.size() == 1) {
|
|
for (unsigned int i = 1; i < opp.summary[0].size(); i++) {
|
|
double move = opp.summary[0][i] * hit_chances_[0];
|
|
opp.summary[0][i] -= move;
|
|
opp.summary[0][maximum<int>(i - u_.damage, 0)] += move;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we killed opponent, it won't attack us.
|
|
double opp_alive_prob = 1.0 - opp.summary[0][0];
|
|
if (summary[0].empty()) {
|
|
summary[0] = std::vector<double>(u_.hp+1);
|
|
if (opp.hit_chances_.size() == 1) {
|
|
summary[0][u_.hp] = 1.0 - opp.hit_chances_[0] * opp_alive_prob;
|
|
summary[0][maximum<int>(u_.hp - opp.u_.damage, 0)] = opp.hit_chances_[0] * opp_alive_prob;
|
|
} else {
|
|
assert(opp.hit_chances_.size() == 0);
|
|
summary[0][u_.hp] = 1.0;
|
|
}
|
|
} else {
|
|
if (opp.hit_chances_.size() == 1) {
|
|
for (unsigned int i = 1; i < summary[0].size(); i++) {
|
|
double move = summary[0][i] * opp.hit_chances_[0] * opp_alive_prob;
|
|
summary[0][i] -= move;
|
|
summary[0][maximum<int>(i - opp.u_.damage, 0)] += move;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void combatant::complex_fight(combatant &opp, unsigned int rounds)
|
|
{
|
|
prob_matrix m(hp_dist.size()-1, opp.hp_dist.size()-1,
|
|
u_.slows && !opp.u_.is_slowed, opp.u_.slows && !u_.is_slowed,
|
|
u_.hp, opp.u_.hp, summary, opp.summary);
|
|
|
|
unsigned max_attacks = maximum(hit_chances_.size(), opp.hit_chances_.size());
|
|
|
|
debug(("A gets %u attacks, B %u\n", hit_chances_.size(), opp.hit_chances_.size()));
|
|
|
|
unsigned int a_damage = u_.damage, a_slow_damage = u_.slow_damage;
|
|
unsigned int b_damage = opp.u_.damage, b_slow_damage = opp.u_.slow_damage;
|
|
|
|
// To simulate stoning, we set to amount which kills, and re-adjust after.
|
|
/** @todo FIXME: This doesn't work for rolling calculations, just first battle. */
|
|
if (u_.stones)
|
|
a_damage = a_slow_damage = opp.u_.max_hp;
|
|
if (opp.u_.stones)
|
|
b_damage = b_slow_damage = u_.max_hp;
|
|
|
|
do {
|
|
for (unsigned int i = 0; i < max_attacks; i++) {
|
|
if (i < hit_chances_.size()) {
|
|
debug(("A strikes\n"));
|
|
m.receive_blow_b(a_damage, a_slow_damage, hit_chances_[i],
|
|
u_.slows && !opp.u_.is_slowed, u_.drains);
|
|
m.dump();
|
|
}
|
|
if (i < opp.hit_chances_.size()) {
|
|
debug(("B strikes\n"));
|
|
m.receive_blow_a(b_damage, b_slow_damage, opp.hit_chances_[i],
|
|
opp.u_.slows && !u_.is_slowed, opp.u_.drains);
|
|
m.dump();
|
|
}
|
|
}
|
|
|
|
debug(("Combat ends:\n"));
|
|
m.dump();
|
|
} while (--rounds && m.dead_prob() < 0.99);
|
|
|
|
if (u_.stones)
|
|
m.remove_stone_distortion_a(u_.damage, u_.slow_damage, opp.u_.hp);
|
|
if (opp.u_.stones)
|
|
m.remove_stone_distortion_b(opp.u_.damage, opp.u_.slow_damage, u_.hp);
|
|
|
|
// We extract results separately, then combine.
|
|
m.extract_results(summary, opp.summary);
|
|
}
|
|
|
|
// Two man enter. One man leave!
|
|
// ... Or maybe two. But definitely not three.
|
|
// Of course, one could be a woman. Or both.
|
|
// And neither could be human, too.
|
|
// Um, ok, it was a stupid thing to say.
|
|
void combatant::fight(combatant &opp)
|
|
{
|
|
unsigned int rounds = maximum<unsigned int>(u_.rounds, opp.u_.rounds);
|
|
|
|
// If defender has firststrike and we don't, reverse.
|
|
if (opp.u_.firststrike && !u_.firststrike) {
|
|
opp.fight(*this);
|
|
return;
|
|
}
|
|
|
|
#ifdef ATTACK_PREDICTION_DEBUG
|
|
printf("A:\n");
|
|
u_.dump();
|
|
printf("B:\n");
|
|
opp.u_.dump();
|
|
#endif
|
|
|
|
// If we've fought before and we have swarm, we must adjust cth array.
|
|
adjust_hitchance();
|
|
opp.adjust_hitchance();
|
|
|
|
#if 0
|
|
std::vector<double> prev = summary[0], opp_prev = opp.summary[0];
|
|
complex_fight(opp, 1);
|
|
std::vector<double> res = summary[0], opp_res = opp.summary[0];
|
|
summary[0] = prev;
|
|
opp.summary[0] = opp_prev;
|
|
#endif
|
|
|
|
// Optimize the simple cases.
|
|
if (rounds == 1 && !u_.slows && !opp.u_.slows &&
|
|
!u_.drains && !opp.u_.drains && !u_.stones && !opp.u_.stones &&
|
|
summary[1].empty() && opp.summary[1].empty()) {
|
|
if (hit_chances_.size() <= 1 && opp.hit_chances_.size() <= 1) {
|
|
one_strike_fight(opp);
|
|
} else if (hit_chances_.size() * u_.damage < opp.min_hp() &&
|
|
opp.hit_chances_.size() * opp.u_.damage < min_hp()) {
|
|
no_death_fight(opp);
|
|
} else {
|
|
complex_fight(opp, rounds);
|
|
}
|
|
} else {
|
|
complex_fight(opp, rounds);
|
|
}
|
|
|
|
#if 0
|
|
assert(summary[0].size() == res.size());
|
|
assert(opp.summary[0].size() == opp_res.size());
|
|
for (unsigned int i = 0; i < summary[0].size(); i++) {
|
|
if (fabs(summary[0][i] - res[i]) > 0.000001) {
|
|
std::cerr << "Mismatch for " << i << " hp: " << summary[0][i] << " should have been " << res[i] << "\n";
|
|
assert(0);
|
|
}
|
|
}
|
|
for (unsigned int i = 0; i < opp.summary[0].size(); i++) {
|
|
if (fabs(opp.summary[0][i] - opp_res[i])> 0.000001) {
|
|
std::cerr << "Mismatch for " << i << " hp: " << opp.summary[0][i] << " should have been " << opp_res[i] << "\n";
|
|
assert(0);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Combine summary into distribution.
|
|
if (summary[1].empty())
|
|
hp_dist = summary[0];
|
|
else {
|
|
for (unsigned int i = 0; i < hp_dist.size(); i++)
|
|
hp_dist[i] = summary[0][i] + summary[1][i];
|
|
}
|
|
if (opp.summary[1].empty())
|
|
opp.hp_dist = opp.summary[0];
|
|
else {
|
|
for (unsigned int i = 0; i < opp.hp_dist.size(); i++)
|
|
opp.hp_dist[i] = opp.summary[0][i] + opp.summary[1][i];
|
|
}
|
|
|
|
// Make sure we don't try to access the vectors out of bounds,
|
|
// drain increases HPs so we determine the number of HP here
|
|
// and make sure it stays within bounds
|
|
const unsigned int hp = minimum<unsigned int>(u_.hp, hp_dist.size() - 1);
|
|
const unsigned int opp_hp = minimum<unsigned int>(opp.u_.hp, opp.hp_dist.size() - 1);
|
|
|
|
// Chance that we / they were touched this time.
|
|
double touched = untouched - hp_dist[hp];
|
|
double opp_touched = opp.untouched - opp.hp_dist[opp_hp];
|
|
if (opp.u_.poisons)
|
|
poisoned += (1 - poisoned) * touched;
|
|
if (u_.poisons)
|
|
opp.poisoned += (1 - opp.poisoned) * opp_touched;
|
|
|
|
if (opp.u_.slows)
|
|
slowed += (1 - slowed) * touched;
|
|
if (u_.slows)
|
|
opp.slowed += (1 - opp.slowed) * opp_touched;
|
|
|
|
/** @todo FIXME: This is approximate: we could drain, then get hit. */
|
|
untouched = hp_dist[hp];
|
|
opp.untouched = opp.hp_dist[opp_hp];
|
|
}
|
|
|
|
double combatant::average_hp(unsigned int healing) const
|
|
{
|
|
double total = 0;
|
|
|
|
// Since sum of probabilities is 1.0, we can just tally weights.
|
|
for (unsigned int i = 1; i < hp_dist.size(); i++) {
|
|
total += hp_dist[i] * minimum<unsigned int>(i + healing, u_.max_hp);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
#if defined(BENCHMARK) || defined(CHECK)
|
|
// We create a significant number of nasty-to-calculate units,
|
|
// and test each one against the others.
|
|
#define NUM_UNITS 50
|
|
|
|
// Stolen from glibc headers sys/time.h
|
|
#define timer_sub(a, b, result) \
|
|
do { \
|
|
(result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \
|
|
(result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \
|
|
if ((result)->tv_usec < 0) { \
|
|
--(result)->tv_sec; \
|
|
(result)->tv_usec += 1000000; \
|
|
} \
|
|
} while (0)
|
|
|
|
#ifdef CHECK
|
|
void combatant::print(const char label[], unsigned int battle) const
|
|
{
|
|
printf("#%u: %s: %u %u %u %2g%% ", battle,
|
|
label, damage_, base_num_attacks_, hp_, base_hit_chance_*100.0);
|
|
if (drains_)
|
|
printf("drains,");
|
|
if (slows_)
|
|
printf("slows,");
|
|
if (berserk_)
|
|
printf("berserk,");
|
|
if (swarm_)
|
|
printf("swarm,");
|
|
if (firststrike_)
|
|
printf("firststrike,");
|
|
printf("maxhp=%u ", hp_dist.size()-1);
|
|
printf(" %.2f", untouched);
|
|
for (unsigned int i = 0; i < hp_dist.size(); i++)
|
|
printf(" %.2f", hp_dist[i] * 100);
|
|
printf("\n");
|
|
}
|
|
#else // ... BENCHMARK
|
|
void combatant::print(const char label[], unsigned int battle) const
|
|
{
|
|
}
|
|
#endif
|
|
|
|
static void run(unsigned specific_battle)
|
|
{
|
|
// N^2 battles
|
|
struct combatant *u[NUM_UNITS];
|
|
unsigned int i, j, k, battle = 0;
|
|
struct timeval start, end, total;
|
|
|
|
for (i = 0; i < NUM_UNITS; i++) {
|
|
unsigned hp = 1 + ((i*3)%23);
|
|
u[i] = new combatant(hp, hp + (i+7)%17, false);
|
|
u[i]->set_weapon((i % 4) + 1, (i % 9) == 0, (i % 5) == 0,
|
|
((i+4) % 4) == 0,
|
|
((i+3) % 5) == 0);
|
|
u[i]->set_effectiveness((i % 7) + 2, 0.3 + (i % 6)*0.1, (i % 8) == 0);
|
|
}
|
|
|
|
gettimeofday(&start, NULL);
|
|
for (i = 0; i < NUM_UNITS; i++) {
|
|
for (j = 0; j < NUM_UNITS; j++) {
|
|
if (i == j)
|
|
continue;
|
|
for (k = 0; k < NUM_UNITS; k++) {
|
|
double untouched;
|
|
if (i == k || j == k)
|
|
continue;
|
|
battle++;
|
|
if (specific_battle && battle != specific_battle)
|
|
continue;
|
|
u[j]->fight(*u[i]);
|
|
// We need this here, because swarm means
|
|
// out num hits can change.
|
|
u[i]->set_effectiveness((i % 7) + 2, 0.3 + (i % 6)*0.1,
|
|
(i % 8) == 0);
|
|
u[k]->fight(*u[i]);
|
|
u[i]->print("Defender", battle);
|
|
u[j]->print("Attacker #1", battle);
|
|
u[k]->print("Attacker #2", battle);
|
|
u[i]->reset();
|
|
u[j]->reset();
|
|
u[k]->reset();
|
|
}
|
|
}
|
|
}
|
|
gettimeofday(&end, NULL);
|
|
|
|
timer_sub(&end, &start, &total);
|
|
|
|
#ifdef BENCHMARK
|
|
printf("Total time for %i combats was %lu.%06lu\n",
|
|
NUM_UNITS*(NUM_UNITS-1)*(NUM_UNITS-2), total.tv_sec, total.tv_usec);
|
|
printf("Time per calc = %li us\n",
|
|
((end.tv_sec-start.tv_sec)*1000000 + (end.tv_usec-start.tv_usec))
|
|
/ (NUM_UNITS*(NUM_UNITS-1)*(NUM_UNITS-2)));
|
|
#else
|
|
printf("Total combats: %i\n", NUM_UNITS*(NUM_UNITS-1)*(NUM_UNITS-2));
|
|
#endif
|
|
|
|
for (i = 0; i < NUM_UNITS; i++) {
|
|
delete u[i];
|
|
}
|
|
|
|
exit(0);
|
|
}
|
|
|
|
static combatant *parse_unit(char ***argv,
|
|
unsigned *damagep = NULL,
|
|
double *hit_chancep = NULL,
|
|
bool *slowsp = NULL)
|
|
{
|
|
unsigned damage, num_attacks, hp, max_hp, hit_chance;
|
|
bool slows, slowed, drains, berserk, swarm, firststrike;
|
|
combatant *u;
|
|
|
|
damage = atoi((*argv)[1]);
|
|
num_attacks = atoi((*argv)[2]);
|
|
hp = max_hp = atoi((*argv)[3]);
|
|
hit_chance = atoi((*argv)[4]);
|
|
slows = false;
|
|
slowed = false;
|
|
drains = false;
|
|
berserk = false;
|
|
swarm = false;
|
|
firststrike = false;
|
|
|
|
if (damagep)
|
|
*damagep = damage;
|
|
if (hit_chancep)
|
|
*hit_chancep = hit_chance/100.0;
|
|
if (slowsp)
|
|
*slowsp = slows;
|
|
|
|
if ((*argv)[5] && atoi((*argv)[5]) == 0) {
|
|
char *max = strstr((*argv)[5], "maxhp=");
|
|
|
|
if (max) {
|
|
max_hp = atoi(max + strlen("maxhp="));
|
|
if (max_hp < hp) {
|
|
fprintf(stderr, "maxhp must be > hitpoints");
|
|
exit(1);
|
|
}
|
|
}
|
|
if (strstr((*argv)[5], "drain")) {
|
|
if (!max) {
|
|
fprintf(stderr, "drain needs maxhp set");
|
|
exit(1);
|
|
}
|
|
drains = true;
|
|
}
|
|
if (strstr((*argv)[5], "slows"))
|
|
slows = true;
|
|
if (strstr((*argv)[5], "slowed"))
|
|
slowed = true;
|
|
if (strstr((*argv)[5], "berserk"))
|
|
berserk = true;
|
|
if (strstr((*argv)[5], "firststrike"))
|
|
firststrike = true;
|
|
if (strstr((*argv)[5], "swarm")) {
|
|
if (!max) {
|
|
fprintf(stderr, "swarm needs maxhp set");
|
|
exit(1);
|
|
}
|
|
swarm = true;
|
|
}
|
|
*argv += 5;
|
|
} else {
|
|
*argv += 4;
|
|
}
|
|
u = new combatant(hp, max_hp, slowed, true);
|
|
u->set_weapon(num_attacks, drains, berserk, swarm, firststrike);
|
|
u->set_effectiveness(damage, hit_chance/100.0, slows);
|
|
return u;
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
combatant *def, *att[20];
|
|
double hit_chance;
|
|
unsigned damage;
|
|
bool slows;
|
|
unsigned int i;
|
|
|
|
if (argc < 3)
|
|
run(argv[1] ? atoi(argv[1]) : 0);
|
|
|
|
if (argc < 9) {
|
|
fprintf(stderr,"Usage: %s <damage> <attacks> <hp> <hitprob> [drain,slows,slowed,swarm,firststrike,berserk,maxhp=<num>] <damage> <attacks> <hp> <hitprob> [drain,slows,slowed,berserk,firststrike,swarm,maxhp=<num>] ...",
|
|
argv[0]);
|
|
exit(1);
|
|
}
|
|
|
|
def = parse_unit(&argv, &damage, &hit_chance, &slows);
|
|
for (i = 0; argv[1]; i++)
|
|
att[i] = parse_unit(&argv);
|
|
att[i] = NULL;
|
|
|
|
for (i = 0; att[i]; i++) {
|
|
// In case defender has swarm, effectiveness changes.
|
|
debug(("Fighting next attacker\n"));
|
|
def->set_effectiveness(damage, hit_chance, slows);
|
|
att[i]->fight(*def);
|
|
}
|
|
|
|
def->print("Defender", 0);
|
|
for (i = 0; att[i]; i++)
|
|
att[i]->print("Attacker", 0);
|
|
|
|
delete def;
|
|
for (i = 0; att[i]; i++)
|
|
delete att[i];
|
|
|
|
return 0;
|
|
}
|
|
#endif // Standalone program
|