Attack prediction code giving probability of each unit's possible HP values.

Code is not currently built by default, as it's not used anywhere yet.
The tools/ code can be used to check the correctness of the algorithm,
esp for any future modifications.  Handles slow, drain, berserk,
firststrike and swarm.
This commit is contained in:
Rusty Russell 2006-01-17 13:22:12 +00:00
parent 1882c2925c
commit 7d76a74f1f
2 changed files with 1261 additions and 0 deletions

850
src/attack_prediction.cpp Normal file
View file

@ -0,0 +1,850 @@
/* $Id$ */
/*
Copyright (C) 2006 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.
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.
*/
#include <cstring> // For memset
#include <vector>
#include "global.hpp"
#include "wassert.hpp"
// 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 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, 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, double hit_chance,
bool b_slows, bool b_drains);
// 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 *prob_matrix::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);
// 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.
wassert(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()) {
// 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 < rows; row++)
val(NEITHER_SLOWED, row, b_hp) = a_summary[0][row];
if (!a_summary[1].empty()) {
for (unsigned int row = 0; row < rows; 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 < cols; col++)
val(NEITHER_SLOWED, a_hp, col) = b_summary[0][col];
if (!b_summary[1].empty()) {
for (unsigned int col = 0; col < cols; col++)
val(B_SLOWED, a_hp, col) = b_summary[1][col];
}
debug(("B has fought before\n"));
dump();
} else
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)
{
return plane[p][row * cols + col];
}
const double &prob_matrix::val(unsigned p, unsigned row, unsigned col) const
{
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, 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 = damage / 2;
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];
}
}
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, 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 = damage/2;
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];
}
}
// This encapsulates all we need to know for this combat.
struct combatant
{
// Construct a combatant.
// As an optimization, if it has no weapons which could drain,
// we can simply never calculate hitpoints > current hp.
combatant(unsigned hp, unsigned max_hp, bool slowed,
bool could_ever_drain = true);
// Select a weapon.
void set_weapon(unsigned num_attacks, bool drains, bool berserk,
bool swarm, bool firststrike);
// Set effect against this particular opponent.
// FIXME: damage if slowed.
void set_effectiveness(unsigned damage, double hit_chance, bool slows);
// Fight!
void fight(combatant &opponent);
// Only used in benchmarking.
void reset();
void print(const char label[], unsigned int battle, double touched = 0.0)
const;
// Resulting probability distribution (may NOT be as large as max_hp)
std::vector<double> hp_dist;
// Resulting chance we were not hit by this opponent (important if
// it poisons)
double untouched;
private:
// How many attacks? (Matters for swarm).
unsigned num_attacks(unsigned int hp) const;
// Usually uniform, but if we have swarm, then can be different.
std::vector<double> hit_chances_;
double base_hit_chance_;
// Starting hitpoints, max hp.
unsigned hp_, max_hp_;
// Are we slowed already? (Halves damage, can't be slowed again).
bool slowed_;
// Weapon stats.
unsigned base_num_attacks_, damage_;
bool drains_, slows_, berserk_, swarm_, firststrike_;
// Summary of matrix used to calculate last battle (unslowed & slowed).
std::vector<double> summary[2];
};
combatant::combatant(unsigned hp, unsigned max_hp, bool slowed,
bool could_ever_drain)
: hp_dist(could_ever_drain ? max_hp+1: hp+1), hp_(hp), max_hp_(max_hp),
slowed_(slowed)
{
}
// Select a weapon.
void combatant::set_weapon(unsigned num_attacks, bool drains, bool berserk,
bool swarm, bool firststrike)
{
base_num_attacks_ = num_attacks;
drains_ = drains;
berserk_ = berserk;
swarm_ = swarm;
firststrike_ = firststrike;
}
unsigned combatant::num_attacks(unsigned int hp) const
{
if (swarm_)
return base_num_attacks_ - (base_num_attacks_*(max_hp_-hp)/max_hp_);
else
return base_num_attacks_;
}
// Set effect against this particular opponent.
void combatant::set_effectiveness(unsigned damage, double hit_chance,
bool slows)
{
slows_ = slows;
base_hit_chance_ = hit_chance;
if (slowed_)
damage_ = damage / 2;
else
damage_ = damage;
if (!swarm_ || summary[0].empty())
hit_chances_ = std::vector<double>(num_attacks(hp_), hit_chance);
else {
// 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.
hit_chances_ = std::vector<double>(base_num_attacks_);
double alive_prob;
if (summary[1].empty())
alive_prob = 1 - summary[0][0];
else
alive_prob = 1 - summary[0][0] - summary[1][0];
for (unsigned int i = 1; i <= max_hp_; i++) {
double prob = summary[0][i];
if (!summary[1].empty())
prob += summary[1][i];
for (unsigned int j = 0; j < num_attacks(i); j++)
hit_chances_[j] += prob * hit_chance / alive_prob;
}
}
debug(("\nhit_chances_ (base %u%%):", (unsigned)(hit_chance * 100.0 + 0.5)));
for (unsigned int i = 0; i < base_num_attacks_; i++)
debug((" %.2f", hit_chances_[i]));
debug(("\n"));
}
void combatant::reset()
{
for (unsigned int i = 0; i < hp_dist.size(); i++)
hp_dist[i] = 0.0;
summary[0] = std::vector<double>();
summary[1] = std::vector<double>();
hit_chances_ = std::vector<double>(num_attacks(hp_), base_hit_chance_);
}
// 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 i, rounds = berserk_ || opp.berserk_ ? 30 : 1;
// If defender has firststrike and we don't, reverse.
if (opp.firststrike_ && !firststrike_) {
opp.fight(*this);
return;
}
debug(("A: %u %u %u %2g%% ",
damage_, base_num_attacks_, hp_, base_hit_chance_*100.0));
if (drains_)
debug(("drains,"));
if (slows_ && !opp.slowed_)
debug(("slows,"));
if (berserk_)
debug(("berserk,"));
if (swarm_)
debug(("swarm,"));
debug(("maxhp=%u\n", hp_dist.size()-1));
debug(("B: %u %u %u %2g%% ", opp.damage_, opp.base_num_attacks_, opp.hp_,
opp.base_hit_chance_*100.0));
if (opp.drains_)
debug(("drains,"));
if (opp.slows_ && !slowed_)
debug(("slows,"));
if (opp.berserk_)
debug(("berserk,"));
if (opp.swarm_)
debug(("swarm,"));
debug(("maxhp=%u\n", opp.hp_dist.size()-1));
prob_matrix m(hp_dist.size()-1, opp.hp_dist.size()-1,
slows_ && !opp.slowed_, opp.slows_ && !slowed_, hp_, opp.hp_,
summary, opp.summary);
untouched = 1.0;
opp.untouched *= 1.0;
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()));
do {
for (i = 0; i < max_attacks; i++) {
if (i < hit_chances_.size()) {
debug(("A strikes\n"));
m.receive_blow_b(damage_, hit_chances_[i], slows_, drains_);
m.dump();
opp.untouched *= (1 - hit_chances_[i]);
}
if (i < opp.hit_chances_.size()) {
debug(("B strikes\n"));
m.receive_blow_a(opp.damage_, opp.hit_chances_[i],
opp.slows_, opp.drains_);
m.dump();
untouched *= (1 - opp.hit_chances_[i]);
}
}
debug(("Combat ends:\n"));
m.dump();
} while (--rounds && m.dead_prob() < 0.99);
// We extract results separately, then combine.
m.extract_results(summary, opp.summary);
if (summary[1].empty())
hp_dist = summary[0];
else {
for (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 (i = 0; i < opp.hp_dist.size(); i++)
opp.hp_dist[i] = opp.summary[0][i] + opp.summary[1][i];
}
}
};
#if defined(BENCHMARK) || defined(CHECK)
// We create a significant number of nasty-to-calculate units, and
// test each one against the others.
#ifdef BENCHMARK
#define NUM_UNITS 50
#else
#define NUM_UNITS 25
#endif
// 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, double touched) 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);
if (touched != 0.0)
printf(" %.2f", touched);
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, double touched) 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/2 + ((i*2)%40);
u[i] = new combatant(hp, hp + (i+7)%20, 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 touched;
if (i == k || j == k)
continue;
battle++;
if (battle < specific_battle)
continue;
u[j]->fight(*u[i]);
touched = 1 - u[i]->untouched;
// 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]);
touched *= 1 - u[i]->untouched;
u[i]->print("Defender", battle, touched);
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
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, 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;
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], "slow"))
slows = 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, false, 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,slow,swarm,firststrike,berserk,maxhp=<num>] <damage> <attacks> <hp> <hitprob> [drain,slow,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);
return 0;
}
#endif // Standalone program

