Skip to content

Commit

Permalink
Completed shooting at armored targets, includes melta, ord, and rendi…
Browse files Browse the repository at this point in the history
…ng. Added README.markdown. Added FKScenarioTarget.java to deal with armored vs unarmored targets in shooting and CC. FKScenarioType.java is now an emum for SHOOTING & CLOSE_COMBAT.
  • Loading branch information
ryanrolds committed Jan 2, 2011
1 parent ec87289 commit 8f971b1
Show file tree
Hide file tree
Showing 6 changed files with 403 additions and 39 deletions.
111 changes: 111 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
FKMath
======

Java library for creating senarios and calculating wounds
--------------------------------------------------------------------------

Important links
-----------------------
* [Wiki](http://github.com/ryanrolds/FKMath/wiki) - Share ideas and documentation here
* [Source Code](http://github.com/ryanrolds/FKMath) - Hosted on Github

License
-------
This software is free to use non-commercially on non-mobile devices and
non-mobile operating systems (i.e. Android, iOS, and WebOS) unless authorized by the creator.
The creator reserves exclusive rights on mobile devices and mobile operating systems.

Overview
--------
Athena is an in-memory datastructure which allows you to store objects, associating each
with a set of Strings, known as tags. You can then retrieve these objects by
specifying a boolean query on the tags. The prototype implementation is written
in Java.

For example, imagine you stored three types of animal with these tags:

<pre>
["four-legs", "hair", "domesticated"] -> "dog"
["four-legs", "hair"] -> "wolf"
["two-legs", "feathers"] -> "bird"
</pre>

Imagine you wanted to retrieve the four legged animals, you could query for:

<pre>
tag("four-legs")
</pre>

Or let's say you wanted to get all the non-domesticated four legged animals:

<pre>
and(tag("four-legs"), not(tag("domesticated")))
</pre>

Athena allows you to do this, and what's more, it strives to allow you
to do this efficiently even with millions of objects and queries far more
complex than these simple examples. It supports any combination of
"and", "or", and "not" boolean operations.

Applications
------------
The motivation for designing Athena was a situation where I have tens of thousands
of objects, each of which may have 10-20 tags, and where I need to be able to
retrieve these objects using boolean queries in a matter of microseconds. This
implies that there is no time to do a disk seek, the data must be in memory,
and an exhaustive search will be out of the question.

How it works
------------
So the naive approach to this problem would be to do an exhaustive scan of
all the objects you've got, checking each against the query, and returning
those that match.

And, in fact, the first time you query Athena, this is exactly what it does.
In other words, an exhaustive scan is the worst case scenario. Fortunately,
Athena then starts to learn how to avoid this.

Currently objects may only be added to Athena, and once added, their tags
cannot be changed. In the future this won't be the case, but we must
walk before we fly.

When objects are added, they are appended to an array (an ArrayList is currently
used for this). Each object is accompanied by a map of "shortcuts", each of
which says "all of the objects after this one and before object X
do NOT match query Y". This means that if you are looking for objects matching
query Y, then you can skip to object X. It further means that if you are
looking for objects that match query Z, and you know that no object that
matches query Z will match query Y, then you can also skip to object X.

So where do these shortcuts come from? Well, much like Hansel and
Gretel dropped breadcrumbs, they are created while Athena is searching for
stuff. Basically as Athena searches, it keeps track of objects it could have
skipped, and creates shortcuts to avoid checking those objects in the future.
I've done my best to document the shortcut creation algorithm through commenting
in the code, most of the action occurs in [StoreIterator.java](http://github.com/sanity/Athena/blob/master/src/main/java/athena/StoreIterator.java).

Note also that, since we can't have an infinite number of shortcuts, we only
keep those that seem to be useful (ie. we delete the least recently used
shortcuts).

How to play with it
-------------------
It should work "out of the box" assuming you have Maven 2 installed. Just grab
the source code and type:

<pre>
mvn assembly:assembly
</pre>

If not please [file a bug report](http://github.com/sanity/Athena/issues).

[This unit test](http://github.com/sanity/Athena/blob/master/src/test/java/athena/IntegrityTests.java)
provides a good example of basic usage.

Current status
--------------
Still just a prototype, doesn't support modification or deletion of objects,
and probably lots of room for efficiency improvements. Also it probably
isn't thread safe. All of these shortcomings should be addressable without
too much effort, this is still a very young endeavor. I can't do this alone,
so if this excites you and you think you can help, please [join our mailing list](http://groups.google.com/group/athena-discuss).
147 changes: 124 additions & 23 deletions src/com/mobilebuttes/fortyk/FKMath.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,67 @@ public class FKMath {
{ 2, 2, 2, 2, 2, 2, 2, 2, 3, 4},
};

public double getProbability(FKScenario s) {
final private int[][] outcomes = { // possible combinations of >= to a number with 1d6, 2d6
{ 6,36},
{ 6,36}, // 1
{ 5,36}, // 2
{ 4,35}, // 3
{ 3,33}, // 4
{ 2,30}, // 5
{ 1,26}, // 6
{ 0,21}, // 7
{ 0,15}, // 8
{ 0,10}, // 9
{ 0, 6}, // 10
{ 0, 3}, // 11
{ 0, 1} // 12
};

final private int[][] twoDHighest = { // Odds of that number with 2d6 pick highest - 1st column if for glancing, 2nd is for pen
{36,36},
{ 1,36}, // 1
{ 3,35}, // 2
{ 5,32}, // 3
{ 7,27}, // 4
{ 9,20}, // 5
{11,11} // 6
};

public void processScenario(FKScenario s) {
double prob = 1.0;
double toHit = calcToHit(s.getBS(),s.isTWL(),s.isOnHitSuccessRR(),s.isOnHitFailureRR());
double toWound = calcToWound(s.getStrength(),s.getToughness(),s.isOnWoundSuccessRR(),s.isOnWoundFailureRR(),s.isRending(),s.isSniper());
double saveChance = calcArmorSave(s.getArmor(),s.getAP(),s.isOnSaveSuccessRR(),s.isOnSaveFailureRR());
double invChance = calcInvSave(s.getInv(),s.isOnSaveSuccessRR(),s.isOnSaveFailureRR());
double fnpChance = calcFNP(s.isFNP(),s.getAP());

prob = toHit;
prob = prob * toWound;
prob = prob * ((saveChance <= invChance) ? saveChance : invChance);
prob = prob * fnpChance;

if(s.isRending() || s.isSniper()) prob = prob + (toHit * ANYONESIDE * invChance);


System.out.println("To Hit("+s.getBS()+"): "+toHit);
System.out.println("To Wound("+s.getStrength()+","+s.getToughness()+"): "+toWound);
System.out.println("Rending: "+s.isRending());
System.out.println("Save("+s.getArmor()+","+s.getAP()+"): "+saveChance);
System.out.println("Inv("+s.getInv()+"): "+invChance);
System.out.println("FNP("+s.isFNP()+"): "+fnpChance);
System.out.println("-----------------");
if(s.getType() == FKScenarioType.SHOOTING) {
double hitChance = calcToHit(s.getBS(),s.isTWL(),s.isOnHitSuccessRR(),s.isOnHitFailureRR());

if(s.getTarget() == FKScenarioTarget.UNARMORED) {
double toWound = calcToWound(s.getStrength(),s.getToughness(),s.isOnWoundSuccessRR(),s.isOnWoundFailureRR(),s.isRending(),s.isSniper());
double saveChance = calcArmorSave(s.getArmor(),s.getAP(),s.isOnSaveSuccessRR(),s.isOnSaveFailureRR());
double coverChance = calcCover(s.getCover());
double invChance = calcInvSave(s.getInv(),s.isOnSaveSuccessRR(),s.isOnSaveFailureRR());
double fnpChance = calcFNP(s.isFNP(),s.getAP());

double bestSave = saveChance;
if(bestSave > coverChance) bestSave = coverChance;
if(bestSave > invChance) bestSave = invChance;

prob = hitChance;
prob = prob * toWound;
prob = prob * bestSave;
prob = prob * fnpChance;

if(s.isRending()) prob = prob + (hitChance * ANYONESIDE * invChance);
} else { // TODO Shooting Armored Target
double glanceChance = calcToGlance(s.getStrength(),s.getArmor(),s.isMelta(),s.isOrdnance(),s.isRending());
double penChance = calcToPen(s.getStrength(),s.getArmor(),s.isMelta(),s.isOrdnance(),s.isRending());
double coverChance = calcCover(s.getCover());

prob = hitChance * (glanceChance + penChance) * coverChance;
}
} else {
// TODO Close Combat
}

return prob;
s.setProbability(prob);
}

// private double getNumberOfWounds(int attacks,double prob) {
Expand Down Expand Up @@ -87,11 +123,76 @@ private double calcInvSave(int inv,boolean onFailureRR,boolean onSuccessRR) {
return factorRerolls(prob,onFailureRR,onSuccessRR);
}

private double calcCover(int cover) { //TODO - make sure that it doesn't need to be inverted
if(cover == 0) return ONE;

return (DICEINVERSION - cover) * ANYONESIDE;
}

private double calcFNP(boolean fnp,int weaponAP) {
if(fnp == false || (weaponAP < 3 && weaponAP != -1)) return ONE;
return 0.5;
}

private double calcToGlance(int str,int armor,boolean melta,boolean ord,boolean rending) {
int diff = armor - str;
int dice = 0; // This is for the outcome table, index 0 is one dice, index 2 is two dice

if(melta) dice++; // Meltas get an extra dice

if(diff < (dice + 1)) return ZERO; // If you can't roll the exact number needed then it will always be a pen hit.

// If the difference is larger when what be rolled it will always fail
if(diff > 12 || (diff > 9 && !melta) || (diff > 6 && !rending)) return ZERO;

if(!melta && rending && diff > 6) {
return ANYONESIDE * (ANYONESIDE * 2.0); // For getting a 6 then getting 1-3 on the d3
}

if(ord && !melta) {
// Get the combination the 2d6 pick highest table;
int combinations = twoDHighest[diff][0];
// Use the combination to work out the odd of getting a glance
return combinations * (ONE / twoDHighest[0][0]);
}

// We want the # of combinations for rolling difference
int combinations = outcomes[diff][dice] - outcomes[diff+1][dice];

//Take the # of combinations and multiple it by the chance of getting one combinations
return combinations * (ONE / outcomes[0][dice]);
}

private double calcToPen(int str,int armor,boolean melta,boolean ord,boolean rending) {
int diff = armor - str;
int dice = 0; // This is for the outcome table, index 0 is one dice, index 2 is two dice

if(melta) dice++; // Meltas get an extra dice

if(diff < (dice + 1)) return ONE; // If you can't roll the exact number needed then it will always be a pen hit.

// If the difference is larger when what be rolled it will always fail
if(diff > 12 || (diff > 9 && !melta) || (diff > 6 && !rending)) return ZERO;

if(!melta && rending && diff >= 6) {
int inverse = 4 - ((diff - 6) + 1);
if(inverse < 1) return ZERO;
return ANYONESIDE * (inverse * (ANYONESIDE * 2.0)); // For getting a 6 then getting 1-3 on the d3
}

if(ord && !melta) {
// Get the combination the 2d6 pick highest table;
int combinations = twoDHighest[diff+1][1];
// Use the combination to work out the odd of getting a pen
return combinations * (ONE / twoDHighest[0][1]);
}

// We want the # of combinations for rolling difference
int combinations = outcomes[diff+1][dice];
//Take the # of combinations and multiple it by the chance of getting any acceptable combination
return combinations * (ONE/ outcomes[0][dice]);
}

private double factorRerolls(double prob,boolean onFailureRR,boolean onSuccessRR) {
if(prob == ZERO) return ZERO;

Expand All @@ -100,4 +201,4 @@ private double factorRerolls(double prob,boolean onFailureRR,boolean onSuccessRR

return prob;
}
}
}
Loading

0 comments on commit 8f971b1

Please sign in to comment.