aboutsummaryrefslogtreecommitdiff
path: root/internal/player
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-07 16:46:32 -0400
committerhistoria <[not public]>2026-07-07 16:46:32 -0400
commit726997d799edc388cd448e978d5bb0755aaa551c (patch)
treeb7aa8dc2dc9d2edcad1c0a4012a28d01d348db69 /internal/player
parentab2597b22ccdb71116992aec78080d9858358d04 (diff)
downloadthehouseoficarus-726997d799edc388cd448e978d5bb0755aaa551c.tar.gz
feat: bitpacked and base64 encoded VisitedRooms in player YAML to keep the file human readable
Diffstat (limited to 'internal/player')
-rw-r--r--internal/player/player.go3
-rw-r--r--internal/player/roomset.go109
-rw-r--r--internal/player/roomset_test.go87
-rw-r--r--internal/player/stats.go7
4 files changed, 199 insertions, 7 deletions
diff --git a/internal/player/player.go b/internal/player/player.go
index 65026e7..fcf274b 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -333,8 +333,7 @@ func New(name string) *Player {
RoomID: 0,
MapSymbols: make(map[int]MapSymbolData),
Stats: PlayerStats{
- RoomsVisited: make(map[int]bool),
- MobKills: make(map[string]int),
+ MobKills: make(map[string]int),
},
}
for _, s := range AllSkills {
diff --git a/internal/player/roomset.go b/internal/player/roomset.go
new file mode 100644
index 0000000..3565bb6
--- /dev/null
+++ b/internal/player/roomset.go
@@ -0,0 +1,109 @@
+package player
+
+import (
+ "encoding/base64"
+ "math/bits"
+
+ "gopkg.in/yaml.v3"
+)
+
+type RoomSet struct {
+ bits []byte
+}
+
+func (rs *RoomSet) Add(roomID int) {
+ if roomID < 0 {
+ return
+ }
+ byteIdx := roomID / 8
+ if byteIdx >= len(rs.bits) {
+ nb := make([]byte, byteIdx+1)
+ copy(nb, rs.bits)
+ rs.bits = nb
+ }
+ rs.bits[byteIdx] |= 1 << (roomID % 8)
+}
+
+func (rs *RoomSet) Has(roomID int) bool {
+ if roomID < 0 {
+ return false
+ }
+ byteIdx := roomID / 8
+ return byteIdx < len(rs.bits) && rs.bits[byteIdx]&(1<<(roomID%8)) != 0
+}
+
+func (rs *RoomSet) Len() int {
+ n := 0
+ for _, b := range rs.bits {
+ n += bits.OnesCount8(b)
+ }
+ return n
+}
+
+func (rs RoomSet) IsZero() bool {
+ return len(rs.bits) == 0
+}
+
+func (rs *RoomSet) Keys() []int {
+ var keys []int
+ for byteIdx, b := range rs.bits {
+ if b == 0 {
+ continue
+ }
+ base := byteIdx * 8
+ for bitIdx := 0; bitIdx < 8; bitIdx++ {
+ if b&(1<<bitIdx) != 0 {
+ keys = append(keys, base+bitIdx)
+ }
+ }
+ }
+ return keys
+}
+
+func (rs *RoomSet) Rekey(idMap map[int]int) (RoomSet, bool) {
+ var next RoomSet
+ changed := false
+ for _, k := range rs.Keys() {
+ newK, ok := idMap[k]
+ if !ok {
+ newK = k
+ } else if newK != k {
+ changed = true
+ }
+ next.Add(newK)
+ }
+ return next, changed
+}
+
+func NewRoomSetFrom(ids ...int) RoomSet {
+ var rs RoomSet
+ for _, id := range ids {
+ rs.Add(id)
+ }
+ return rs
+}
+
+func (rs RoomSet) MarshalYAML() (any, error) {
+ return base64.StdEncoding.EncodeToString(rs.bits), nil
+}
+
+func (rs *RoomSet) UnmarshalYAML(value *yaml.Node) error {
+ if value.Kind == yaml.ScalarNode && (value.Tag == "!!null" || value.Value == "null") {
+ rs.bits = nil
+ return nil
+ }
+ var s string
+ if err := value.Decode(&s); err != nil {
+ return err
+ }
+ if s == "" {
+ rs.bits = nil
+ return nil
+ }
+ b, err := base64.StdEncoding.DecodeString(s)
+ if err != nil {
+ return err
+ }
+ rs.bits = b
+ return nil
+}
diff --git a/internal/player/roomset_test.go b/internal/player/roomset_test.go
new file mode 100644
index 0000000..2b6b9cb
--- /dev/null
+++ b/internal/player/roomset_test.go
@@ -0,0 +1,87 @@
+package player
+
+import (
+ "strings"
+ "testing"
+
+ "gopkg.in/yaml.v3"
+)
+
+func TestRoomSetYAMLRoundTrip(t *testing.T) {
+ stats := PlayerStats{
+ RoomsVisited: NewRoomSetFrom(1, 5, 300),
+ MobKills: map[string]int{"rat": 3},
+ TotalKills: 3,
+ Deaths: 0,
+ }
+ data, err := yaml.Marshal(stats)
+ if err != nil {
+ t.Fatalf("marshal: %v", err)
+ }
+ out := string(data)
+ if !strings.Contains(out, "rooms_visited:") {
+ t.Fatalf("expected rooms_visited key in output:\n%s", out)
+ }
+
+ var loaded PlayerStats
+ if err := yaml.Unmarshal(data, &loaded); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if loaded.RoomsVisited.Len() != 3 {
+ t.Errorf("Len = %d, want 3", loaded.RoomsVisited.Len())
+ }
+ for _, id := range []int{1, 5, 300} {
+ if !loaded.RoomsVisited.Has(id) {
+ t.Errorf("expected Has(%d) == true", id)
+ }
+ }
+ if loaded.RoomsVisited.Has(2) {
+ t.Error("expected Has(2) == false")
+ }
+}
+
+func TestRoomSetOmitEmpty(t *testing.T) {
+ stats := PlayerStats{}
+ data, err := yaml.Marshal(stats)
+ if err != nil {
+ t.Fatalf("marshal: %v", err)
+ }
+ out := string(data)
+ if strings.Contains(out, "rooms_visited:") {
+ t.Errorf("expected rooms_visited to be absent for empty set:\n%s", out)
+ }
+
+ var loaded PlayerStats
+ if err := yaml.Unmarshal(data, &loaded); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if loaded.RoomsVisited.Len() != 0 {
+ t.Errorf("Len = %d, want 0", loaded.RoomsVisited.Len())
+ }
+}
+
+func TestRoomSetIsZero(t *testing.T) {
+ var zero RoomSet
+ if !zero.IsZero() {
+ t.Error("zero value should be IsZero")
+ }
+ visited := NewRoomSetFrom(1)
+ if visited.IsZero() {
+ t.Error("non-empty set should not be IsZero")
+ }
+}
+
+func TestRoomSetMarshalEmpty(t *testing.T) {
+ var rs RoomSet
+ v, err := rs.MarshalYAML()
+ if err != nil {
+ t.Fatalf("MarshalYAML: %v", err)
+ }
+ s, ok := v.(string)
+ if !ok {
+ t.Fatalf("MarshalYAML returned %T, want string", v)
+ }
+ if s != "" {
+ t.Errorf("MarshalYAML empty = %q, want empty string", s)
+ }
+}
diff --git a/internal/player/stats.go b/internal/player/stats.go
index 82b741c..2b63558 100644
--- a/internal/player/stats.go
+++ b/internal/player/stats.go
@@ -1,17 +1,14 @@
package player
type PlayerStats struct {
- RoomsVisited map[int]bool `yaml:"rooms_visited,omitempty"`
+ RoomsVisited RoomSet `yaml:"rooms_visited,omitempty"`
MobKills map[string]int `yaml:"mob_kills,omitempty"`
TotalKills int `yaml:"total_kills"`
Deaths int `yaml:"deaths"`
}
func (s *PlayerStats) RecordRoomVisit(roomID int) {
- if s.RoomsVisited == nil {
- s.RoomsVisited = make(map[int]bool)
- }
- s.RoomsVisited[roomID] = true
+ s.RoomsVisited.Add(roomID)
}
func (s *PlayerStats) RecordMobKill(mobDefID string) {