411
tools/wesnoth-attack-sim.c Normal file
View file

@ -0,0 +1,411 @@
/* Simulate Wesnoth combat. */
#define _GNU_SOURCE
#include <stdbool.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <sys/time.h>
#include <time.h>
#include <errno.h>
/* from the Linux Kernel:
* min()/max() macros that also do
* strict type-checking.. See the
* "unnecessary" pointer comparison.
*/
#define max(x,y) ({ \
typeof(x) _x = (x); \
typeof(y) _y = (y); \
(void) (&_x == &_y); \
_x > _y ? _x : _y; })
struct unit
{
unsigned damage, num_attacks, hp, max_hp;
unsigned hit_chance;
bool slowed, slows, drains, berserk, swarm, firststrike;
bool touched;
};
static void __attribute__((noreturn, format(printf,1,2)))
barf(const char *fmt, ...)
{
char *str;
va_list arglist;
fprintf(stderr, "FATAL: ");
va_start(arglist, fmt);
vasprintf(&str, fmt, arglist);
va_end(arglist);
fprintf(stderr, "%s\n", str);
free(str);
exit(1);
}
static bool hits(unsigned int chance)
{
return (random() % 100) < chance;
}
static void strike(struct unit *attacker, struct unit *defender)
{
unsigned damage = attacker->damage;
if (!hits(attacker->hit_chance))
return;
if (damage > defender->hp)
damage = defender->hp;
if (attacker->slows && !defender->slowed) {
defender->slowed = true;
defender->damage /= 2;
}
if (attacker->drains) {
attacker->hp += damage/2;
if (attacker->hp > attacker->max_hp)
attacker->hp = attacker->max_hp;
}
defender->hp -= damage;
defender->touched = true;
}
// A attacks B.
static void simulate_attack(struct unit *a, struct unit *b)
{
unsigned int i, j;
for (i = 0; i < ((a->berserk || b->berserk) ? 30: 1); i++) {
for (j = 0; j < max(a->num_attacks, b->num_attacks); j++) {
if (j < a->num_attacks) {
strike(a, b);
if (b->hp == 0)
return;
}
if (j < b->num_attacks) {
strike(b, a);
if (a->hp == 0)
return;
}
}
}
}
static unsigned int num_attacks(unsigned base, unsigned max, unsigned hp,
bool swarm)
{
if (!swarm)
return base;
/* Swarm scales num attacks by hp. */
return base - (base*(max-hp) / max);
}
/* This gives a max variation of around 1%. */
static void calculate_attack(const struct unit *defender,
double defender_res[],
double *defender_touched,
const struct unit *attackers[],
double *attacker_res[],
unsigned num_attackers,
unsigned num_sims)
{
unsigned int i, j;
*defender_touched = 0;
for (i = 0; i < num_sims; i++) {
struct unit def = *defender;
def.slowed = false;
for (j = 0; j < num_attackers && def.hp; j++) {
struct unit att = *attackers[j];
att.slowed = false;
att.num_attacks = num_attacks(att.num_attacks,
att.max_hp, att.hp,
att.swarm);
def.num_attacks = num_attacks(defender->num_attacks,
defender->max_hp, def.hp,
def.swarm);
if (def.firststrike && !att.firststrike)
simulate_attack(&def, &att);
else
simulate_attack(&att, &def);
attacker_res[j][att.hp]++;
}
defender_res[def.hp]++;
if (def.touched)
(*defender_touched)++;
}
/* Now normalize each one by number of battles it was in. */
for (i = 0; i <= defender->max_hp; i++)
defender_res[i] /= num_sims;
*defender_touched /= num_sims;
for (i = 0; i < num_attackers; i++) {
unsigned int battles = 0;
for (j = 0; j <= attackers[i]->max_hp; j++) {
battles += attacker_res[i][j];
attacker_res[i][j] /= num_sims;
}
/* Any battle we weren't in, we're unscathed. */
attacker_res[i][attackers[i]->hp]
+= (1.0*num_sims-battles)/num_sims;
}
}
static struct unit *parse_unit(char ***argv)
{
struct unit *u = malloc(sizeof(*u));
u->damage = atoi((*argv)[1]);
u->num_attacks = atoi((*argv)[2]);
u->hp = u->max_hp = atoi((*argv)[3]);
u->hit_chance = atoi((*argv)[4]);
u->slows = false;
u->slowed = false;
u->drains = false;
u->berserk = false;
u->swarm = false;
u->firststrike = false;
if ((*argv)[5] && atoi((*argv)[5]) == 0) {
char *max = strstr((*argv)[5], "maxhp=");
if (max) {
u->max_hp = atoi(max + strlen("maxhp="));
if (u->max_hp < u->hp)
barf("maxhp must be > hitpoints");
}
if (strstr((*argv)[5], "drain")) {
if (!max)
barf("drain needs maxhp set");
u->drains = true;
}
if (strstr((*argv)[5], "slow"))
u->slows = true;
if (strstr((*argv)[5], "berserk"))
u->berserk = true;
if (strstr((*argv)[5], "firststrike"))
u->firststrike = true;
if (strstr((*argv)[5], "swarm")) {
if (!max)
barf("swarm needs maxhp set");
u->swarm = true;
}
*argv += 5;
} else
*argv += 4;
return u;
}
#if 0
static void graph_prob(unsigned int hp, double prob)
{
unsigned int i, percent;
percent = (prob + 1/200.0) * 100;
printf("%-3u %3u%% |", hp, percent);
for (i = 0; i < percent; i++)
printf("#");
printf("\n");
}
#endif
static void draw_results(const double res[], const struct unit *u,
const char label[])
{
unsigned int i;
printf("#0: %s: %u %u %u %u%% ",
label, u->damage, u->num_attacks, u->hp, u->hit_chance);
if (u->drains)
printf("drains,");
if (u->slows)
printf("slows,");
if (u->berserk)
printf("berserk,");
if (u->swarm)
printf("swarm,");
if (u->firststrike)
printf("firststrike,");
printf("maxhp=%u ", u->max_hp);
for (i = 0; i < u->max_hp+1; i++)
printf(" %.2f", res[i]*100);
printf("\n");
}
static void compare_results(const double res[], const struct unit *u,
const char label[], unsigned battle, double def_touched, FILE *f)
{
unsigned int i;
char line[128], cmp[128];
sprintf(cmp, "#%u: %s: %u %u %u %u%% ", battle,
label, u->damage, u->num_attacks, u->hp, u->hit_chance);
if (u->drains)
sprintf(cmp+strlen(cmp), "drains,");
if (u->slows)
sprintf(cmp+strlen(cmp), "slows,");
if (u->berserk)
sprintf(cmp+strlen(cmp), "berserk,");
if (u->swarm)
sprintf(cmp+strlen(cmp), "swarm,");
if (u->firststrike)
sprintf(cmp+strlen(cmp), "firststrike,");
sprintf(cmp+strlen(cmp), "maxhp=%u", u->max_hp);
if (fread(line, strlen(cmp), 1, f) != 1)
barf("Unexpected end of file on battle %u", battle);
if (strncmp(line, cmp, strlen(cmp)) != 0)
barf("Battle %u is different: '%.*s' should be '%s'",
battle, strlen(cmp), line, cmp);
if (def_touched != 0.0) {
double val;
if (fscanf(f, " %lf", &val) != 1)
barf("Malformed touched: %s hp %u battle %u",
label, i, battle);
if (abs(val - def_touched)*100 > 1.0)
barf("Expected %f touched, got %f battle %u",
def_touched, val, battle);
}
for (i = 0; i < u->max_hp+1; i++) {
double val;
if (fscanf(f, " %lf", &val) != 1)
barf("Malformed hp line: %s hp %u battle %u",
label, i, battle);
#if 0
if (abs(val - res[i]*100) > 5.0)
barf("Battle %u %s hp %u %f should be %f",
battle, label, i, val, res[i]*100);
#endif
if (abs(val - res[i]*100) > 1.0)
printf("Warning: battle %u %s hp %u %f should be %f\n",
battle, label, i, val, res[i]*100);
}
fscanf(f, "\n");
}
static void check_sum(double *arr, unsigned int num)
{
unsigned int i;
double sum = 0;
for (i = 0; i <= num; i++)
sum += arr[i];
assert(sum > 0.999 && sum < 1.001);
}
#define NUM_UNITS 25
static void check(const char *filename)
{
/* N^2 battles. */
struct unit u[NUM_UNITS];
unsigned int i, j, k, battle = 0, percent;
FILE *f = fopen(filename, "r");
if (!f)
barf("Could not open %s for reading: %s",
filename, strerror(errno));
printf("Creating %i units...\n", NUM_UNITS);
for (i = 0; i < NUM_UNITS; i++) {
u[i].hp = 1 + i/2 + ((i*2)%40);
u[i].max_hp = u[i].hp + (i+7)%20;
u[i].damage = (i % 7) + 2;
u[i].num_attacks = (i % 4) + 1;
u[i].slows = (i % 8) == 0;
u[i].drains = (i % 9) == 0;
u[i].berserk = (i % 5) == 0;
u[i].hit_chance = 30 + (i % 6)*10;
u[i].swarm = ((i+4) % 4) == 0;
// u[i].swarm = false;
u[i].firststrike = ((i+3) % 5) == 0;
}
printf("Beginning battle...\n");
percent = NUM_UNITS*(NUM_UNITS-1)*(NUM_UNITS-2)/100;
srandom(time(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++) {
if (k == i || k == j)
continue;
double i_result[u[i].max_hp+1];
double j_result[u[j].max_hp+1];
double k_result[u[k].max_hp+1];
double i_touched;
double *attacker_res[2];
const struct unit *attackers[2];
memset(i_result, 0, sizeof(i_result));
memset(j_result, 0, sizeof(j_result));
memset(k_result, 0, sizeof(k_result));
attacker_res[0] = j_result;
attacker_res[1] = k_result;
attackers[0] = &u[j];
attackers[1] = &u[k];
calculate_attack(&u[i], i_result, &i_touched,
attackers, attacker_res, 2,
10000);
battle++;
check_sum(i_result, u[i].max_hp);
check_sum(j_result, u[j].max_hp);
check_sum(k_result, u[k].max_hp);
compare_results(i_result, &u[i], "Defender",
battle, i_touched, f);
compare_results(j_result, &u[j], "Attacker #1",
battle, 0.0, f);
compare_results(k_result, &u[k], "Attacker #2",
battle, 0.0, f);
if ((battle % percent) == 0) {
printf(".");
fflush(stdout);
}
}
}
}
printf("\nTotal combats: %i\n", NUM_UNITS*(NUM_UNITS-1)*(NUM_UNITS-2));
exit(0);
}
int main(int argc, char *argv[])
{
unsigned int i;
double *res_def, *res_att[argc / 4], touched;
const struct unit *def, *attacker[argc / 4 + 1];
if (argc == 3 && strcmp(argv[1], "--check") == 0)
check(argv[2]);
if (argc < 9)
barf("Usage: %s --check <results-file>\n"
"\t%s <damage> <attacks> <hp> <hitprob> [drain,slow,swarm,firststrike,berserk,maxhp=<num>] <damage> <attacks> <hp> <hitprob> [drain,slow,berserk,firststrike,swarm,maxhp=<num>] ...",
argv[0], argv[0]);
def = parse_unit(&argv);
res_def = calloc(sizeof(double), def->max_hp+1);
for (i = 0; argv[1]; i++) {
attacker[i] = parse_unit(&argv);
res_att[i] = calloc(sizeof(double), attacker[i]->max_hp+1);
}
attacker[i] = NULL;
srandom(time(NULL));
calculate_attack(def, res_def, &touched, attacker, res_att, i, 10000);
draw_results(res_def, def, "Defender");
for (i = 0; attacker[i]; i++)
draw_results(res_att[i], attacker[i], "Attacker");
return 0;
}