Skip to content

psnfiller/cheat

Repository files navigation

Cheat

Repo: github.com/psnfiller/cheat

A gRPC implementation of the card game Cheat (aka BS): a dealer service, two bot opponents (random and a heuristic "smart" bot with card-counting), a personal web client, and a public multi-tenant web frontend at cheat.psn.af. See prompt.md for the original game rules spec.

Layout

proto/cheat.proto       the gRPC service definition (see "Writing a client" below)
gen/cheatpb/             generated protobuf/gRPC code (go generate ./... / buf generate)
internal/game/           pure rules engine: deck, adjacency rules, turn/accusation state machine
internal/dealer/         gRPC service wrapping internal/game (single-writer goroutine per game)
internal/strategy/       bot decision logic (RandomBot, HeuristicBot), shared by real clients and the simulator
internal/simulate/       in-process game runner (no gRPC) for fast self-play tuning
internal/sharp/          the strongest brain: event-driven card counting + Bayesian accusations, with its own eval harness
internal/neural/         a neural policy trained by reinforcement learning (no programmed tactics; see cmd/neuraltrain)
internal/gamelog/        ground-truth game logging (see "Where the logs are" below)
internal/statusweb/      spectator HTML page for cmd/server
cmd/server/              the dealer + spectator page, for local play
cmd/randomclient/        bot: plays legal moves at random
cmd/smartclient/         bot: plays via internal/strategy.HeuristicBot
cmd/sharpclient/         bot: plays via internal/sharp.Brain (strongest; ~85% vs smartclient heads-up)
cmd/neuralclient/        bot: plays via internal/neural's RL-trained policy
cmd/neuraltrain/         trains internal/neural's model (evolution strategies over the real engine)
cmd/webclient/           human web client for local play (pairs with cmd/server)
cmd/cheatweb/            the public multi-tenant service behind cheat.psn.af
cmd/tune/                self-play parameter tuning for HeuristicBot
cmd/analyzelogs/         fits a "humanlike" bot profile from real logged games

Playing locally

go build -o /tmp/cheat-server ./cmd/server && /tmp/cheat-server -players 2
go build -o /tmp/cheat-smart ./cmd/smartclient && /tmp/cheat-smart
go build -o /tmp/cheat-webclient ./cmd/webclient && /tmp/cheat-webclient

