Build strategies

SDK Documentation

Complete reference for the rpsa_sdk package, organized around the decisions you make while building and validating a strategy.

Strategy Contract

Subclass Strategy, implement next_move(), and return one legal move every round.

Economy Context

Opt in with uses_context to read wallets, auctions, perks, and live match state.

Validation Ready

Understand AST scans, smoke tests, context checks, and the most common blockers.

Strategy Base Class

The required shape of every uploaded strategy file.

Import Strategy from rpsa_sdk. Your class must subclass it and implement next_move(context: Context) -> str.

Return rock, paper, or scissors. Any other value forfeits the round.
Optionally implement notify(own_move, opp_move) to track opponent history.
Set uses_context = True to opt in to wallets, auctions, and perk activation.
Only one Strategy subclass is permitted per file. Multiple subclasses fail AST validation.

Context API Reference

Live match data and economy actions available when uses_context is enabled.

When uses_context = True, the runtime injects a Context object before each call to next_move(). Read methods reflect current match state. Mutating methods run inside a DB savepoint and return a result dict; they never raise.

Read Methods
NameDescription
context.credits()Your current wallet balance as Decimal. Refreshed from the database each call.
context.opponent_walletOpponent wallet balance as a turn-start snapshot.
context.roundCurrent round number, 1-indexed. Round 0 is the pre-match bid window.
context.consecutive_wins()Number of consecutive wins at the start of this turn. Returns 0 on a loss streak.
context.market_state()Full market snapshot with total_rounds, posted_price_items, active_auctions, active_perks, and purchases.

Place Auction Bid

context.bid(stock_id: str, amount: Decimal)

Returns { ok: True, bid_id: str } on success or { ok: False, error: str } on failure. Common errors include WINDOW_CLOSED, INSUFFICIENT_FUNDS, and INVALID_BID.

Activate Owned Perk

context.activate(perk_code: str, t_start: int, **params)

Returns activation metadata on success or an error result on failure. t_start must be at least context.round + 1. WindowWager and WindowHedge require a Decimal stake param.

Opponent history
The context does not expose the opponent move history directly. Use notify(own_move, opp_move) to track it yourself between rounds.

Import Allowlist

The sandbox validates imports before a strategy can run.

The sandbox enforces a strict allowlist at process startup. Importing anything outside these lists fails validation with an AST scan error.

Standard Library - Allowed

abc, builtins, collections, copy,
dataclasses, decimal, enum, functools,
heapq, itertools, json, math,
operator, pathlib, pickle, queue,
random, re, statistics, string,
struct, time, typing, uuid

ML Packages - Tier B/C

numpy, scipy, sklearn,
torch, torchvision,
tensorflow, keras,
xgboost, lightgbm,
pandas (read-only ops)
Blocked
os, sys, subprocess, socket, ctypes,
importlib, _socket, _subprocess, _ctypes,
# plus calls to eval, exec, compile, __import__
# and attributes: __class__, __subclasses__,
# __globals__, __builtins__
# and open(..., "w") or open(..., "a")

Private C extension modules with names starting with _ are allowed unless explicitly blocked above.

Validation Pipeline

Every upload must pass three checks before season enrollment.

Every upload is queued for validation. All stages must pass before the strategy can be enrolled in a season. Validation typically completes in under 2 minutes.

Stage 1 - AST Scan
  • Parses the uploaded file with ast.parse(). Syntax errors fail immediately at upload with 422.
  • Checks blocked imports, blocked function calls, and blocked attribute access.
  • Verifies exactly one Strategy subclass exists.
  • Checks for write-mode open() calls.
  • Detects Tier A or Tier B automatically. Tier C is assigned at upload for zip archives.
Stage 2 - Smoke Test
  • Runs your strategy for 100 rounds against AlwaysRock in a sandboxed subprocess.
  • Uses a 2-minute total time limit and a per-turn budget.
  • Records peak memory and fails memory violations.
  • Passes when the strategy completes all 100 rounds without crash, timeout, or memory violation.
  • Invalid move returns are noted in the report, but they do not fail the smoke test.
Stage 3 - Context Test
  • Runs 50 rounds with a mock context whose credits cycle through 0, 10, 30, 100, and 500.
  • Verifies the strategy can receive and use the context object without raising.
  • Records bid() and activate() calls for economy diagnostics.
  • Passes when the strategy completes all 50 rounds without exception.
Common failure reasons
Importing random is allowed, but os.urandom is not. Calls like time.sleep() can time out, large state can exceed memory limits, and re-uploading resets validation to pending.

Execution Tiers

Choose the smallest tier that fits your strategy.

Tier A

Pure Python

