Pyric
Navigate

Firestore Security Rules · Isolated browser sandbox

Play chess against Security Rules

Move a piece; Pyric commits the new board or denies the write.

Try the board

Choose a piece and its destination. e2 → e4 is allowed. e2 → e5 is denied and leaves the board unchanged. Switch the identity to see turn ownership enforced.

Choose a scenario to run a complete sequence through the Rules. Fool’s Mate and Scholar’s Mate end in checkmate. The opening remains in progress. The illegal pawn leap is denied without changing the board.

A move is a Firestore write

move → proposed Firestore document → Security Rules → commit or deny → next board

Each game is one Firestore document. A move proposes its next state: one square is emptied, another receives the piece, the turn changes, and the move count increases.

Pyric evaluates that write against Firestore Security Rules in the browser. An allowed write changes the document and the board. When the Rules deny a write, the document and board stay unchanged.

Why the Rules look like this

Firestore Security Rules cannot loop over the pieces on a chessboard. A request also has limits on expressions, function calls, and document reads. These constraints determine how the chess Rules are structured.

Movement geometry lives in a read-only Firestore document, where the Rules can look up valid destinations and the squares between them. Check detection names each opposing piece explicitly. Each move type starts in a separate branch so unrelated checks can stop early.

Look up the available helpers

rules_version = '2+modules';

import { isAuthenticated } from 'auth';
import { validSimpleMove } from 'geometry';
import { isPlaying, moveIncremented, participantsUnchanged } from 'state';
import { isMyTurn, turnFlipped } from 'turns';

Before using a helper, an agent can ask firestore_rules_stdlib_list and firestore_rules_stdlib_get for its exact name, arguments, and examples. The imports above come from that library. firestore_resolve_modules then turns them into ordinary version 2 Rules.

The shared helpers cover authentication, participants, turns, move counts, and movement across a grid. The functions left in this file deal with chess: blocked paths, captures, castling, and king safety.

Route each move to one branch

Before writing the proposed game document, the board labels each available move from the piece and its destination:

Piece and destinationmoveType
Non-pawn moving to an empty squarenormal
Non-pawn moving to an occupied squarecapture
Pawn moving one square to an empty squarepawn_forward
Pawn moving two squares to an empty squaredouble_pawn
Pawn moving to an occupied squarepawn_capture

The board stores the label in the proposed /chess-v2/{gameId} document. The matching Rules branch then checks whether the move is legal. In the Rules, resource.data is the current game and request.resource.data is that proposed next game.

The allow update clauses inside this match block are alternatives. Firestore allows the update when any one of them returns true. Because a proposed document has one moveType, only its matching branch can get past the first comparison.

allow update: if request.resource.data.moveType == 'normal'
      && request.resource.data.capturedPiece == ''
      && request.resource.data.status == 'playing'
      && baseMoveChecks() && validPieceMove()
      && resource.data[request.resource.data.moveTo] == ''
      && pieceMovedCorrectly() && myKingSafe();

allow update: if request.resource.data.moveType == 'capture'
      && request.resource.data.capturedPiece != ''
      && request.resource.data.status == 'playing'
      && baseMoveChecks() && validPieceMove()
      && captureValid() && pieceMovedCorrectly() && myKingSafe();

The normal branch requires an empty destination and no captured piece. The capture branch requires a captured piece, and captureValid() checks that the proposed document removes it. Both branches check the player and turn, the piece’s movement, the changed board fields, and king safety. Pawn moves have separate branches.

The first comparison in each branch is deliberately cheap. A capture skips the normal-move checks, and a normal move skips the capture checks. firestore_lint_rules warns the agent when branches share a gate and may waste the evaluation budget, as well as when a branch calls too many functions or reads too many documents.

Derive checkmate from the board

Security Rules decide whether a move may be stored. After an allowed move commits, the browser checks the resulting board. It reports checkmate only when the king is attacked and no legal move, capture, block, king escape, or en passant response removes the attack.

The write contains the move and the next board state. After it commits, the browser derives checkmate from that board.

Test specific moves

