package main import ( "context" "fmt" "log" "math/rand" "sync" "time" "github.com/ascii8/nakama-go" ) const ( ServerKey = "defaultkey" URL = "http://127.0.0.1:7350" // Assuming default local Nakama ) func main() { rand.Seed(time.Now().UnixNano()) fmt.Println("=== Starting Matchmaker Tests ===") // Test 1: Free vs Free (Should Match) fmt.Println("\n[Test 1] Free vs Free (Expect Success)") if err := runMatchTest("minesweeper_free", "minesweeper_free", true); err != nil { log.Printf("Test 1 Failed: %v", err) } else { fmt.Println("✅ Test 1 Passed") } // Test 2: Paid vs Paid (Should Match) fmt.Println("\n[Test 2] Paid vs Paid (Expect Success)") if err := runMatchTest("minesweeper", "minesweeper", true); err != nil { log.Printf("Test 2 Failed: %v", err) } else { fmt.Println("✅ Test 2 Passed") } // Test 3: Free vs Paid (Should NOT Match) fmt.Println("\n[Test 3] Mixed Properties (Wide Query) (Expect Rejection by Hook)") if err := runMixedTest(); err != nil { fmt.Println("✅ Test 3 Passed (No Match formed/accepted)") } else { log.Printf("❌ Test 3 Failed: Mixed match was created!") } // Test 4: Missing Property (Should Fail) fmt.Println("\n[Test 4] Missing Property (Expect Rejection)") if err := runMissingPropertyTest(); err != nil { fmt.Println("✅ Test 4 Passed (Match rejected/failed as expected)") } else { log.Printf("❌ Test 4 Failed: Match created without game_type property!") } } func getClient() *nakama.Client { return nakama.New( nakama.WithServerKey(ServerKey), nakama.WithURL(URL), ) } func runMatchTest(type1, type2 string, expectSuccess bool) error { var wg sync.WaitGroup wg.Add(2) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() errChan := make(chan error, 2) matchChan := make(chan string, 2) // Use wildcard query "*" to force potential match, relying on PROPERTY validation by server hook // OR use specific query +properties.game_type:X to be realistic. // Since we want to test if "3 free 1 paid" match, let's use specific queries first to ensure basic flows work. // Actually, the user's issue was "3 free 1 paid matched". // If they used specific queries, they wouldn't match at all in Matchmaker. // The fact they matched implies they might be using broad queries or the client logic has a fallback query. // But let's assume specific queries for "Success" tests and "*" for "Mixed/Failure" tests. q1 := fmt.Sprintf("+properties.game_type:%s", type1) q2 := fmt.Sprintf("+properties.game_type:%s", type2) go runClient(ctx, &wg, type1, q1, errChan, matchChan) go runClient(ctx, &wg, type2, q2, errChan, matchChan) go func() { wg.Wait() close(matchChan) close(errChan) }() matches := 0 for { select { case err, ok := <-errChan: if ok { return err } case _, ok := <-matchChan: if !ok { if matches == 2 { return nil } if expectSuccess { return fmt.Errorf("timeout/insufficient matches") } return fmt.Errorf("expected failure") // Treated as success for negative test } matches++ if matches == 2 { return nil } case <-ctx.Done(): if expectSuccess { return fmt.Errorf("timeout") } return fmt.Errorf("timeout (expected)") } } } func runMixedTest() error { var wg sync.WaitGroup wg.Add(2) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() errChan := make(chan error, 2) matchChan := make(chan string, 2) // Force them to 'see' each other with wildcard query go runClient(ctx, &wg, "minesweeper_free", "*", errChan, matchChan) go runClient(ctx, &wg, "minesweeper", "*", errChan, matchChan) matches := 0 for { select { case <-ctx.Done(): return fmt.Errorf("timeout") // Good result case _, ok := <-matchChan: if ok { matches++ if matches == 2 { return nil // Bad result! Match succeeded } } } } } func runMissingPropertyTest() error { var wg sync.WaitGroup wg.Add(2) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() matchChan := make(chan string, 2) runBadClient := func() { defer wg.Done() client := getClient() id := fmt.Sprintf("bad_%d", rand.Int()) // Authenticate err := client.AuthenticateCustom(ctx, id, true, "") if err != nil { return } // NewConn conn, err := client.NewConn(ctx) if err != nil { return } conn.MatchmakerMatchedHandler = func(ctx context.Context, m *nakama.MatchmakerMatchedMsg) { matchChan <- m.GetMatchId() } if err := conn.Open(ctx); err != nil { return } // Add matchmaker with NO properties msg := nakama.MatchmakerAdd("*", 2, 2) conn.MatchmakerAdd(ctx, msg) select { case <-ctx.Done(): } } go runBadClient() go runBadClient() // Wait a bit time.Sleep(2 * time.Second) if len(matchChan) > 0 { return nil // Bad } return fmt.Errorf("no match") // Good } func runClient(ctx context.Context, wg *sync.WaitGroup, gameType string, query string, errChan chan error, matchChan chan string) { defer wg.Done() client := getClient() uID := fmt.Sprintf("u_%s_%d", gameType, rand.Int()) if err := client.AuthenticateCustom(ctx, uID, true, ""); err != nil { errChan <- fmt.Errorf("auth failed: %w", err) return } conn, err := client.NewConn(ctx) if err != nil { errChan <- fmt.Errorf("newconn failed: %w", err) return } conn.MatchmakerMatchedHandler = func(ctx context.Context, m *nakama.MatchmakerMatchedMsg) { id := m.GetMatchId() token := m.GetToken() log.Printf("[%s] MATCHED! ID: %q, Token: %q", uID, id, token) if id == "" && token == "" { return // Ignore empty match? verification needed } if id != "" { matchChan <- id } else { matchChan <- token } } if err := conn.Open(ctx); err != nil { errChan <- fmt.Errorf("conn open failed: %w", err) return } props := map[string]string{"game_type": gameType} msg := nakama.MatchmakerAdd(query, 2, 2).WithStringProperties(props) log.Printf("[%s] Adding to matchmaker (Query: %s, Props: %v)", uID, query, props) if _, err := conn.MatchmakerAdd(ctx, msg); err != nil { errChan <- fmt.Errorf("add matchmaker failed: %w", err) return } select { case <-ctx.Done(): } }