diff options
| author | historia <[not public]> | 2026-10-05 06:03:49 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-10-05 06:03:49 -0400 |
| commit | c84a9944356f590d90d819d652bdf1349ebacd40 (patch) | |
| tree | af1ee15992e141fb3eb6f26cac02cf06306d4982 /internal/game/prediction_interface_test.go | |
| parent | 8b78aa4cd930fde145959704ba7bdc0e57465b9b (diff) | |
| download | thehouseoficarus-c84a9944356f590d90d819d652bdf1349ebacd40.tar.gz | |
feat: prediction exchange with live polymarket data
Diffstat (limited to 'internal/game/prediction_interface_test.go')
| -rw-r--r-- | internal/game/prediction_interface_test.go | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/internal/game/prediction_interface_test.go b/internal/game/prediction_interface_test.go new file mode 100644 index 0000000..e6a9dc9 --- /dev/null +++ b/internal/game/prediction_interface_test.go @@ -0,0 +1,272 @@ +package game + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "regexp" + "strconv" + "strings" + "sync/atomic" + "testing" + "time" + "unicode/utf8" + + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/prediction" +) + +func predictionTestServer(t *testing.T, g *Game, m prediction.Market) { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/markets": + json.NewEncoder(w).Encode([]prediction.Market{m}) + case "/markets/1": + json.NewEncoder(w).Encode(m) + case "/prices-history": + w.Write([]byte(`{"history":[{"t":100,"p":0.3},{"t":200,"p":0.6}]}`)) + default: + w.WriteHeader(404) + } + })) + t.Cleanup(server.Close) + g.PredictionClient = prediction.NewClient() + g.PredictionClient.Gamma = server.URL + g.PredictionClient.CLOB = server.URL +} +func TestPredictionModalCommandsAndEntryAliases(t *testing.T) { + for _, alias := range []string{"predictions", "market", "counter", "exchange", "prediction market counter"} { + t.Run(alias, func(t *testing.T) { + g, s, conn, m := predictionFixture(t) + predictionTestServer(t, g, m) + if _, ok := commandRegistry["predictions"]; ok { + t.Fatal("standalone predictions command survives") + } + if _, ok := commandRegistry["predict"]; ok { + t.Fatal("standalone predict alias survives") + } + if g.predictionInputOwned(s, "active") { + t.Fatal("counter verbs escaped session scope") + } + g.doUse(s, alias) + if !s.PredictionActive || s.PredictionScreen != "list" { + t.Fatalf("not entered: %s", conn.data) + } + g.handleGameCommand(s, "1") + if s.PredictionSelected != "1" || s.PredictionScreen != "view" { + t.Fatalf("numeric selection failed: %s", conn.data) + } + g.handleGameCommand(s, "buy 10 9") + if s.PredictionScreen != "view" || s.PredictionSelected != "1" || len(s.Player.PredictionContracts) != 0 { + t.Fatal("rejected order lost selection or changed ledger") + } + g.handleGameCommand(s, "buy 10 1") + if len(s.Player.PredictionContracts) != 1 || s.Player.Credits != 9570 { + t.Fatalf("context buy failed: %s", conn.data) + } + if s.PredictionScreen != "view" { + t.Fatal("purchase lost the selected market") + } + g.handleGameCommand(s, "active") + g.handleGameCommand(s, "1") + if s.PredictionScreen != "view" { + t.Fatal("portfolio number did not open holding") + } + g.handleGameCommand(s, "active") + g.handleGameCommand(s, "sell 1") + if s.Player.PredictionContracts[0].Status != "sold" { + t.Fatal("context sale failed") + } + g.handleGameCommand(s, "stop") + if s.PredictionActive || g.predictionInputOwned(s, "active") { + t.Fatal("stop did not leave mode") + } + }) + } +} +func TestPredictionScreenGlyphsColorAndWidths(t *testing.T) { + for _, unicode := range []bool{true, false} { + t.Run(map[bool]string{true: "unicode", false: "ascii"}[unicode], func(t *testing.T) { + g, s, conn, m := predictionFixture(t) + s.Player.Options["unicode"] = unicode + s.Player.Options["color"] = "xterm256" + s.Player.Options["casino_hints"] = "on" + g.predictionList(s, []prediction.Market{m}, 1, "") + m.Prices = `["0.60","0.40"]` + g.predictionList(s, []prediction.Market{m}, 1, "") + g.predictionView(s, m, []prediction.Point{{T: 100, P: .3}, {T: 200, P: .6}}) + text := string(conn.data) + if !strings.Contains(text, "\x1b[") { + t.Fatal("no color") + } + text = regexp.MustCompile(`\x1b\[[0-9;]*m`).ReplaceAllString(text, "") + if strings.Contains(text, "predictions buy") || strings.Contains(text, "predictions board") { + t.Fatal("old command hints") + } + for _, want := range []string{"Available", "buy", "ODDS HISTORY", "Period move:", "+30.0", "Market", "Odds", "Outcome"} { + if !strings.Contains(text, want) { + t.Fatalf("missing %q", want) + } + } + if unicode { + for _, glyph := range []string{"╔", "║", "█", "░", "↑", "─"} { + if !strings.Contains(text, glyph) { + t.Fatalf("missing glyph %s", glyph) + } + } + } else { + for _, r := range text { + if r > 127 { + t.Fatalf("ASCII mode emitted %U", r) + } + } + if !strings.Contains(text, "#") || !strings.Contains(text, "^") { + t.Fatal("ASCII graphics missing") + } + } + for _, line := range strings.Split(text, "\n") { + if width := color.VisibleLen(strings.TrimSuffix(line, "\r")); width > 80 { + t.Fatalf("line exceeds terminal (%d): %s", width, line) + } + } + if !utf8.ValidString(text) { + t.Fatal("broken UTF-8") + } + }) + } +} +func TestPredictionHintPreferencesAndWorldCommands(t *testing.T) { + g, s, conn, m := predictionFixture(t) + predictionTestServer(t, g, m) + s.Player.Options["casino_hints"] = "off" + g.doUse(s, "market") + if strings.Contains(string(conn.data), "view N") { + t.Fatal("ignored hints off") + } + conn.data = nil + g.handleGameCommand(s, "what") + if !strings.Contains(string(conn.data), "view N") { + t.Fatal("global what did not show the casino menu") + } + if g.predictionInputOwned(s, "what") || g.predictionInputOwned(s, "verbs") { + t.Fatal("global commands claimed by counter") + } + if strings.Contains(string(conn.data), "YOUR NEXT MOVE") || strings.Contains(string(conn.data), "what /") { + t.Fatal("custom hint footer survived") + } + if g.predictionInputOwned(s, "north") || g.predictionInputOwned(s, "inventory") { + t.Fatal("world command intercepted") + } + epoch := s.PredictionEpoch + g.handlePredictionInput(s, "use", []string{"market"}) + if !s.PredictionActive || s.PredictionEpoch != epoch { + t.Fatal("reusing counter invalidates pending operation") + } + g.handlePredictionInput(s, "play", []string{"roulette"}) + if s.PredictionActive { + t.Fatal("another casino game did not release mode") + } +} +func TestPredictionStopCancelsPendingTrade(t *testing.T) { + g, s, _, m := predictionFixture(t) + entered, canceled := make(chan struct{}), make(chan struct{}) + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls.Add(1) + close(entered) + <-r.Context().Done() + close(canceled) + })) + defer server.Close() + g.PredictionClient = prediction.NewClient() + g.PredictionClient.Gamma = server.URL + g.owner = newOwner() + defer g.owner.stop() + g.loggedInChars = map[string]*net.Session{"alice": s} + s.PredictionActive = true + s.PredictionSelected = m.ID + s.PredictionScreen = "view" + g.run(func() { g.executePredictions(s, []string{"buy", "10", "1"}, "") }) + select { + case <-entered: + case <-time.After(time.Second): + t.Fatal("request not started") + } + g.run(func() { g.executeCommand(s, "stop", nil, "stop") }) + select { + case <-canceled: + case <-time.After(time.Second): + t.Fatal("stop did not cancel network request") + } + g.run(func() { + if s.PredictionActive || s.PredictionBusy || s.Player.Credits != 10000 || len(s.Player.PredictionContracts) != 0 { + t.Fatal("stopped trade changed state") + } + }) + if calls.Load() != 1 { + t.Fatal("unexpected requests") + } +} +func TestPredictionRoomEntryClearsModeAndCancelsRequest(t *testing.T) { + _, s, _, _ := predictionFixture(t) + canceled := false + s.PredictionActive = true + s.PredictionBusy = true + s.PredictionSelected = "1" + s.PredictionCancel = func() { canceled = true } + hub := net.NewHub() + hub.Add(s) + hub.EnterRoom(s, 2) + if s.PredictionActive || s.PredictionBusy || s.PredictionSelected != "" || !canceled { + t.Fatal("room change retained counter mode") + } +} + +func TestPredictionBackKeepsSearchAndPage(t *testing.T) { + g, s, _, m := predictionFixture(t) + markets := make([]prediction.Market, 21) + for i := range markets { + markets[i] = m + markets[i].ID = strconv.Itoa(i + 1) + markets[i].Question = "Sports market " + markets[i].ID + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/markets": + json.NewEncoder(w).Encode(markets) + case "/prices-history": + w.Write([]byte(`{"history":[]}`)) + default: + n, _ := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/markets/")) + if n < 1 || n > len(markets) { + w.WriteHeader(404) + return + } + json.NewEncoder(w).Encode(markets[n-1]) + } + })) + defer server.Close() + g.PredictionClient = prediction.NewClient() + g.PredictionClient.Gamma = server.URL + g.PredictionClient.CLOB = server.URL + g.doUse(s, "market") + g.handleGameCommand(s, "find sports") + g.handleGameCommand(s, "next") + if s.PredictionPage != 2 { + t.Fatal("next did not turn page") + } + g.handleGameCommand(s, "5") + if s.PredictionSelected != "5" { + t.Fatal("wrong selected market") + } + g.handleGameCommand(s, "back") + if s.PredictionScreen != "list" || s.PredictionPage != 2 || s.PredictionSearch != "sports" { + t.Fatal("back lost browsing position") + } + g.handleGameCommand(s, "markets") + if s.PredictionPage != 1 || s.PredictionSearch != "" { + t.Fatal("markets did not reset selection") + } +} |
