Use this template when you want a public repository (free GitHub Actions minutes) without publishing your penalty-shootout strategy. Only an encrypted payload lives in git; the workflow decrypts it just-in-time, submits your move, then deletes the plaintext.
-
Fork this repository – Fork it to your own GitHub account so GitHub Actions can run your player automatically.
-
Add repository secrets – In Settings → Secrets and variables → Actions create:
PLAYER_NAME– the public name you want the server to displaySERVER_URL– the base UBX server URLGAME_TOKEN– a fine-grained GitHub Personal Access Token with:- Repository access: Only your player repository (select the specific repo)
- Repository permissions (all set to Read and write):
Actions: Read and write(required - to trigger workflows via repository_dispatch)Workflows: Read and write(required - for workflow management)Contents: Read and write(required - for repository access)
-
Set up encryption and encrypt your strategy:
a. Edit your strategy:
- Edit
strategy.pywith your strategy logic
b. Encrypt your strategy:
- Run:
python scripts/setup_encryption.py - The script will generate an encryption key and display it
- In Settings → Secrets and variables → Actions, add:
ENCRYPTION_KEY– paste the encryption key shown by the script
- Commit the encrypted file:
git add strategy.py.encrypted - Commit and push:
git commit -m "Add encrypted strategy" && git push - Important: Only commit
strategy.py.encrypted- do NOT commit changes tostrategy.py
Note: If you need to update your strategy later, run
python scripts/setup_encryption.pyagain. You can reuse the sameENCRYPTION_KEYby passing it with--key <your-key>. - Edit
Register your player by running the registration workflow via GitHub Actions:
- Go to Actions tab in your repository
- Select the Register Player workflow from the left sidebar
- Click Run workflow → Run workflow button
The scripts interact with the game server via REST API endpoints:
-
register.py– Validates required secrets, readsPLAYER_NAMEfrom the environment, and sends a POST request to/registerwith your player name and repository information. The server authenticates using yourGAME_TOKENand stores your player configuration. -
bot.py– Executes your game strategy:- Decrypts
strategy.py.encryptedtostrategy.py(handled by the workflow) - Sends a GET request to
/status?player_name=<PLAYER_NAME>to retrieve the current game state (turn, opponents, history), where<PLAYER_NAME>is the value from yourPLAYER_NAMEsecret - Calls your
strategy(state)function with the game state - Sends a POST request to
/actionwith your chosenshootandkeepdirections for each opponent - Cleans up the decrypted
strategy.pyfile
Customise your strategy by editing the
strategy(state)function instrategy.pyto analyze the game state and return action directions. The function must return the dictionary format described below. - Decrypts
/status responds with a JSON dictionary—{} at the very beginning of a fresh game—that contains:
playerIds: every registered player ID (these are what you target in your action maps).myPlayerId: the ID associated with your GitHub token.opponentsIds: convenience list of all other IDs.state: a list where each entry records one completed turn. Penalty shootout rounds look like:In this snapshot,[ { "_turnId": 1, "player-id-A": { "player-id-B": { "shoot": 2, "keep": 0, "outcome": true }, "player-id-C": { "shoot": 0, "keep": 1, "outcome": false } }, "player-id-B": { "player-id-A": { "shoot": 1, "keep": 2, "outcome": false }, "player-id-C": { "shoot": 2, "keep": 0, "outcome": true } }, "player-id-C": { "player-id-A": { "shoot": 0, "keep": 1, "outcome": true }, "player-id-B": { "shoot": 1, "keep": 2, "outcome": false } } }, { "_turnId": 2, "...": { "...": "..." } } ]state[0]["player-id-A"]["player-id-B"]summarizes the penalty with A shooting and B keeping in the first turn: A shot right (2), B dived left (0), andoutcomeistrue(goal scored).turnId: the current turn number (metadata describing where the match is).
Your strategy(state) function must return a dictionary with two maps, one for shooting and one for keeping. For example, if the server identifies you as "player-A" and you face opponents "player-B" and "player-C", one admissible return value is
{
"shoot": { "player-B": 2, "player-C": 0 },
"keep": { "player-B": 1, "player-C": 1 }
}where
shootlists the direction (integers0,1, or2) you will shoot against each opponent.keeplists the direction you will guard against each opponent.- Opponent IDs come straight from
playerIds/opponentsIdsin the/statuspayload.