diff options
| author | historia <[not public]> | 2026-07-07 16:46:32 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-07 16:46:32 -0400 |
| commit | 726997d799edc388cd448e978d5bb0755aaa551c (patch) | |
| tree | b7aa8dc2dc9d2edcad1c0a4012a28d01d348db69 /internal/player | |
| parent | ab2597b22ccdb71116992aec78080d9858358d04 (diff) | |
| download | thehouseoficarus-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.go | 3 | ||||
| -rw-r--r-- | internal/player/roomset.go | 109 | ||||
| -rw-r--r-- | internal/player/roomset_test.go | 87 | ||||
| -rw-r--r-- | internal/player/stats.go | 7 |
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) { |