No ML libraries. Uses only the standard library allowlist and rpsa_sdk. Fastest execution with the lowest sandbox overhead.

Simple Strategy

from rpsa_sdk import Strategy

class RockAlways(Strategy):
    def next_move(self, context) -> str:
        return "rock"

Tier B

ML Imports

May import NumPy, PyTorch, scikit-learn, and other ML packages from the allowlist. Detected by the AST scanner.

NumPy-Based

import numpy as np
from rpsa_sdk import Strategy

class RandomBot(Strategy):
    def next_move(self, context) -> str:
        return np.random.choice(["rock", "paper", "scissors"])

Tier C

Model File

A zip archive containing strategy.py and a trained model file. Use this when your strategy loads a model artifact.

Loads Model From File

import pickle
from rpsa_sdk import Strategy

class ModelBot(Strategy):
    def __init__(self):
        with open(self.model_path, "rb") as f:
            self.model = pickle.load(f)

    def next_move(self, context) -> str:
        return self.model.predict()

Tier C ZIP Archive Guide

Ship a trained model alongside strategy.py.

Tier C uploads must be a .zip archive with this exact structure:

archive.zip
|-- strategy.py       # required: must contain your Strategy subclass
|-- model.<ext>       # required: pkl, pt, h5, joblib, onnx, ...
|-- requirements.txt  # optional: extra allowed packages

Accessing the model path

Before the first call to next_move(), the runtime sets self.model_path to an absolute path for your extracted model file.

def __init__(self):
    import pickle
    with open(self.model_path, "rb") as f:
        self.clf = pickle.load(f)

Requirements and limits

  • Use only packages from the ML allowlist.
  • Avoid exact version pins unless necessary.
  • Model file size: 200 MB or less.
  • Total archive size: 210 MB or less.
  • No unexpected files outside strategy.py, model, and requirements.txt.

Strategy Patterns

Useful building blocks for stronger entries.

These patterns separate a competitive entry from AlwaysRock. Combine them in a single strategy for better expected value.

Pattern 1 - Opponent Frequency Tracking

Track opponent move history through notify(), predict the next move from recent frequency, and counter it.

from collections import Counter
from rpsa_sdk import Strategy

class FrequencyCounter(Strategy):
    uses_context = True

    def __init__(self):
        self._opp_history = []
        self._beats = {"rock": "paper", "paper": "scissors", "scissors": "rock"}

    def next_move(self, context) -> str:
        if len(self._opp_history) < 10:
            return "rock"
        counts = Counter(self._opp_history[-20:])
        predicted = counts.most_common(1)[0][0]
        return self._beats[predicted]

    def notify(self, own_move: str, opp_move: str) -> None:
        self._opp_history.append(opp_move)

Pattern 2 - Wallet-Adaptive Aggression

Read both wallets from context and shift aggression based on the capital gap.

from rpsa_sdk import Strategy

class WalletAdaptive(Strategy):
    uses_context = True

    def next_move(self, context) -> str:
        my_wallet = context.credits()
        opp_wallet = context.opponent_wallet

        # Behind by more than 50 cr: switch to aggressive rock spam
        if opp_wallet - my_wallet > 50:
            return "rock"
        # Comfortably ahead: play safe scissors to avoid paper traps
        if my_wallet - opp_wallet > 30:
            return "scissors"
        return "paper"

Pattern 3 - Auction-Aware Bidding

Read market_state(), bid during open auction windows, and activate owned perks at strategic rounds.

from decimal import Decimal
from rpsa_sdk import Strategy

class AuctionAware(Strategy):
    uses_context = True
    _bid_placed = False

    def next_move(self, context) -> str:
        market = context.market_state()

        # Bid on LossCap in the pre-match Vickrey if we have not yet
        if not self._bid_placed and context.round <= 1:
            for auction in market["active_auctions"]:
                if "LossCap" in auction.get("perk_id", ""):
                    result = context.bid(auction["stock_id"], Decimal("15"))
                    if result["ok"]:
                        self._bid_placed = True

        # Activate LossCap once we own it; schedule it for the next round
        owned = {p["perk_code"] for p in market.get("purchases", [])}
        activated = {p["perk_code"] for p in market.get("active_perks", [])}
        if "LossCap" in owned and "LossCap" not in activated and context.round == 5:
            context.activate("LossCap", t_start=context.round + 1)

        return "rock"
Auction tip
Check market_state()["active_auctions"] every round. A stock with window_phase == "open" accepts bids; closed means the auction has cleared.
Related references

Keep the docs close while iterating.

Jump between the SDK, the rules, and the participation guide when you are tuning a strategy for ranked play.

Upload Strategy