points.h (3613B)
1 #pragma once 2 #include <Arduino.h> 3 #include <Preferences.h> 4 #include <string.h> 5 #include "config.h" 6 7 // ───────────────────────────────────────────────────────────────────────── 8 // Points ledger — per-friend attribution of your proximity score. 9 // 10 // The global points tally (main.cpp) counts one point per minute spent with 11 // crew close enough to clear the RSSI gate. This ledger records WHO you shared 12 // those minutes with: each earning tick is credited to EVERY in-gate friend. 13 // So a friend's seconds = how many of your social minutes they were part of 14 // (each ≤ the tally, but they overlap — a minute spent in a group of three 15 // credits all three). "Best Friends" ranks the top sharers, most first. 16 // 17 // Bounded (crew is small) and main-loop-only — no locking needed, unlike the 18 // peer table. Persisted to NVS as a blob so a reboot mid-con keeps the ranking. 19 // ───────────────────────────────────────────────────────────────────────── 20 21 #ifndef MAX_POINT_FRIENDS 22 #define MAX_POINT_FRIENDS 24 // > expected crew size; evicts the smallest 23 #endif 24 25 struct PointFriend { 26 char handle[HANDLE_MAX_LEN + 1]; 27 uint32_t seconds; // gated proximity time credited to this friend 28 }; 29 30 class PointsLedger { 31 public: 32 // Credit `secs` of proximity to `handle` (the closest crew this tick). 33 void credit(const char* handle, uint32_t secs) { 34 if (!secs || !handle || !handle[0]) return; 35 int idx = find(handle); 36 if (idx < 0) { 37 idx = (_n < MAX_POINT_FRIENDS) ? _n++ : smallest(); 38 memset(&_f[idx], 0, sizeof(PointFriend)); 39 strncpy(_f[idx].handle, handle, HANDLE_MAX_LEN); 40 _f[idx].handle[HANDLE_MAX_LEN] = '\0'; 41 } 42 _f[idx].seconds += secs; 43 } 44 45 uint32_t total() const { 46 uint32_t s = 0; 47 for (int i = 0; i < _n; i++) s += _f[i].seconds; 48 return s; 49 } 50 51 // Copy entries into `out` sorted by contribution (most first); returns the 52 // number copied (≤ max). `out` must hold up to MAX_POINT_FRIENDS. 53 int top(PointFriend* out, int max) const { 54 for (int i = 0; i < _n; i++) out[i] = _f[i]; 55 for (int i = 1; i < _n; i++) { // insertion sort, n is tiny 56 PointFriend key = out[i]; 57 int j = i - 1; 58 while (j >= 0 && out[j].seconds < key.seconds) { out[j + 1] = out[j]; j--; } 59 out[j + 1] = key; 60 } 61 return (_n < max) ? _n : max; 62 } 63 64 void save(Preferences& p) const { 65 p.putUInt("pts_n", (uint32_t)_n); 66 if (_n > 0) p.putBytes("pts", _f, (size_t)_n * sizeof(PointFriend)); 67 } 68 69 void load(Preferences& p) { 70 memset(_f, 0, sizeof(_f)); 71 uint32_t n = p.getUInt("pts_n", 0); 72 if (n == 0 || n > MAX_POINT_FRIENDS) { _n = 0; return; } 73 size_t want = (size_t)n * sizeof(PointFriend); 74 size_t got = p.getBytes("pts", _f, want); 75 _n = (got == want) ? (int)n : 0; // partial/missing blob → start clean 76 for (int i = 0; i < _n; i++) _f[i].handle[HANDLE_MAX_LEN] = '\0'; 77 } 78 79 private: 80 PointFriend _f[MAX_POINT_FRIENDS]; 81 int _n = 0; 82 83 int find(const char* handle) const { 84 for (int i = 0; i < _n; i++) 85 if (strncmp(_f[i].handle, handle, HANDLE_MAX_LEN) == 0) return i; 86 return -1; 87 } 88 89 int smallest() const { 90 int idx = 0; 91 for (int i = 1; i < _n; i++) 92 if (_f[i].seconds < _f[idx].seconds) idx = i; 93 return idx; 94 } 95 };