Hello,
some time ago I've worked on improving the "Damage Calculations" dialog
that is
shown when the user is considering to attack an enemy unit. I've also
tweaked
the code to improve the selection of the defense weapon when a unit is
attacked.
Unfortunatly, I had to stop development for a while since I've been kept
busy
with other projects (my apologies to Rusty for disappearing). Now that
I've got
some time, here's a resume of the modifications:
--------------------------------------------------
Damage Calculations dialog:
The new dialog shows the damage computations and the hit points
distribution of
the units after the battle. Here's a sketch:
Damage Calculations
Attacker Defender
Base damage 10 Base damage 4
Morning +25% Morning -25%
Attacker resistance vs blade
* 0.5
Damage per strike 12
Summary 12-3 (40%) Damage per strike 2
Chance of being unscathed 9.0% Summary
2-2 (70%)
Chance of being unscathed
21.6%
Expected Battle Result (hp) Expected Battle Result (hp)
-------------------------------------
--------------------------------------
| 51|== | 9.0%| | 39|====
|21.6%|
| 49|=========== |42.0%| | 27|=========
|43.2%|
| 47|============ |49.0%| | 15|======
|28.8%|
------------------------------------- | 3|=
| 6.4%|
--------------------------------------
--------------------------------------------------
Battle simulator:
Rusty made a great battle simulator that can predict all the possible
outcomes
of a battle. The simulator handles drain, slow, first strike, berserk and
swarm. I use this simulator to obtain the hit points distribution of the
units
after the battle. However, the simulator can also be used by the AI to
accuratly predict the outcome of a battle (rather than relying on 50 dry
runs
like it is done currently). Remarkably, Rusty's simulator can "chain" many
attacks against a defender unit. Example: pikeman attacks wolfrider, then
shaman attacks wolfrider. After the first battle, the wolfrider will have
different possible HP values (e.g. 24, 10, 0 [bogus values]). The simulator
takes those values (and slow state) into account when it computes the
possible
states after the second battle. The final HP values of the wolfrider could
thus be (24, 15, 10, 5, 0). Since the simulator computes the probability
that
each unit remain unscathed (not touched by any blow), it is possible to get
the probability of a unit getting poisoned/slowed by an attack.
Rusty spent much time optimizing his simulator. I *believe* that using the
simulator is faster than performing 50 dry runs, but I've conducted no
benchmarks. Rusty is the guy who would know.
Possible caveat: due to the necessity of handling "slow + drain" properly,
Rusty had to use a slower implementation that involves a two-dimensions
matrix. The length and width of this matrix is determined by the maximum
HP of
the units that fight. So, while the computations are fast for units that
have
~75 HP (?), they will get much slower as the health of the units increase
[O(n^2)]. Hence, if Wesnoth is to allow units that have lots of HP, the
simulator will probably be too slow for heavy usage by the AI (perhaps the
current approximation code can be used in those cases). Also, the
simulator is
slower to simulate battles involving 'berserk'.
--------------------------------------------------
"battle_context" object:
To create the new dialog, I had to get the HP distributions and present the
different factors influencing the damage computations. In Wesnoth SVN
code (not
my code), evaluate_battle_stats() is used to get the strings explaining
those
factors and get the XX/XX/XX probability outcome string. However, I had
a number
of issues with evaluate_battle_stats() and its associated "battle_stats"
object:
Firstly, before I designed the new dialog, I wanted to improve the heuristic
used to select the weapon used on defense. evaluate_battle_stats() does
not make
this easy since some information such as the amount of damage dealt by the
attacker / defender are computed after the defense weapon has been chosen.
Secondly, the information contained in "battle_stats" is derived from the
current tactical situation (in other words, evaluate_battle_stats()
looks at the
map and sees that it is day, that a leadership bonus applies to the
attacker,
etc). However, I considered using Rusty's simulator eventually to
compute the
outcome of many attacks on a single defender (when the AI is planning).
For the
second and subsequent attacks, the tactical situation is not the same as the
"former" tactical situation (perhaps a unit will have leveled and provide
leadership, etc). Thus, I wanted to be able to manually set some factors
like
the leadership bonus and the time of day both for the attacker and the
defender.
Currently, evaluate_battle_stats() provides support for overriding the
terrain
used for the attacker (attacker_terrain_override) but it is not possible to
override other factors.
Finally, I wasn't fond of the way evaluate_battle_stats() mixes GUI
stuff with
the battle computations. If 'strings' isn't NULL,
evaluate_battle_stats() fills
it out with strings describing the computations. Of course, this
approach may
avoid code duplication. However I feel it would be cleaner if there was
a clear
separation between the GUI and the actual computations.
For these reasons, at the time I started writing the new dialog code, I used
the "battle_context" object I had made to fix the (perceived) issues
mentionned
above. "battle_context" is designed to replace "battle_stats"
altogether. Here's
a quick rundown of the API:
The "battle_context" object has some fields that are mandatory and some
fields
that are optional. Initially, the user sets the mandatory field with
void init(const gamemap& map, std::vector<team>& teams,
std::map<gamemap::location,unit>& units,
const gamestatus& status, const gamemap::location& attacker_loc,
const gamemap::location& defender_loc,
const attack_type& attacker_weapon);
The arguments are equivalent to the arguments used with
evaluate_battle_stats().
Then, the user may optionally specify the leadership/slowed/poisoned/time of
day/backstab state of the attacker and the defender, by setting those fields
directly in the "battle_context" object.
Afterward, the user calls compute_battle_stats() to compute the battle
statistics. This method first looks at the optional fields and obtains
the value
of any unitialized field by observing the current situation on the
battlefield,
like evaluate_battle_stats() does. Then, the method determines which
weapon will
be used on defense, if any. To do this, the method generates a
"battle_context"
object for each defense weapon that matches the attacker weapon's range.
Each of
those "battle_context" objects computes the stats of the attacker and
defender
weapons used (damage dealt, number of strikes, drain amount, etc).
Then, an heuristic function is used to determine which is the best defense
weapon. The heuristic compares the current "best" defense weapon with
the next
available defense weapon and specifies which is the best weapon. At the
end of
the process, the original "battle_context" object contains the complete
stats of
the battle with the best defense weapon. Since the heuristic function
has access
to the complete battle stats when it compares two defense weapons, it
can make
an enlightened choice. For instance, the heuristic function could elicit
not to
use a low-damage poisoning attack if the attacker is already poisoned or is
undead. I know that some developers do not want a clever heuristic though :)
For now, the heuristic of "battle_context" is compatible with the
heuristic used
in evaluate_battle_stats().
Obviously the "battle_context" object is slower than
evaluate_battle_stats() due
to the extra work to select the defender's weapon. However, in most
cases, only
one defense weapon is available. I expect "battle_context" to be perhaps
2 times
slower than evaluate_battle_stats() (just guessing, I didn't benchmark).
"battle_context" doesn't fill out 'strings' with the strings describing the
compution it makes. However, all the necessary information can be
obtained by
looking at the fields of "battle_context".
Last minute note: "battle_context" no longer exactly matches the actual
computations done by evaluate_battle_stats() since Xan has been
modifying the
code extensively today. I've updated my code so that it compiles with
the new
interfaces. However, some of Xan's stuff is a work in progress and is
likely to
change in the next few days. Because of this I didn't update my code to
reflect
Xan's changes on backstab, charge, swarm and steadfast.
--------------------------------------------------
"battle_prediction_preview_pane" object:
I modified attack_enemy() to compute the battle stats with
"attack_context", in
addition to the usual evaluate_battle_stats(). The logic doesn't change
much:
// If set to 1, the new dialog is shown, otherwise the old dialog is shown.
#if 1
attack_prediction_displayer ap_displayer(gui_, bc_vector);
std::vector<gui::dialog_button> buttons;
buttons.push_back(gui::dialog_button(&ap_displayer, _("Damage
Calculations")));
#else
attack_calculations_displayer calc_displayer(gui_,stats);
std::vector<gui::dialog_button> buttons;
buttons.push_back(gui::dialog_button(&calc_displayer,_("Damage
Calculations")));
#endif
"battle_prediction_preview_pane" is the object used to show the dialog. It
generates the graphics, obtains the strings describing the computations and
displays the dialog (straightforward code).
Some notes about the graphics. HP values >= initial HP are shown in
green, 0 HP
is shown red, otherwise it's yellow. At most 10 lines are displayed in the
graphs. The lines with the highest probability values are retained when the
limit is exceeded. Thus, when two scuttlefishes or berseckers fight, it's
possible that some values with a low probability are not shown.
--------------------------------------------------
Code modifications:
Rusty made a file called "attack_prediction.cpp" that contains his
simulator.
I've modified the file to extract the header "attack_prediction.hpp"
from it.
"Makefile.am" was modified to add "attack_prediction.cpp". The file
"playturn.cpp" was modified to contain the dialog code as described
above. The
files "actions.{cpp|hpp}" were modified to add the "battle_context"
code. The
file "font.hpp" was modified to add a prototype for the function
draw_text_line() (I needed this to draw text in a SDL surface directly).
--------------------------------------------------
Summary:
The damage computations dialog code shouldn't cause problems. My
"battle_context" code probably will, since it aims at replacing existing
code.
The question is wheter you guys are happy with evaluate_battle_stats() or if
you'd prefer "battle_context". In the former case, some work would have
to be
spent on reworking the dialog so that it uses evaluate_battle_stats() (I
guess
it would be possible to add "battle_context" just for the dialog but
that would
be a maintenance nightmare). Otherwise, evaluate_battle_stats() would
have to be
progressively phased out. The functions attack() and
attack_analysis::analyze()
use it and would need to be updated. In the process, analyze() could
make use of
Rusty's simulator to get rid of the dry runs and enhance the accuracy of the
predictions (for instance, the current dry runs do not currently take
'berserk'
into account as far as I can see).
Last minute note: I spoke with Xan regarding my "battle_context" code and it
seems there is hope that it might be integrated. Therefore I've posted
my patch
on Gna since Xan is updating evaluate_battle_stats() and I'm updating my
patch
in response to those changes. I'll try to keep my code compiling as the
various
interfaces are updated. However, the sooner my patch is accepted (or
rejected),
the less time I'll spend maintaining :) Hence here is the code. It doesn't
replace "battle_stats" in the AI and attack() yet, but if the developers
give
me the authorisation, I'll perform the conversion to battle_context
thoroughly
in the code.
I await your comments, thanks for your time!
Laurent Birtz
_______________________________________________
Wesnoth-dev mailing list
[email protected]
https://mail.gna.org/listinfo/wesnoth-dev