After changing the Rules, the agent uses firestore_simulate_rules to try specific moves with a player and a board state. Stateful sandbox sessions run longer sequences. If a move is denied unexpectedly, sandbox_inspect shows the request, identity, active Rules, and denial.

The local suite covers allowed and denied geometry, blocked paths, captures, pins, check, turn ownership, resignation, and checkmate. It also checks that a rejected move leaves the Firestore document unchanged.

These checks happen in Pyric’s local development mirror. Important cases also run against a Firebase project before shipping.

Full Security Rules

These are the Rules running the board above. Look for the imports at the top, the separate moveType gates, and the explicit king-safety checks.

Security Rules
rules_version = '2+modules';

import { isAuthenticated } from 'auth';
import { validSimpleMove } from 'geometry';
import { isPlaying, moveIncremented, participantsUnchanged } from 'state';
import { isMyTurn, turnFlipped } from 'turns';

service cloud.firestore {
  match /databases/{database}/documents {
    match /gameConfig/{id} { allow read: if true; allow write: if false; }
    match /chess-v2/{gameId} {

      function cfg() { return get(/databases/$(database)/documents/gameConfig/chessv2).data; }

      function isOwnPiece() {
        let piece = resource.data[request.resource.data.moveFrom];
        return (resource.data.currentTurn == 'host'
                && (piece == 'K' || piece == 'Q' || piece == 'R' || piece == 'B' || piece == 'N' || piece == 'P'))
            || (resource.data.currentTurn == 'guest'
                && (piece == 'k' || piece == 'q' || piece == 'r' || piece == 'b' || piece == 'n' || piece == 'p'));
      }
      function baseMoveChecks() {
        return isAuthenticated() && isPlaying()
            && isMyTurn() && turnFlipped() && isOwnPiece()
            && moveIncremented() && participantsUnchanged();
      }

      function validPieceMove() {
        let mf = request.resource.data.moveFrom;
        let mt = request.resource.data.moveTo;
        let piece = resource.data[mf];
        let p = cfg().paths[mf][mt];
        return validSimpleMove(cfg())
            && (cfg().pieceCategory[piece] == 'jump'
                || ((p.len < 1 || resource.data[p.c0] == '')
                    && (p.len < 2 || resource.data[p.c1] == '')
                    && (p.len < 3 || resource.data[p.c2] == '')
                    && (p.len < 4 || resource.data[p.c3] == '')
                    && (p.len < 5 || resource.data[p.c4] == '')
                    && (p.len < 6 || resource.data[p.c5] == '')));
      }
      function validPawnForward() {
        let mf = request.resource.data.moveFrom;
        let mt = request.resource.data.moveTo;
        let piece = resource.data[mf];
        return (piece == 'P' || piece == 'p') && mt in cfg().pawnForward[piece][mf] && resource.data[mt] == '';
      }
      function validPawnDouble() {
        let mf = request.resource.data.moveFrom;
        let mt = request.resource.data.moveTo;
        let piece = resource.data[mf];
        return (piece == 'P' || piece == 'p')
            && mf in cfg().pawnDouble[piece]
            && cfg().pawnDouble[piece][mf].to == mt
            && resource.data[mt] == ''
            && resource.data[cfg().pawnDouble[piece][mf].between] == '';
      }
      function validPawnCapture() {
        let mf = request.resource.data.moveFrom;
        let mt = request.resource.data.moveTo;
        let piece = resource.data[mf];
        let target = resource.data[mt];
        return (piece == 'P' || piece == 'p')
            && mt in cfg().pawnAttacks[piece][mf]
            && target != ''
            && ((piece == 'P' && (target == 'k' || target == 'q' || target == 'r' || target == 'b' || target == 'n' || target == 'p'))
                || (piece == 'p' && (target == 'K' || target == 'Q' || target == 'R' || target == 'B' || target == 'N' || target == 'P')));
      }
      function validPromotion() {
        let mt = request.resource.data.moveTo;
        let piece = resource.data[request.resource.data.moveFrom];
        let pt = request.resource.data.promotedTo;
        return request.resource.data.moveType == 'promotion'
            && ((piece == 'P' && mt >= 'a8' && mt <= 'h8' && (pt == 'Q' || pt == 'R' || pt == 'B' || pt == 'N'))
                || (piece == 'p' && mt >= 'a1' && mt <= 'h1' && (pt == 'q' || pt == 'r' || pt == 'b' || pt == 'n')));
      }
      function pieceMovedCorrectly() {
        let mf = request.resource.data.moveFrom;
        let mt = request.resource.data.moveTo;
        let piece = resource.data[mf];
        return request.resource.data[mf] == ''
            && (request.resource.data.moveType != 'promotion'
                ? request.resource.data[mt] == piece
                : request.resource.data[mt] == request.resource.data.promotedTo)
            && resource.data[request.resource.data.movedPiece] == mf
            && request.resource.data[request.resource.data.movedPiece] == mt;
      }
      function captureValid() {
        let cp = request.resource.data.capturedPiece;
        return cp != ''
            && resource.data[cp] == request.resource.data.moveTo
            && request.resource.data[cp] == '';
      }
      function validEnPassant() {
        let mf = request.resource.data.moveFrom;
        let mt = request.resource.data.moveTo;
        let piece = resource.data[mf];
        return (piece == 'P' || piece == 'p')
            && mt in cfg().pawnAttacks[piece][mf]
            && resource.data[mt] == ''
            && resource.data.lastDoublePawn == mt;
      }
      function validCastleKingside() {
        let turn = resource.data.currentTurn;
        let c = cfg().castling;
        let castle = turn == 'host' ? c.wk : c.bk;
        let kingMoved = turn == 'host' ? resource.data.hp_K_moved : resource.data.gp_k_moved;
        let rookMoved = turn == 'host' ? resource.data.hp_R2_moved : resource.data.gp_r2_moved;
        return !kingMoved && !rookMoved
            && request.resource.data.moveFrom == castle.kingFrom
            && request.resource.data.moveTo == castle.kingTo
            && resource.data[castle.between[0]] == ''
            && resource.data[castle.between[1]] == '';
      }
      function validCastleQueenside() {
        let turn = resource.data.currentTurn;
        let c = cfg().castling;
        let castle = turn == 'host' ? c.wq : c.bq;
        let kingMoved = turn == 'host' ? resource.data.hp_K_moved : resource.data.gp_k_moved;
        let rookMoved = turn == 'host' ? resource.data.hp_R1_moved : resource.data.gp_r1_moved;
        return !kingMoved && !rookMoved
            && request.resource.data.moveFrom == castle.kingFrom
            && request.resource.data.moveTo == castle.kingTo
            && resource.data[castle.between[0]] == ''
            && resource.data[castle.between[1]] == ''
            && resource.data[castle.between[2]] == '';
      }

      function hPC(cfg) {
        let b = request.resource.data;
        let ks = b.hp_K;
        return (b.gp_p1 != '' && b[b.gp_p1] != '' && ks in cfg.pawnAttacks[b[b.gp_p1]][b.gp_p1])
            || (b.gp_p2 != '' && b[b.gp_p2] != '' && ks in cfg.pawnAttacks[b[b.gp_p2]][b.gp_p2])
            || (b.gp_p3 != '' && b[b.gp_p3] != '' && ks in cfg.pawnAttacks[b[b.gp_p3]][b.gp_p3])
            || (b.gp_p4 != '' && b[b.gp_p4] != '' && ks in cfg.pawnAttacks[b[b.gp_p4]][b.gp_p4])
            || (b.gp_p5 != '' && b[b.gp_p5] != '' && ks in cfg.pawnAttacks[b[b.gp_p5]][b.gp_p5])
            || (b.gp_p6 != '' && b[b.gp_p6] != '' && ks in cfg.pawnAttacks[b[b.gp_p6]][b.gp_p6])
            || (b.gp_p7 != '' && b[b.gp_p7] != '' && ks in cfg.pawnAttacks[b[b.gp_p7]][b.gp_p7])
            || (b.gp_p8 != '' && b[b.gp_p8] != '' && ks in cfg.pawnAttacks[b[b.gp_p8]][b.gp_p8]);
      }
      function hMC(cfg) {
        let b = request.resource.data;
        let ks = b.hp_K;
        return (b.gp_n1 != '' && b[b.gp_n1] != '' && ks in cfg.moves[b[b.gp_n1]][b.gp_n1])
            || (b.gp_n2 != '' && b[b.gp_n2] != '' && ks in cfg.moves[b[b.gp_n2]][b.gp_n2])
            || (b.gp_k != '' && b[b.gp_k] != '' && ks in cfg.moves[b[b.gp_k]][b.gp_k]);
      }
      function hSC(cfg) {
        let b = request.resource.data;
        let ks = b.hp_K;
        return (b.gp_q != '' && b[b.gp_q] != '' && ks in cfg.moves[b[b.gp_q]][b.gp_q]
            && (cfg.paths[b.gp_q][ks].len < 1 || b[cfg.paths[b.gp_q][ks].c0] == '')
            && (cfg.paths[b.gp_q][ks].len < 2 || b[cfg.paths[b.gp_q][ks].c1] == '')
            && (cfg.paths[b.gp_q][ks].len < 3 || b[cfg.paths[b.gp_q][ks].c2] == '')
            && (cfg.paths[b.gp_q][ks].len < 4 || b[cfg.paths[b.gp_q][ks].c3] == '')
            && (cfg.paths[b.gp_q][ks].len < 5 || b[cfg.paths[b.gp_q][ks].c4] == ''))
            || (b.gp_r1 != '' && b[b.gp_r1] != '' && ks in cfg.moves[b[b.gp_r1]][b.gp_r1]
            && (cfg.paths[b.gp_r1][ks].len < 1 || b[cfg.paths[b.gp_r1][ks].c0] == '')
            && (cfg.paths[b.gp_r1][ks].len < 2 || b[cfg.paths[b.gp_r1][ks].c1] == '')
            && (cfg.paths[b.gp_r1][ks].len < 3 || b[cfg.paths[b.gp_r1][ks].c2] == '')
            && (cfg.paths[b.gp_r1][ks].len < 4 || b[cfg.paths[b.gp_r1][ks].c3] == '')
            && (cfg.paths[b.gp_r1][ks].len < 5 || b[cfg.paths[b.gp_r1][ks].c4] == ''))
            || (b.gp_r2 != '' && b[b.gp_r2] != '' && ks in cfg.moves[b[b.gp_r2]][b.gp_r2]
            && (cfg.paths[b.gp_r2][ks].len < 1 || b[cfg.paths[b.gp_r2][ks].c0] == '')
            && (cfg.paths[b.gp_r2][ks].len < 2 || b[cfg.paths[b.gp_r2][ks].c1] == '')
            && (cfg.paths[b.gp_r2][ks].len < 3 || b[cfg.paths[b.gp_r2][ks].c2] == '')
            && (cfg.paths[b.gp_r2][ks].len < 4 || b[cfg.paths[b.gp_r2][ks].c3] == '')
            && (cfg.paths[b.gp_r2][ks].len < 5 || b[cfg.paths[b.gp_r2][ks].c4] == ''))
            || (b.gp_b1 != '' && b[b.gp_b1] != '' && ks in cfg.moves[b[b.gp_b1]][b.gp_b1]
            && (cfg.paths[b.gp_b1][ks].len < 1 || b[cfg.paths[b.gp_b1][ks].c0] == '')
            && (cfg.paths[b.gp_b1][ks].len < 2 || b[cfg.paths[b.gp_b1][ks].c1] == '')
            && (cfg.paths[b.gp_b1][ks].len < 3 || b[cfg.paths[b.gp_b1][ks].c2] == '')
            && (cfg.paths[b.gp_b1][ks].len < 4 || b[cfg.paths[b.gp_b1][ks].c3] == '')
            && (cfg.paths[b.gp_b1][ks].len < 5 || b[cfg.paths[b.gp_b1][ks].c4] == ''))
            || (b.gp_b2 != '' && b[b.gp_b2] != '' && ks in cfg.moves[b[b.gp_b2]][b.gp_b2]
            && (cfg.paths[b.gp_b2][ks].len < 1 || b[cfg.paths[b.gp_b2][ks].c0] == '')
            && (cfg.paths[b.gp_b2][ks].len < 2 || b[cfg.paths[b.gp_b2][ks].c1] == '')
            && (cfg.paths[b.gp_b2][ks].len < 3 || b[cfg.paths[b.gp_b2][ks].c2] == '')
            && (cfg.paths[b.gp_b2][ks].len < 4 || b[cfg.paths[b.gp_b2][ks].c3] == '')
            && (cfg.paths[b.gp_b2][ks].len < 5 || b[cfg.paths[b.gp_b2][ks].c4] == ''));
      }
      function gPC(cfg) {
        let b = request.resource.data;
        let ks = b.gp_k;
        return (b.hp_P1 != '' && b[b.hp_P1] != '' && ks in cfg.pawnAttacks[b[b.hp_P1]][b.hp_P1])
            || (b.hp_P2 != '' && b[b.hp_P2] != '' && ks in cfg.pawnAttacks[b[b.hp_P2]][b.hp_P2])
            || (b.hp_P3 != '' && b[b.hp_P3] != '' && ks in cfg.pawnAttacks[b[b.hp_P3]][b.hp_P3])
            || (b.hp_P4 != '' && b[b.hp_P4] != '' && ks in cfg.pawnAttacks[b[b.hp_P4]][b.hp_P4])
            || (b.hp_P5 != '' && b[b.hp_P5] != '' && ks in cfg.pawnAttacks[b[b.hp_P5]][b.hp_P5])
            || (b.hp_P6 != '' && b[b.hp_P6] != '' && ks in cfg.pawnAttacks[b[b.hp_P6]][b.hp_P6])
            || (b.hp_P7 != '' && b[b.hp_P7] != '' && ks in cfg.pawnAttacks[b[b.hp_P7]][b.hp_P7])
            || (b.hp_P8 != '' && b[b.hp_P8] != '' && ks in cfg.pawnAttacks[b[b.hp_P8]][b.hp_P8]);
      }
      function gMC(cfg) {
        let b = request.resource.data;
        let ks = b.gp_k;
        return (b.hp_N1 != '' && b[b.hp_N1] != '' && ks in cfg.moves[b[b.hp_N1]][b.hp_N1])
            || (b.hp_N2 != '' && b[b.hp_N2] != '' && ks in cfg.moves[b[b.hp_N2]][b.hp_N2])
            || (b.hp_K != '' && b[b.hp_K] != '' && ks in cfg.moves[b[b.hp_K]][b.hp_K]);
      }
      function gSC(cfg) {
        let b = request.resource.data;
        let ks = b.gp_k;
        return (b.hp_Q != '' && b[b.hp_Q] != '' && ks in cfg.moves[b[b.hp_Q]][b.hp_Q]
            && (cfg.paths[b.hp_Q][ks].len < 1 || b[cfg.paths[b.hp_Q][ks].c0] == '')
            && (cfg.paths[b.hp_Q][ks].len < 2 || b[cfg.paths[b.hp_Q][ks].c1] == '')
            && (cfg.paths[b.hp_Q][ks].len < 3 || b[cfg.paths[b.hp_Q][ks].c2] == '')
            && (cfg.paths[b.hp_Q][ks].len < 4 || b[cfg.paths[b.hp_Q][ks].c3] == '')
            && (cfg.paths[b.hp_Q][ks].len < 5 || b[cfg.paths[b.hp_Q][ks].c4] == ''))
            || (b.hp_R1 != '' && b[b.hp_R1] != '' && ks in cfg.moves[b[b.hp_R1]][b.hp_R1]
            && (cfg.paths[b.hp_R1][ks].len < 1 || b[cfg.paths[b.hp_R1][ks].c0] == '')
            && (cfg.paths[b.hp_R1][ks].len < 2 || b[cfg.paths[b.hp_R1][ks].c1] == '')
            && (cfg.paths[b.hp_R1][ks].len < 3 || b[cfg.paths[b.hp_R1][ks].c2] == '')
            && (cfg.paths[b.hp_R1][ks].len < 4 || b[cfg.paths[b.hp_R1][ks].c3] == '')
            && (cfg.paths[b.hp_R1][ks].len < 5 || b[cfg.paths[b.hp_R1][ks].c4] == ''))
            || (b.hp_R2 != '' && b[b.hp_R2] != '' && ks in cfg.moves[b[b.hp_R2]][b.hp_R2]
            && (cfg.paths[b.hp_R2][ks].len < 1 || b[cfg.paths[b.hp_R2][ks].c0] == '')
            && (cfg.paths[b.hp_R2][ks].len < 2 || b[cfg.paths[b.hp_R2][ks].c1] == '')
            && (cfg.paths[b.hp_R2][ks].len < 3 || b[cfg.paths[b.hp_R2][ks].c2] == '')
            && (cfg.paths[b.hp_R2][ks].len < 4 || b[cfg.paths[b.hp_R2][ks].c3] == '')
            && (cfg.paths[b.hp_R2][ks].len < 5 || b[cfg.paths[b.hp_R2][ks].c4] == ''))
            || (b.hp_B1 != '' && b[b.hp_B1] != '' && ks in cfg.moves[b[b.hp_B1]][b.hp_B1]
            && (cfg.paths[b.hp_B1][ks].len < 1 || b[cfg.paths[b.hp_B1][ks].c0] == '')
            && (cfg.paths[b.hp_B1][ks].len < 2 || b[cfg.paths[b.hp_B1][ks].c1] == '')
            && (cfg.paths[b.hp_B1][ks].len < 3 || b[cfg.paths[b.hp_B1][ks].c2] == '')
            && (cfg.paths[b.hp_B1][ks].len < 4 || b[cfg.paths[b.hp_B1][ks].c3] == '')
            && (cfg.paths[b.hp_B1][ks].len < 5 || b[cfg.paths[b.hp_B1][ks].c4] == ''))
            || (b.hp_B2 != '' && b[b.hp_B2] != '' && ks in cfg.moves[b[b.hp_B2]][b.hp_B2]
            && (cfg.paths[b.hp_B2][ks].len < 1 || b[cfg.paths[b.hp_B2][ks].c0] == '')
            && (cfg.paths[b.hp_B2][ks].len < 2 || b[cfg.paths[b.hp_B2][ks].c1] == '')
            && (cfg.paths[b.hp_B2][ks].len < 3 || b[cfg.paths[b.hp_B2][ks].c2] == '')
            && (cfg.paths[b.hp_B2][ks].len < 4 || b[cfg.paths[b.hp_B2][ks].c3] == '')
            && (cfg.paths[b.hp_B2][ks].len < 5 || b[cfg.paths[b.hp_B2][ks].c4] == ''));
      }

      function myKingSafe() {
        let c = cfg();
        return (resource.data.currentTurn == 'host' && !hPC(c) && !hMC(c) && !hSC(c))
            || (resource.data.currentTurn == 'guest' && !gPC(c) && !gMC(c) && !gSC(c));
      }
      function opponentInCheck() {
        let c = cfg();
        return (resource.data.currentTurn == 'host' && (gPC(c) || gMC(c) || gSC(c)))
            || (resource.data.currentTurn == 'guest' && (hPC(c) || hMC(c) || hSC(c)));
      }

      // ═══ Lobby ═══
      allow read: if request.auth != null;
      allow create: if request.auth != null
            && request.resource.data.host == request.auth.uid
            && request.resource.data.status == 'waiting'
            && d.a1 == 'R'
            && d.a2 == 'P'
            && d.a7 == 'p'
            && d.a8 == 'r'
            && d.b1 == 'N'
            && d.b2 == 'P'
            && d.b7 == 'p'
            && d.b8 == 'n'
            && d.c1 == 'B'
            && d.c2 == 'P'
            && d.c7 == 'p'
            && d.c8 == 'b'
            && d.d1 == 'Q'
            && d.d2 == 'P'
            && d.d7 == 'p'
            && d.d8 == 'q'
            && d.e1 == 'K'
            && d.e2 == 'P'
            && d.e7 == 'p'
            && d.e8 == 'k'
            && d.f1 == 'B'
            && d.f2 == 'P'
            && d.f7 == 'p'
            && d.f8 == 'b'
            && d.g1 == 'N'
            && d.g2 == 'P'
            && d.g7 == 'p'
            && d.g8 == 'n'
            && d.h1 == 'R'
            && d.h2 == 'P'
            && d.h7 == 'p'
            && d.h8 == 'r';
      allow update: if request.auth != null
            && resource.data.status == 'waiting'
            && resource.data.guest == ''
            && request.resource.data.guest == request.auth.uid
            && request.auth.uid != resource.data.host
            && request.resource.data.status == 'playing'
            && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['guest', 'status']);
      allow delete: if request.auth != null
            && resource.data.status == 'waiting'
            && request.auth.uid == resource.data.host;

      // ═══ UNIQUE moveType gates — each rule category has its own ═══
      // Pawn forward
      allow update: if request.resource.data.moveType == 'pawn_forward'
            && request.resource.data.capturedPiece == '' && request.resource.data.status == 'playing'
            && baseMoveChecks() && validPawnForward() && request.resource.data.promotedTo == ''
            && pieceMovedCorrectly() && myKingSafe();
      // Pawn capture
      allow update: if request.resource.data.moveType == 'pawn_capture'
            && request.resource.data.capturedPiece != '' && request.resource.data.status == 'playing'
            && baseMoveChecks() && validPawnCapture() && request.resource.data.promotedTo == ''
            && captureValid() && pieceMovedCorrectly() && myKingSafe();
      // Pawn double
      allow update: if request.resource.data.moveType == 'double_pawn'
            && request.resource.data.capturedPiece == '' && request.resource.data.status == 'playing'
            && baseMoveChecks() && validPawnDouble() && pieceMovedCorrectly() && myKingSafe();
      // Promotion (forward)
      allow update: if request.resource.data.moveType == 'promotion'
            && request.resource.data.capturedPiece == '' && request.resource.data.status == 'playing'
            && baseMoveChecks() && validPawnForward() && validPromotion() && pieceMovedCorrectly() && myKingSafe();
      // Promotion (capture)
      allow update: if request.resource.data.moveType == 'promotion_capture'
            && request.resource.data.capturedPiece != '' && request.resource.data.status == 'playing'
            && baseMoveChecks() && validPawnCapture() && validPromotion() && captureValid() && pieceMovedCorrectly() && myKingSafe();
      // En passant
      allow update: if request.resource.data.moveType == 'en_passant'
            && request.resource.data.status == 'playing'
            && baseMoveChecks() && validEnPassant()
            && request.resource.data.capturedPiece != '' && request.resource.data[request.resource.data.capturedPiece] == ''
            && pieceMovedCorrectly() && myKingSafe();
      // Standard piece move (non-pawn)
      allow update: if request.resource.data.moveType == 'normal'
            && request.resource.data.capturedPiece == '' && request.resource.data.status == 'playing'
            && baseMoveChecks() && validPieceMove() && resource.data[request.resource.data.moveTo] == ''
            && pieceMovedCorrectly() && myKingSafe();
      // Standard piece capture
      allow update: if request.resource.data.moveType == 'capture'
            && request.resource.data.capturedPiece != '' && request.resource.data.status == 'playing'
            && baseMoveChecks() && validPieceMove() && captureValid() && pieceMovedCorrectly() && myKingSafe();
      // Castling kingside
      allow update: if request.resource.data.moveType == 'castle_k'
            && request.resource.data.status == 'playing'
            && baseMoveChecks() && validCastleKingside() && myKingSafe();
      // Castling queenside
      allow update: if request.resource.data.moveType == 'castle_q'
            && request.resource.data.status == 'playing'
            && baseMoveChecks() && validCastleQueenside() && myKingSafe();
      // Draw
      allow update: if request.resource.data.moveType == 'draw'
            && request.resource.data.status == 'draw'
            && resource.data.status == 'playing' && isMyTurn();
      // Resign
      allow update: if request.resource.data.moveType == 'resign'
            && request.resource.data.status == 'resigned'
            && resource.data.status == 'playing'
            && request.auth != null
            && (request.auth.uid == resource.data.host || request.auth.uid == resource.data.guest);
    }
  }
}