Skip to content
Snippets Groups Projects
Commit 730f99b2 authored by Hans Moog's avatar Hans Moog
Browse files

Feat: finished final mana calculator specs / API

parent 4ff72850
No related branches found
No related tags found
No related merge requests found
...@@ -25,10 +25,10 @@ func (balance *Balance) GetValue() uint64 { ...@@ -25,10 +25,10 @@ func (balance *Balance) GetValue() uint64 {
} }
func (balance *Balance) AddTransfer(movedCoins uint64, receivedTime uint64, spentTime uint64) { func (balance *Balance) AddTransfer(movedCoins uint64, receivedTime uint64, spentTime uint64) {
gainedMana, lastErosion, _ := balance.calculator.ManaOfTransferDiscrete(movedCoins, receivedTime, spentTime) gainedMana, _ := balance.calculator.GenerateMana(movedCoins, spentTime-receivedTime)
if lastErosion >= balance.lastErosion { if spentTime >= balance.lastErosion {
balance.Erode(lastErosion) balance.Erode(spentTime)
} else { } else {
fmt.Println("empty") fmt.Println("empty")
// revert old actions // revert old actions
...@@ -41,7 +41,7 @@ func (balance *Balance) AddTransfer(movedCoins uint64, receivedTime uint64, spen ...@@ -41,7 +41,7 @@ func (balance *Balance) AddTransfer(movedCoins uint64, receivedTime uint64, spen
func (balance *Balance) Erode(erosionTime uint64) { func (balance *Balance) Erode(erosionTime uint64) {
if balance.lastErosion <= erosionTime { if balance.lastErosion <= erosionTime {
balance.currentBalance, balance.lastErosion, _ = balance.calculator.ErodedManaDiscrete(balance.currentBalance, balance.lastErosion, erosionTime) balance.currentBalance, _ = balance.calculator.ErodeMana(balance.currentBalance, erosionTime-balance.lastErosion)
} else { } else {
fmt.Println("empty") fmt.Println("empty")
// revert old erosions // revert old erosions
......
...@@ -4,135 +4,60 @@ import ( ...@@ -4,135 +4,60 @@ import (
"math" "math"
) )
// A calculator that can be used to calculate the changes of mana due to erosion or mana generation.
type Calculator struct { type Calculator struct {
decayInterval uint64 decayInterval float64
decayRate float64 decayRate float64
coinsPerMana uint64 options *CalculatorOptions
decayFactor float64 tokenSupplyScalefactor float64
manaScaleFactor float64
} }
func NewCalculator(decayInterval uint64, decayRate float64, coinsPerMana uint64) (result *Calculator) { // Creates a new calculator that can be used to calculate the changes of mana due to erosion or mana generation.
result = &Calculator{ func NewCalculator(decayInterval float64, decayRate float64, optionalOptions ...CalculatorOption) *Calculator {
return &Calculator{
// store key settings
decayInterval: decayInterval, decayInterval: decayInterval,
decayRate: decayRate, decayRate: decayRate,
coinsPerMana: coinsPerMana,
decayFactor: 1 - decayRate,
}
// make mana reach exactly the token supply // configure optional parameters
result.manaScaleFactor = result.decayRate / result.decayFactor options: DEFAULT_OPTIONS.Override(optionalOptions...),
return // derive important factors ...
} // ... make mana reach exactly the token supply as it's max value (n coins => n mana)
tokenSupplyScalefactor: decayRate / (1 - decayRate),
func (calculator *Calculator) ManaOfTransferDiscrete(movedCoins uint64, receivedTime uint64, spentTime uint64) (result uint64, lastErosion uint64, roundingError float64) {
if spentTime <= receivedTime {
return 0, 0, 0
} }
baseMana := movedCoins / calculator.coinsPerMana
scaleFactor := 1 - calculator.decayRate
erosionIntervals := spentTime/calculator.decayInterval - receivedTime/calculator.decayInterval
var totalManaReceived float64
switch true {
// no decay intervals
case erosionIntervals == 0:
lastIntervalDuration := spentTime - receivedTime
if lastIntervalDuration != 0 {
totalManaReceived += float64(CoinTimeDestroyed(baseMana, lastIntervalDuration))
}
lastErosion = 0
// only 1 decay interval
case erosionIntervals == 1:
firstIntervalDuration := calculator.decayInterval - receivedTime%calculator.decayInterval
gainsInFirstInterval := float64(CoinTimeDestroyed(baseMana, firstIntervalDuration)) * math.Pow(scaleFactor, float64(erosionIntervals))
totalManaReceived += gainsInFirstInterval
lastIntervalDuration := spentTime % calculator.decayInterval
if lastIntervalDuration != 0 {
totalManaReceived += float64(CoinTimeDestroyed(baseMana, lastIntervalDuration))
}
lastErosion = spentTime - lastIntervalDuration
// multiple decay intervals
default:
firstIntervalDuration := calculator.decayInterval - receivedTime%calculator.decayInterval
gainsInFirstInterval := float64(CoinTimeDestroyed(baseMana, firstIntervalDuration)) * math.Pow(scaleFactor, float64(erosionIntervals))
totalManaReceived += gainsInFirstInterval
gainsInConsecutiveIntervals := float64(CoinTimeDestroyed(baseMana, calculator.decayInterval)) * scaleFactor * (1 - math.Pow(scaleFactor, float64(erosionIntervals-1))) / (1 - scaleFactor)
totalManaReceived += gainsInConsecutiveIntervals
lastIntervalDuration := spentTime % calculator.decayInterval
if lastIntervalDuration != 0 {
totalManaReceived += float64(CoinTimeDestroyed(baseMana, lastIntervalDuration))
}
lastErosion = spentTime - lastIntervalDuration
}
result = uint64(totalManaReceived)
roundingError = totalManaReceived - float64(result)
return
} }
func (calculator *Calculator) ManaOfTransferContinuous(movedCoins uint64, heldTime uint64) (result uint64, roundingError float64) { // Returns the amount of mana that was generated by holding the given amount of coins for the given time.
func (calculator *Calculator) GenerateMana(coins uint64, heldTime uint64) (result uint64, roundingError float64) {
// calculate results
relativeDecayTime := float64(heldTime) / float64(calculator.decayInterval) relativeDecayTime := float64(heldTime) / float64(calculator.decayInterval)
erosionFactor := (1 - math.Pow(1-calculator.decayRate, float64(relativeDecayTime+1)) - calculator.decayRate) / calculator.decayRate
gainedMana := float64(coins) * calculator.options.ManaScaleFactor * calculator.tokenSupplyScalefactor * erosionFactor
erosionFactor := (1-math.Pow(calculator.decayFactor, float64(relativeDecayTime+1)))/calculator.decayRate - 1 // assign rounded results & determine roundingErrors
gainedMana := float64(movedCoins) * calculator.manaScaleFactor * erosionFactor
result = uint64(math.Round(gainedMana)) result = uint64(math.Round(gainedMana))
roundingError = gainedMana - float64(result) roundingError = gainedMana - float64(result)
return return
} }
func (calculator *Calculator) ManaOfTransferContinuous1(movedCoins uint64, heldTime uint64) (result uint64, roundingError float64) { // Returns the amount of mana that is left after the erosion of the given amount for the given time.
gainsInConsecutiveIntervals := (float64(movedCoins) / calculator.decayRate) * (1 - math.Pow(math.E, -calculator.decayRate*float64(heldTime))) func (calculator *Calculator) ErodeMana(mana uint64, decayTime uint64) (result uint64, roundingError float64) {
// if no time has passed -> return unchanged values
result = uint64(gainsInConsecutiveIntervals) if decayTime == 0 {
roundingError = gainsInConsecutiveIntervals - float64(result)
return
}
func (calculator *Calculator) ErodedManaContinuous(mana uint64, erosionTime uint64) (result uint64, roundingError float64) {
if erosionTime == 0 {
result = mana result = mana
roundingError = 0
return return
} }
// calculate results
growthFactor := math.Log(1-calculator.decayRate) / float64(calculator.decayInterval) growthFactor := math.Log(1-calculator.decayRate) / float64(calculator.decayInterval)
erodedMana := float64(mana) * math.Pow(math.E, growthFactor*float64(erosionTime)) erodedMana := float64(mana) * math.Pow(math.E, growthFactor*float64(decayTime))
result = uint64(erodedMana) // assign rounded results & determine roundingErrors
result = uint64(math.Round(erodedMana))
roundingError = erodedMana - float64(result) roundingError = erodedMana - float64(result)
return return
} }
func (calculator *Calculator) ErodedManaDiscrete(mana uint64, erosionStartTime uint64, erosionEndTime uint64) (result uint64, lastErosion uint64, roundingError float64) {
if erosionStartTime > erosionEndTime {
panic("negative erosion duration")
}
erosionIntervals := erosionEndTime/calculator.decayInterval - erosionStartTime/calculator.decayInterval
erodedValue := math.Pow(float64(1-calculator.decayRate), float64(erosionIntervals)) * float64(mana)
result = uint64(erodedValue)
lastErosion = erosionEndTime
roundingError = erodedValue - float64(result)
return
}
package mana
// region default options //////////////////////////////////////////////////////////////////////////////////////////////
var DEFAULT_OPTIONS = &CalculatorOptions{
ManaScaleFactor: 1,
}
// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////////
// region calculator options ///////////////////////////////////////////////////////////////////////////////////////////
func ManaScaleFactor(manaScaleFactor float64) CalculatorOption {
return func(args *CalculatorOptions) {
args.ManaScaleFactor = manaScaleFactor
}
}
// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////////
// region type definitions /////////////////////////////////////////////////////////////////////////////////////////////
type CalculatorOptions struct {
ManaScaleFactor float64
}
func (options CalculatorOptions) Override(optionalOptions ...CalculatorOption) *CalculatorOptions {
result := &options
for _, option := range optionalOptions {
option(result)
}
return result
}
type CalculatorOption func(*CalculatorOptions)
// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////////
package mana
func CoinTimeDestroyed(transferredValue uint64, parkedTime uint64) uint64 {
return transferredValue * parkedTime
}
const (
DECAY_INTERVAL = 10
DECAY_RATE = 0.01
COINS_PER_MANA = 10
)
package mana package mana
import ( import (
"fmt"
"math"
"testing" "testing"
"github.com/magiconair/properties/assert" "github.com/magiconair/properties/assert"
) )
func BenchmarkCalculator_ManaOfTransferContinuous(b *testing.B) { func BenchmarkCalculator_GenerateMana(b *testing.B) {
calculator := NewCalculator(10, 0.1, 1) calculator := NewCalculator(10, 0.1)
b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
calculator.ManaOfTransferContinuous(10000000, 100000000000) calculator.GenerateMana(1000000, 100000000000)
} }
} }
func TestBalance_Erode(t *testing.T) { func TestCalculator_GenerateMana(t *testing.T) {
calculator := NewCalculator(50, 0.1, 9) calculator := NewCalculator(500, 0.1, ManaScaleFactor(2))
balance := NewBalance(calculator)
calculator1 := NewCalculator(5000, 0.2, 1)
fmt.Println("===")
fmt.Println(calculator1.ManaOfTransferContinuous(1000, 5000000))
fmt.Println(calculator.ManaOfTransferDiscrete(1000, 0, 50))
balance.AddTransfer(500, 0, 10)
assert.Equal(t, balance.GetValue(), uint64(450)) generatedMana, _ := calculator.GenerateMana(1000, 0)
assert.Equal(t, generatedMana, uint64(0))
balance.Erode(20) generatedMana, _ = calculator.GenerateMana(1000, 500)
assert.Equal(t, generatedMana, uint64(200))
assert.Equal(t, balance.GetValue(), uint64(405)) generatedMana, _ = calculator.GenerateMana(1000, 5000000)
assert.Equal(t, generatedMana, uint64(2000))
balance.Erode(40)
assert.Equal(t, balance.GetValue(), uint64(328))
} }
func calcManaContinuous(calculator *Calculator, coins uint64, timeHeld uint64) (result uint64, roundingError float64) { func TestCalculator_ManaSymmetry(t *testing.T) {
scaleFactor := 1 - math.Pow(1-calculator.decayRate, float64(timeHeld)/float64(calculator.decayInterval)) calculator := NewCalculator(500, 0.1, ManaScaleFactor(2))
fmt.Println(scaleFactor)
erodedGains := float64(coins) * float64(calculator.decayInterval) * scaleFactor
result = uint64(erodedGains)
roundingError = erodedGains - float64(result)
return
}
func TestCalculator_ManaOfTransfer(t *testing.T) {
manaCalculator := NewCalculator(10, 0.1, 10)
var mana, lastErosion uint64
var roundingError float64
mana, lastErosion, _ = manaCalculator.ManaOfTransferDiscrete(49, 0, 0)
assert.Equal(t, mana, uint64(0))
assert.Equal(t, lastErosion, uint64(0))
fmt.Println(calcManaContinuous(manaCalculator, 50, 10))
fmt.Println(manaCalculator.ManaOfTransferDiscrete(50, 0, 10))
fmt.Println(calcManaContinuous(manaCalculator, 50, 20))
fmt.Println(manaCalculator.ManaOfTransferDiscrete(50, 0, 20))
mana, lastErosion, _ = manaCalculator.ManaOfTransferDiscrete(49, 0, 1) // 1st case: generate mana by spending two times
assert.Equal(t, mana, uint64(4)) generatedManaStep1, _ := calculator.GenerateMana(1000, 500)
assert.Equal(t, lastErosion, uint64(0)) generatedManaStep2, _ := calculator.GenerateMana(1000, 500)
generatedManaStep3, _ := calculator.GenerateMana(1000, 500)
mana, lastErosion, _ = manaCalculator.ManaOfTransferDiscrete(49, 0, 10) // the first "realized" mana part starts decaying while the coins of the 2nd spend are gaining weight again
assert.Equal(t, mana, uint64(36)) erodedMana1, _ := calculator.ErodeMana(generatedManaStep1, 1000)
assert.Equal(t, lastErosion, uint64(10)) erodedMana2, _ := calculator.ErodeMana(generatedManaStep2, 500)
mana, lastErosion, _ = manaCalculator.ManaOfTransferDiscrete(50, 0, 31) // 2nd case: generate mana by spending only once
assert.Equal(t, mana, uint64(101)) generatedManaWithoutSpends, _ := calculator.GenerateMana(1000, 1500)
assert.Equal(t, lastErosion, uint64(30))
fmt.Println(roundingError) // the two mana values should be equal
assert.Equal(t, generatedManaWithoutSpends, erodedMana1+erodedMana2+generatedManaStep3)
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment