Skip to content

Commit

Permalink
Reorganize the perten integer fractional math code.
Browse files Browse the repository at this point in the history
Now using a value in a struct instead of just a typedef. This
is to intentionally make it harder to mix with other types as
I found many bugs related to bad type conversions.
  • Loading branch information
perim committed Jan 11, 2024
1 parent e78ea30 commit b9a0305
Show file tree
Hide file tree
Showing 10 changed files with 463 additions and 289 deletions.
16 changes: 11 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ ADD_EXECUTABLE(test1 tests/test1.cpp ${DICE_SRC})
TARGET_INCLUDE_DIRECTORIES(test1 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
TARGET_LINK_LIBRARIES(test1 ${DICE_LIBS})

ADD_EXECUTABLE(test_arpgstats tests/test_arpgstats.cpp arpgstats.h dmath.h)
ADD_EXECUTABLE(perten_test tests/perten_test.cpp ${DICE_SRC} perten.h)
TARGET_INCLUDE_DIRECTORIES(perten_test PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
TARGET_LINK_LIBRARIES(perten_test ${DICE_LIBS})

ADD_EXECUTABLE(test_arpgstats tests/test_arpgstats.cpp arpgstats.h dmath.h perten.h)
TARGET_INCLUDE_DIRECTORIES(test_arpgstats PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
TARGET_LINK_LIBRARIES(test_arpgstats ${DICE_LIBS})

Expand All @@ -37,9 +41,9 @@ ADD_EXECUTABLE(perf_derive tests/perf_derive.cpp dice.cpp dice.h dmath.h)
TARGET_INCLUDE_DIRECTORIES(perf_derive PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
TARGET_LINK_LIBRARIES(perf_derive ${DICE_LIBS})

ADD_EXECUTABLE(perf_dmath tests/perf_dmath.cpp dmath.h)
TARGET_INCLUDE_DIRECTORIES(perf_dmath PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
TARGET_LINK_LIBRARIES(perf_dmath ${DICE_LIBS})
ADD_EXECUTABLE(perf_perten tests/perf_perten.cpp dmath.h perten.h)
TARGET_INCLUDE_DIRECTORIES(perf_perten PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
TARGET_LINK_LIBRARIES(perf_perten ${DICE_LIBS})

ADD_EXECUTABLE(perf_table_rolls tests/perf_table_rolls.cpp dice.cpp dice.h dmath.h dmath.cpp)
TARGET_INCLUDE_DIRECTORIES(perf_table_rolls PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
Expand Down Expand Up @@ -94,4 +98,6 @@ ADD_TEST(perf_quadratic_roll perf_quadratic_roll)
ADD_TEST(stats stats)
ADD_TEST(perf_prd perf_prd)
ADD_TEST(visualization visualization)
ADD_TEST(test_arpgstats test_arpgstats)
#ADD_TEST(test_arpgstats test_arpgstats)
ADD_TEST(perten_test perten_test)
ADD_TEST(perf_perten perf_perten)
269 changes: 148 additions & 121 deletions arpgstats.h

Large diffs are not rendered by default.

46 changes: 0 additions & 46 deletions dmath.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,49 +73,3 @@ static inline constexpr __attribute__((const)) uint64_t lfsr_init(uint64_t stat

/// CPU time measurement
static inline uint64_t cpu_gettime() { struct timespec t; clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t); return ((uint64_t)t.tv_sec * 1000000000ull + (uint64_t)t.tv_nsec); }

// -- 2^10 fractional math --
// Numbers based around 100 are not good to do fast math on, so we can use to a 2^10 system instead. We call these "perten" values. Math using
// these values avoid most divisions and use less multiplies. These values are capped at their numerical maximum, and will never overflow while
// using this API. An "increase" goes to infinity, while a "reduction" approaches but never reaches the base value ceiling. These can both be
// used on the same value if they are meant to be counteracting each other. Be careful about conversions back and forth between perten and
// percent, especially if you use percent as a intermediary value, as this conversion can be lossy and lead to cascading errors. For example
// percent -> perten -> percent is stable, while perten -> percent -> perten is not. We can safely operate on down to 0.1% values, but floating
// point precision beyond this point will be lost.
using perten = uint32_t;
const int perten_bits = 10;
const perten perten_base = (1 << perten_bits);
static inline constexpr __attribute__((const)) int perten_to_percent(perten v) { return (((uint64_t)v + 1) * 100u) >> perten_bits; }
static inline constexpr __attribute__((const)) float perten_to_percentf(perten v) { return ceil((float)v * 1000.0f / (float)perten_base) / 10.0f; }
static inline constexpr __attribute__((const)) perten percent_to_perten(int v) { return std::min<uint64_t>((v << perten_bits) / 100u, UINT32_MAX); }
static inline constexpr __attribute__((const)) perten percent_to_perten(float v) { return (v * perten_base) / 100.0f; }
static inline constexpr __attribute__((const)) perten perten_add_increase(perten orig, perten mod) { return std::min<uint64_t>(((uint64_t)orig * (mod + perten_base)) >> perten_bits, UINT32_MAX); }
static inline constexpr __attribute__((const)) perten perten_add_reduction(perten orig, perten mod) { return ((uint64_t)orig * (perten_base - mod)) >> perten_bits; }
static inline constexpr __attribute__((const)) perten perten_apply(perten orig, perten mod) { return std::min<uint64_t>(((uint64_t)orig * (uint64_t)mod) >> perten_bits, UINT32_MAX); }
static inline constexpr __attribute__((const)) perten perten_apply_reverse(perten orig, perten mod) { return ((uint64_t)orig << perten_bits) / mod; }

// -- Lazy conditional perten multiplicative matrix
// This implements a simple cache for a matrix of values, where each column has a conditional and the only output is the cached sum
// of the column values for a given row. The type has to be unsigned. The maximum value of T is reserved for cache maintenance.
template<size_t C, size_t R>
struct lazy_conditional_matrix
{
lazy_conditional_matrix() { enabled.fill(false); cache.fill(UINT32_MAX); for (auto& i : matrix) for (auto& j : i) j = perten_base; }

/// return multiplicative sum of all enabled columns
inline perten row(int r) { if (cache[r] == UINT32_MAX) calc(r); return cache[r]; }

/// modify one value and dirty its cache line
inline void modify(int c, int r, perten value) { matrix[c][r] = value; cache[r] = UINT32_MAX; }

/// modify one condition and dirty relevant cache lines
void toggle(int c, bool v) { if (enabled[c] != v) { enabled[c] = v; for (unsigned r = 0; r < R; r++) if (matrix[c][r] != perten_base) cache[r] = UINT32_MAX; } }

inline bool is_enabled(int c) const { return enabled[c]; }

private:
void calc(int r) { cache[r] = perten_base; for (unsigned c = 0; c < C; c++) { if (enabled[c]) cache[r] = (cache[r] * matrix[c][r]) >> perten_bits; } }
std::array<std::array<perten, R>, C> matrix;
std::array<uint_fast8_t, C> enabled;
std::array<perten, R> cache;
};
8 changes: 6 additions & 2 deletions doc/arpgstats.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ We define a 'power' as a finite resource of some kind that the entity uses
or needs and can replenish. For example health, mana, stamina, or energy. You
should set the power_types constant to the number of powers you will use.

Internally arpgstats stores the amount as a fraction of 1024. You can use this
amount directly or use it as a fraction to derive your own values.
The current and max values are communicated as percentages, but internally stored
as more fine-grained integer fractions.

Definitions:
* Regeneration - amount of power automatically regained each tick
Expand Down Expand Up @@ -84,6 +84,10 @@ from each power until we reach the first power, which then takes on the remainin
if possible. It is assumed that the first power is the most valuable, without which very
bad things will happen (eg death).

The damage distribution is how much of the damage that should be attributed to each power.
The sum total of damage distribution must always be 100%, and by default the first power
has 100% of the damage.

There is a built-in limit to how many seconds of damage over time we can track. This is
defined by the constant `max_effect_secs`. Damage over time exceeding this limit is lost.

Expand Down
10 changes: 4 additions & 6 deletions doc/chunky.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ chunks will get matching exits.
It can pseudo-randomly place doors and one-way doors in the
dungeon and attempt to place them to minimize backtracking.

The dungeon is stored as a typical text-based rogue-like format
as text in memory for ease of debugging and display. Each room
contains an isolatedness score which tells you how far it is off
the main path through the chunk.
Each room contains an isolatedness score which tells you how far
it is off the main path through the chunk.

Example dungeon:
```
Expand Down Expand Up @@ -54,11 +52,11 @@ Example dungeon:
This could be generated with just this code:
```c++
chunkconfig config(s);
chunk c(config);
chunk c(config); // create with default size and location
c.generate_exits(); // create our exits depending our chunk location
chunk_filter_connect_exits(c); // make sure we have conenctivity
chunk_filter_room_expand(c); // this is where most rooms are made
chunk_filter_one_way_doors(c);
chunk_filter_one_way_doors(c); // add secret one-way doors
c.beautify(); // prepare it for pretty printing
c.print_chunk(); // print to screen
```
Expand Down
109 changes: 109 additions & 0 deletions perten.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#pragma once

// -- 2^10 fractional math --
// Floating point is notoriously non-deterministic when we want to be portable and can be slow, while numbers based around 100 (percentage) or any other
// 10-based value are not good for fast or accurate math. For this reason we here define a 2^10 system instead which we call "perten" values. Math using
// these values avoid most divisions and use less multiply instructions. These values are capped at their numerical maximum, and will never overflow or
// wrap around while using this API. The design is a bit clunky to make it so strongly typed that you cannot accidentally intermix it with other types
// of values. It is designed to easily and safely implement a multiplicative stats system.

// An "increase" approaches the type maximum, while a "reduce" approaches the base value ceiling. These can both be used on the same
// value if they are meant to be counteracting each other. Be careful about conversions back and forth between perten and percent, especially if you use
// percent as a intermediary value, as this conversion can be lossy and lead to cascading errors. For example percent -> perten -> percent is stable,
// while perten -> percent -> perten is not. For example adding a 10% value in perten and multiplying it by 10 will not yield you exactly 100%. We can
// safely operate on 0.1% precision and up to 4 million in total value, any precision beyond this point will be lost.

#include "dmath.h"
#include <cassert>

/// Ten bits precision
const int perten_bits = 10;

/// `perten_base` is equal to 1.0 or 100%
const uint32_t perten_base = (1 << perten_bits);

struct perten
{
uint32_t value = 0;
bool operator==(const perten& rhs) const { return value == rhs.value; }
bool operator<=(const perten& rhs) const { return value <= rhs.value; }
bool operator>=(const perten& rhs) const { return value >= rhs.value; }
bool operator!=(const perten& rhs) const { return value != rhs.value; }
bool operator>(const perten& rhs) const { return value > rhs.value; }
bool operator<(const perten& rhs) const { return value < rhs.value; }
perten operator+(const perten& rhs) const { return perten{value + rhs.value}; }
perten operator-(const perten& rhs) const { return perten{value - rhs.value}; }
void operator+=(const perten& rhs) { value += rhs.value; }
void operator-=(const perten& rhs) { value -= rhs.value; }
/*
perten& operator=(perten& other) { value = other; }
perten operator=(perten other) { value = other; }
perten operator=(uint32_t other) { value = other << perten_bits; }
perten operator=(int other) { value = other << perten_bits; }*/
};

// Using these constants rather than converting from percentages preserves perfect accuracy
const perten perten_empty = perten{0};
const perten perten_full = perten{perten_base}; // 1024
const perten perten_half = perten{1 << (perten_bits - 1)}; // 512
const perten perten_quarter = perten{1 << (perten_bits - 2)}; // 256, or 25%
const perten perten_eight = perten{1 << (perten_bits - 3)}; // 128, close to 12%
const perten perten_16th = perten{1 << (perten_bits - 4)}; // 64, close to 6%
const perten perten_32th = perten{1 << (perten_bits - 5)}; // 32, close to 3%
const perten perten_64th = perten{1 << (perten_bits - 6)}; // 16, close to 1.6%
const perten perten_128th = perten{1 << (perten_bits - 7)}; // 8, close to 0.8%
const perten perten_256th = perten{1 << (perten_bits - 8)}; // 4, close to 0.4%
const perten perten_512th = perten{1 << (perten_bits - 9)}; // 2, close to 0.2%
const perten perten_1024th = perten{1 << (perten_bits - 10)}; // 1, close to 0.1%
const perten perten_double = perten{perten_base * 2};
const perten perten_triple = perten{perten_base * 3};
const perten perten_tenth = perten{102}; // but note how perten_tenth * 10 != perten_full
const perten perten_max = perten{UINT32_MAX};

/// Reduce `source` by `amount` and reduce `amount` by the amount actually reduced. Cannot reduce below zero.
static inline void perten_drain(perten& amount, perten& source) { const uint32_t old_amount = amount.value; amount.value -= std::min<uint32_t>(amount.value, source.value); source.value -= std::min<uint32_t>(old_amount, source.value); }
static inline __attribute__((const)) perten perten_multiply(perten a, perten b) { return perten{(uint32_t)((uint64_t)a.value * (uint64_t)b.value) >> perten_bits}; }
static inline __attribute__((const)) int perten_to_int(perten v) { return (int)(v.value >> perten_bits); }
static inline __attribute__((const)) unsigned perten_to_uint(perten v) { return (v.value >> perten_bits); }
static inline __attribute__((const)) double perten_to_float(perten v) { return ((double)v.value / (double)perten_base); }
static inline __attribute__((const)) int perten_to_percent(perten v) { return (((uint64_t)v.value + 1) * 100u) >> perten_bits; }
static inline __attribute__((const)) double perten_to_percentf(perten v) { return ceil((double)v.value * 1000.0f / (double)perten_base) / 10.0f; }
static inline __attribute__((const)) perten perten_from_percent(int v) { return perten{(uint32_t)std::min<uint64_t>((((uint32_t)v) << perten_bits) / 100u, UINT32_MAX)}; }
static inline __attribute__((const)) perten perten_from_percent(uint32_t v) { return perten{(uint32_t)std::min<uint64_t>((v << perten_bits) / 100u, UINT32_MAX)}; }
static inline __attribute__((const)) perten perten_from_percent(double v) { return perten{(uint32_t)((v * perten_base) / 100.0f)}; }
static inline __attribute__((const)) perten perten_from_uint(uint32_t v) { return perten{v << perten_bits}; }
static inline __attribute__((const)) perten perten_from_int(int v) { assert(v >= 0); return perten{((uint32_t)v) << perten_bits}; }
/// Multiplicatively increase a perten value by another perten value. It will not overflow.
static inline __attribute__((const)) perten perten_increase(perten orig, perten mod) { return perten{(uint32_t)std::min<uint64_t>(((uint64_t)orig.value * (mod.value + perten_base)) >> perten_bits, UINT32_MAX)}; }
/// Multiplicatively reduce a perten value by another perten value. It will approach but never reach the base value (ie 1.0 or 100%).
static inline __attribute__((const)) perten perten_reduce(perten orig, perten mod) { return perten{((uint32_t)((uint64_t)orig.value * (perten_base - mod.value)) >> perten_bits)}; }
/// Modify any value by a perten value.
static inline __attribute__((const)) perten perten_apply(perten orig, perten mod) { return perten{(uint32_t)std::min<uint64_t>(((uint64_t)orig.value * (uint64_t)mod.value) >> perten_bits, UINT32_MAX)}; }
/// Reverse a modification to any value by a perten value.
static inline __attribute__((const)) perten perten_apply_reverse(perten orig, perten mod) { return perten{(uint32_t)((uint64_t)orig.value << perten_bits) / mod.value}; }

// -- Lazy conditional perten multiplicative matrix
// This implements a simple cache for a matrix of values, where each column has a conditional and the only output is the cached sum
// of the column values for a given row. The type has to be unsigned. The maximum value of T is reserved for cache maintenance.
template<size_t C, size_t R>
struct lazy_conditional_matrix
{
lazy_conditional_matrix() { enabled.fill(false); cache.fill(perten_max); for (auto& i : matrix) for (auto& j : i) j = perten_full; }

/// return multiplicative sum of all enabled columns
inline perten row(int r) { if (cache[r] == perten_max) calc(r); return cache[r]; }

/// modify one value and dirty its cache line
inline void modify(int c, int r, perten value) { matrix[c][r] = value; cache[r] = perten_max; }

/// modify one condition and dirty relevant cache lines
void toggle(int c, bool v) { if (enabled[c] != v) { enabled[c] = v; for (unsigned r = 0; r < R; r++) if (matrix[c][r] != perten_full) cache[r] = perten_max; } }

inline bool is_enabled(int c) const { return enabled[c]; }

private:
void calc(int r) { cache[r] = perten_full; for (unsigned c = 0; c < C; c++) { if (enabled[c]) cache[r] = perten_multiply(cache[r], matrix[c][r]); } }
std::array<std::array<perten, R>, C> matrix;
std::array<uint_fast8_t, C> enabled;
std::array<perten, R> cache;
};
19 changes: 7 additions & 12 deletions tests/perf_dmath.cpp → tests/perf_perten.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "dmath.h"
#include "perten.h"
#include <assert.h>
#include <stdio.h>
#include <stdint.h>
Expand All @@ -7,7 +7,7 @@
#include <limits>
#include <array>

static uint64_t a = 0;
static perten a = perten_empty;

int main(int argc, char **argv)
{
Expand All @@ -16,20 +16,15 @@ int main(int argc, char **argv)
{
for (int j = 0; j < 128; j++)
{
m.modify(i, j, 140);
m.modify(i, j, perten{140});
}
}
for (int i = 0; i < 128; i++)
{
for (int j = 0; j < 128; j++) a += m.row(j);
for (int j = 0; j < 128; j++) a += m.row(j);
for (int j = 0; j < 128; j++) a += m.row(j);
for (int j = 0; j < 128; j++) perten_apply(a, m.row(j));
for (int j = 0; j < 128; j++) perten_apply(a, m.row(j));
for (int j = 0; j < 128; j++) perten_apply(a, m.row(j));
}

uint64_t sum = 0;
for (int i = 1; i < 50000; i++)
{
sum += isqrt(i);
}
return (int)sum * 0;
return perten_to_int(a);
}
Loading

0 comments on commit b9a0305

Please sign in to comment.