aboutsummaryrefslogtreecommitdiff
path: root/internal/game/prediction_interface_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/prediction_interface_test.go')
-rw-r--r--internal/game/prediction_interface_test.go272
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")
+ }
+}