Then open the URL cmd/webclient prints (default http://localhost:8081). cmd/server's spectator page (default http://localhost:8080) shows the live board without revealing anyone's hand.

Where the logs are

Every game played through cmd/cheatweb (including the public site) is logged for later analysis — one append-only JSON-Lines file per game.

  • Production (cheat.psn.af): /home/psn/cheat-container/logs on chewy, mounted into the container at /data/logs (see docker-compose.yml).
  • Local runs: pass -log-dir <path> to cmd/cheatweb (defaults to /data/logs, which won't exist outside the container — pass an explicit path, or -log-dir "" to disable logging entirely; a failure to open the log directory is only ever a warning, never a reason to refuse to start a game).

Files are named <start-time>-<game-id>.jsonl, e.g. 20260704T173550Z-9cf7cd1a970cf301a25635a38a7118ff.jsonl. cmd/server / cmd/randomclient / cmd/smartclient / cmd/webclient (the personal, gRPC-based tools) don't produce these — only cmd/cheatweb's hosted sessions do, since that's the only place with the concept of "a real human playing."

What's in a log. Each line is one gamelog.TrainingEvent (see internal/gamelog/gamelog.go), one of three types:

  • "play" — what was actually played, known and recorded the instant it's played, regardless of whether anyone ever challenges it (the dealer always knows the true cards; ground truth doesn't depend on a challenge happening). Fields: claimed_rank, count, actual_ranks (the real cards), was_cheating, had_truthful_option (did the player have a legal, honest move available and choose to lie anyway), and hand_size_before.
  • "decision" — an accuse-or-pass call by someone other than the current player, with decision, the claim being judged (claimed_rank/count), pile_size, pt_hand_size, and (for "accuse") the resolved was_cheating.
  • "game_over" — just winner_id.

Every event carries game_id, player_id, and is_human (so you can filter to real human decisions vs. the bot's own). No PII is logged — no IP address, no name, nothing beyond these game-domain facts.

Analyzing logs: go run ./cmd/analyzelogs -dir /home/psn/cheat-container/logs prints sample sizes and a fitted "humanlike" HeuristicParams profile (how often real players bluff when they don't have to, how their accuse/pass calls correlate with claim size and pile pressure). Add -json out.json to save it, then feed it into cmd/tune -opponent humanlike -humanlike-params out.json to sanity-check (or eventually tune) the bot against real human behavior instead of only synthetic opponents. Treat the output as a rough starting point until enough real games have accumulated — the tool prints a warning when the sample is small.

Writing a client

A "client" is anything that speaks the Cheat gRPC service defined in proto/cheat.proto and dials a running cmd/server (or any internal/dealer.Server). The easiest way in is to read cmd/randomclient/main.go top to bottom — it's the smallest complete example — then cmd/smartclient/main.go if you want to see a real decision-making brain wired in the same way.

The protocol

  1. Register(name) — blocks until the server-configured number of players have all called Register, then returns your player_id and the full turn order. (The dealer deals the instant the last player registers.)
  2. StreamEvents(player_id) — open this right after registering. It's a server-streaming RPC that pushes every GameEvent you need: public events everyone gets (CardsPlayed, AccusationWindowOpen, ChallengeResolved, TurnStarted, TurnPassed, GameOver, HandSizes) plus one private event only you get (YourHand, sent whenever your hand changes). Card faces are never sent to anyone except their owner.
  3. React to events as they arrive:
    • TurnStarted{player_id == you} → decide what to play, call Play(player_id, card_indices, claimed_rank). card_indices are positions into the hand you last saw via YourHand — you don't have to actually hold the rank you claim; that's the whole game. claimed_rank must be the same, one above, or one below the last claimed rank (wrapping King↔Ace), unless no claim has been made yet this round (the very first play of the game, or right after a challenge fully clears the pile — you'll know because that's exactly when the pile empties), in which case any rank is legal.
    • AccusationWindowOpen{pt_player_id != you} → decide whether to call it. Call Accuse(player_id) to say the last play was a lie, or Pass(player_id) to let it stand. First response wins; a losing race just comes back with ok=false, which is fine to ignore.
    • GameOver → done.

All four action RPCs (Play/Accuse/Pass, plus Register) return {ok, error} rather than a gRPC error for game-rule rejections (wrong turn, illegal claim, already resolved, etc.) — a gRPC-level error means something more fundamental broke (bad player ID, game not started).

Minimal skeleton

conn, _ := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
client := cheatpb.NewCheatClient(conn)

reg, _ := client.Register(ctx, &cheatpb.RegisterRequest{Name: "MyBot"})
stream, _ := client.StreamEvents(ctx, &cheatpb.StreamRequest{PlayerId: reg.PlayerId})

for {
    ev, err := stream.Recv()
    if err != nil { return }
    switch e := ev.Event.(type) {
    case *cheatpb.GameEvent_YourHand:
        hand = e.YourHand.Cards
    case *cheatpb.GameEvent_TurnStarted:
        if e.TurnStarted.PlayerId == reg.PlayerId {
            client.Play(ctx, &cheatpb.PlayRequest{PlayerId: reg.PlayerId, CardIndices: ..., ClaimedRank: ...})
        }
    case *cheatpb.GameEvent_AccusationWindowOpen:
        if e.AccusationWindowOpen.PtPlayerId != reg.PlayerId {
            client.Pass(ctx, &cheatpb.PassRequest{PlayerId: reg.PlayerId}) // or Accuse
        }
    case *cheatpb.GameEvent_GameOver:
        return
    }
}

Reusing the existing decision logic

If you're writing a Go bot, you don't have to invent play/accusation logic from scratch — internal/strategy.Bot is a small interface (ChoosePlay(PlayContext, *rand.Rand) Decision, ShouldAccuse(AccusationContext, *rand.Rand) bool) already implemented by RandomBot and HeuristicBot. cmd/smartclient shows the full wiring: track your own hand and the current claim window from events, build a PlayContext/AccusationContext each time you need a decision, and call into the bot. The same interface is what internal/simulate drives directly (no gRPC) for fast self-play tuning — see cmd/tune if you want to evaluate or tune a new strategy before wiring it into a real client.

If your bot needs more than those thin per-decision contexts (card counting, opponent modeling — anything derived from the whole event stream), model it on internal/sharp instead (its design and tuning history are written up in internal/sharp/README.md): its Brain consumes the full GameEvent vocabulary via a small EventPlayer interface, and the package's harness runs EventPlayers and strategy.Bots (via AdaptBot) against each other in-process by the thousand, so a new brain can be measured before it ever touches gRPC (go test ./internal/sharp; set SHARP_SWEEP=1 for the slower diagnostics and parameter sweeps). cmd/sharpclient is the reference gRPC wiring for an event-driven brain.

Non-Go clients

Only proto/cheat.proto is authoritative. Generate stubs for another language with buf generate (or plain protoc) pointed at that file — buf.yaml/buf.gen.yaml in this directory are the Go-specific config, not needed for other languages, just the .proto itself.

About

Cheat card game: gRPC dealer, heuristic/Bayesian/neural-RL bots, public web frontend

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages