// Package fixed provides deterministic Q32.32 fixed-point arithmetic. // // No floating-point operation appears anywhere in this package. Results must be // bit-identical across architectures and between native and WASM builds, which // is what allows a player's browser to independently replay a round and reach // exactly the same outcome as the server. package fixed import ( "math/bits" "strconv" ) // F is a Q32.32 fixed-point number: an int64 with 32 fractional bits. type F int64 // One is the fixed-point representation of 1.0. const One F = 1 << 32 const fracBits = 32 // FromInt converts a whole number to fixed-point. func FromInt(v int64) F { return F(v << fracBits) } // Int truncates toward negative infinity and returns the whole part. func (a F) Int() int64 { return int64(a) >> fracBits } func (a F) Add(b F) F { return a + b } func (a F) Sub(b F) F { return a - b } // Mul multiplies via a 128-bit intermediate so no precision is lost before the // shift back down. A naive (a*b)>>32 overflows for operands above roughly 2^15. func (a F) Mul(b F) F { neg := false x, y := int64(a), int64(b) if x < 0 { x, neg = -x, !neg } if y < 0 { y, neg = -y, !neg } hi, lo := bits.Mul64(uint64(x), uint64(y)) res := int64(lo>>fracBits | hi<<(64-fracBits)) if neg { res = -res } return F(res) } // Div divides via a 128-bit intermediate for the same reason as Mul. func (a F) Div(b F) F { if b == 0 { panic("fixed: division by zero") } neg := false x, y := int64(a), int64(b) if x < 0 { x, neg = -x, !neg } if y < 0 { y, neg = -y, !neg } hi := uint64(x) >> (64 - fracBits) lo := uint64(x) << fracBits q, _ := bits.Div64(hi, lo, uint64(y)) res := int64(q) if neg { res = -res } return F(res) } // Sqrt returns the fixed-point square root using integer Newton iteration. // It converges in well under the iteration cap for the full int64 range. func Sqrt(a F) F { if a < 0 { panic("fixed: sqrt of negative") } if a == 0 { return 0 } // Initial guess: half the bit length puts us within a factor of two. shift := uint(bits.Len64(uint64(a))+fracBits) / 2 x := F(1) << shift for i := 0; i < 64; i++ { next := (x + a.Div(x)) / 2 if next == x || next == x-1 { x = next break } x = next } // Newton can land one ulp high; step down while the square exceeds the input. for x > 0 && x.Mul(x) > a { x-- } return x } // String renders the value with six fractional digits, using integer math only. func (a F) String() string { neg := a < 0 if neg { a = -a } whole := int64(a) >> fracBits frac := int64(a) & (int64(One) - 1) micros := (frac * 1_000_000) >> fracBits s := strconv.FormatInt(whole, 10) + "." + pad6(micros) if neg { return "-" + s } return s } func pad6(v int64) string { s := strconv.FormatInt(v, 10) for len(s) < 6 { s = "0" + s } return s }