···2929 // themes: a list of puzzle themes.
3030 Themes []string `json:"themes,omitempty" cborgen:"themes,omitempty"`
3131}
3232+3333+// PuzzleDefs_ThemeView is a "themeView" in the org.lichess.puzzle.defs schema.
3434+//
3535+// A puzzle theme with its display name and description.
3636+type PuzzleDefs_ThemeView struct {
3737+ // description: a sentence describing the theme.
3838+ Description string `json:"description" cborgen:"description"`
3939+ // key: the theme identifier used in puzzle themes and the getRandom themes filter (e.g. "fork").
4040+ Key string `json:"key" cborgen:"key"`
4141+ // name: the human-readable display name of the theme (e.g. "Fork").
4242+ Name string `json:"name" cborgen:"name"`
4343+}
···11+<?xml version="1.0" encoding="UTF-8"?>
22+<resources>
33+ <string name="advancedPawn">Advanced pawn</string>
44+ <string name="advancedPawnDescription">One of your pawns is deep into the opponent position, maybe threatening to promote.</string>
55+ <string name="advantage">Advantage</string>
66+ <string name="advantageDescription">Seize your chance to get a decisive advantage. (200cp ≤ eval ≤ 600cp)</string>
77+ <string name="anastasiaMate">Anastasia's mate</string>
88+ <string name="anastasiaMateDescription">A knight and rook or queen team up to trap the opposing king between the side of the board and a friendly piece.</string>
99+ <string name="arabianMate">Arabian mate</string>
1010+ <string name="arabianMateDescription">A knight and a rook team up to trap the opposing king on a corner of the board.</string>
1111+ <string name="attackingF2F7">Attacking f2 or f7</string>
1212+ <string name="attackingF2F7Description">An attack focusing on the f2 or f7 pawn, such as in the fried liver opening.</string>
1313+ <string name="attraction">Attraction</string>
1414+ <string name="attractionDescription">An exchange or sacrifice encouraging or forcing an opponent piece to a square that allows a follow-up tactic.</string>
1515+ <string name="backRankMate">Back rank mate</string>
1616+ <string name="backRankMateDescription">Checkmate the king on the home rank, when it is trapped there by its own pieces.</string>
1717+ <string name="balestraMate">Balestra mate</string>
1818+ <string name="balestraMateDescription">A bishop delivers the checkmate, while a queen blocks the remaining escape squares</string>
1919+ <string name="blindSwineMate">Blind Swine mate</string>
2020+ <string name="blindSwineMateDescription">Two rooks team up to mate the king in an area of 2 by 2 squares.</string>
2121+ <string name="bishopEndgame">Bishop endgame</string>
2222+ <string name="bishopEndgameDescription">An endgame with only bishops and pawns.</string>
2323+ <string name="bodenMate">Boden's mate</string>
2424+ <string name="bodenMateDescription">Two attacking bishops on criss-crossing diagonals deliver mate to a king obstructed by friendly pieces.</string>
2525+ <string name="castling">Castling</string>
2626+ <string name="castlingDescription">Bring the king to safety, and deploy the rook for attack.</string>
2727+ <string name="capturingDefender">Capture the defender</string>
2828+ <string name="capturingDefenderDescription">Removing a piece that is critical to defence of another piece, allowing the now undefended piece to be captured on a following move.</string>
2929+ <string name="collinearMove">Collinear move</string>
3030+ <string name="collinearMoveDescription">Two opposing pieces face each other, and one slides along the line of attack without capturing the enemy piece.</string>
3131+ <string name="cornerMate">Corner mate</string>
3232+ <string name="cornerMateDescription">Confine the king to the corner using a rook or queen and a knight to engage the checkmate.</string>
3333+ <string name="crushing">Crushing</string>
3434+ <string name="crushingDescription">Spot the opponent blunder to obtain a crushing advantage. (eval ≥ 600cp)</string>
3535+ <string name="discoveredCheck">Discovered check</string>
3636+ <string name="discoveredCheckDescription">Move a piece to reveal a check from a hidden attacking piece, which often leads to a decisive advantage.</string>
3737+ <string name="doubleBishopMate">Double bishop mate</string>
3838+ <string name="doubleBishopMateDescription">Two attacking bishops on adjacent diagonals deliver mate to a king obstructed by friendly pieces.</string>
3939+ <string name="dovetailMate">Dovetail mate</string>
4040+ <string name="dovetailMateDescription">A queen delivers mate to an adjacent king, whose only two escape squares are obstructed by friendly pieces.</string>
4141+ <string name="equality">Equality</string>
4242+ <string name="equalityDescription">Come back from a losing position, and secure a draw or a balanced position. (eval ≤ 200cp)</string>
4343+ <string name="kingsideAttack">Kingside attack</string>
4444+ <string name="kingsideAttackDescription">An attack of the opponent's king, after they castled on the king side.</string>
4545+ <string name="clearance">Clearance</string>
4646+ <string name="clearanceDescription">A move, often with tempo, that clears a square, file or diagonal for a follow-up tactical idea.</string>
4747+ <string name="defensiveMove">Defensive move</string>
4848+ <string name="defensiveMoveDescription">A precise move or sequence of moves that is needed to avoid losing material or another advantage.</string>
4949+ <string name="deflection">Deflection</string>
5050+ <string name="deflectionDescription">A move that distracts an opposing piece from another duty that it performs, such as guarding a key square. Sometimes also called "overloading".</string>
5151+ <string name="discoveredAttack">Discovered attack</string>
5252+ <string name="discoveredAttackDescription">Moving a piece (such as a knight), that previously blocked an attack by a long range piece (such as a rook), out of the way of that piece.</string>
5353+ <string name="doubleCheck">Double check</string>
5454+ <string name="doubleCheckDescription">Checking with two pieces at once, as a result of a discovered attack where both the moving piece and the unveiled piece attack the opponent's king.</string>
5555+ <string name="endgame">Endgame</string>
5656+ <string name="endgameDescription">A tactic during the last phase of the game.</string>
5757+ <string name="enPassantDescription">A tactic involving the en passant rule, where a pawn can capture an opponent pawn that has bypassed it using its initial two-square move.</string>
5858+ <string name="epauletteMate">Epaulette mate</string>
5959+ <string name="epauletteMateDescription">Two adjacent escape squares for a checked king are occupied by other pieces.</string>
6060+ <string name="exposedKing">Exposed king</string>
6161+ <string name="exposedKingDescription">A tactic involving a king with few defenders around it, often leading to checkmate.</string>
6262+ <string name="fork">Fork</string>
6363+ <string name="forkDescription">A move where the moved piece attacks two opponent pieces at once.</string>
6464+ <string name="hangingPiece">Hanging piece</string>
6565+ <string name="hangingPieceDescription">A tactic involving an opponent piece being undefended or insufficiently defended and free to capture.</string>
6666+ <string name="hookMate">Hook mate</string>
6767+ <string name="hookMateDescription">Checkmate with a rook, knight, and pawn along with one enemy pawn to limit the enemy king's escape.</string>
6868+ <string name="interference">Interference</string>
6969+ <string name="interferenceDescription">Moving a piece between two opponent pieces to leave one or both opponent pieces undefended, such as a knight on a defended square between two rooks.</string>
7070+ <string name="intermezzo">Intermezzo</string>
7171+ <string name="intermezzoDescription">Instead of playing the expected move, first interpose another move posing an immediate threat that the opponent must answer. Also known as "Zwischenzug" or "In between".</string>
7272+ <string name="killBoxMate">Kill box mate</string>
7373+ <string name="killBoxMateDescription">A rook is next to the enemy king and supported by a queen that also blocks the king's escape squares. The rook and the queen catch the enemy king in a 3 by 3 "kill box".</string>
7474+ <string name="pillsburysMate">Pillsbury's mate</string>
7575+ <string name="pillsburysMateDescription">The rook delivers checkmate, while the bishop helps to confine it.</string>
7676+ <string name="morphysMate">Morphy's mate</string>
7777+ <string name="morphysMateDescription">Use the bishop to check the king, while your rook helps to confine it.</string>
7878+ <string name="swallowstailMate">Swallow's tail mate</string>
7979+ <string name="swallowstailMateDescription">A checkmate pattern that visually resembles the appearance of a swallow’s tail, similar to a V shape.</string>
8080+ <string name="triangleMate">Triangle mate</string>
8181+ <string name="triangleMateDescription">The queen and rook, one square away from the enemy king, are on the same rank or file, separated by one square, forming a triangle.</string>
8282+ <string name="vukovicMate">Vuković mate</string>
8383+ <string name="vukovicMateDescription">A rook and knight team up to mate the king. The rook delivers mate while supported by a third piece, and the knight is used to block the king's escape squares.</string>
8484+ <string name="knightEndgame">Knight endgame</string>
8585+ <string name="knightEndgameDescription">An endgame with only knights and pawns.</string>
8686+ <string name="long">Long puzzle</string>
8787+ <string name="longDescription">Three moves to win.</string>
8888+ <string name="master">Master games</string>
8989+ <string name="masterDescription">Puzzles from games played by titled players.</string>
9090+ <string name="masterVsMaster">Master vs Master games</string>
9191+ <string name="masterVsMasterDescription">Puzzles from games between two titled players.</string>
9292+ <string name="mate">Checkmate</string>
9393+ <string name="mateDescription">Win the game with style.</string>
9494+ <string name="mateIn1">Mate in 1</string>
9595+ <string name="mateIn1Description">Deliver checkmate in one move.</string>
9696+ <string name="mateIn2">Mate in 2</string>
9797+ <string name="mateIn2Description">Deliver checkmate in two moves.</string>
9898+ <string name="mateIn3">Mate in 3</string>
9999+ <string name="mateIn3Description">Deliver checkmate in three moves.</string>
100100+ <string name="mateIn4">Mate in 4</string>
101101+ <string name="mateIn4Description">Deliver checkmate in four moves.</string>
102102+ <string name="mateIn5">Mate in 5 or more</string>
103103+ <string name="mateIn5Description">Figure out a long mating sequence.</string>
104104+ <string name="middlegame">Middlegame</string>
105105+ <string name="middlegameDescription">A tactic during the second phase of the game.</string>
106106+ <string name="oneMove">One-move puzzle</string>
107107+ <string name="oneMoveDescription">A puzzle that is only one move long.</string>
108108+ <string name="opening">Opening</string>
109109+ <string name="openingDescription">A tactic during the first phase of the game.</string>
110110+ <string name="operaMate">Opera mate</string>
111111+ <string name="operaMateDescription">Check the king with a rook and use a bishop to defend the rook.</string>
112112+ <string name="pawnEndgame">Pawn endgame</string>
113113+ <string name="pawnEndgameDescription">An endgame with only pawns.</string>
114114+ <string name="pin">Pin</string>
115115+ <string name="pinDescription">A tactic involving pins, where a piece is unable to move without revealing an attack on a higher value piece.</string>
116116+ <string name="promotion">Promotion</string>
117117+ <string name="promotionDescription">Promote one of your pawn to a queen or minor piece.</string>
118118+ <string name="queenEndgame">Queen endgame</string>
119119+ <string name="queenEndgameDescription">An endgame with only queens and pawns.</string>
120120+ <string name="queenRookEndgame">Queen and Rook</string>
121121+ <string name="queenRookEndgameDescription">An endgame with only queens, rooks and pawns.</string>
122122+ <string name="queensideAttack">Queenside attack</string>
123123+ <string name="queensideAttackDescription">An attack of the opponent's king, after they castled on the queen side.</string>
124124+ <string name="quietMove">Quiet move</string>
125125+ <string name="quietMoveDescription">A move that does not check, capture, or create an immediate threat to capture. Instead, it prepares a hidden and unavoidable threat for a later move.</string>
126126+ <string name="rookEndgame">Rook endgame</string>
127127+ <string name="rookEndgameDescription">An endgame with only rooks and pawns.</string>
128128+ <string name="sacrifice">Sacrifice</string>
129129+ <string name="sacrificeDescription">A tactic involving giving up material in the short-term, to gain an advantage again after a forced sequence of moves.</string>
130130+ <string name="short">Short puzzle</string>
131131+ <string name="shortDescription">Two moves to win.</string>
132132+ <string name="skewer">Skewer</string>
133133+ <string name="skewerDescription">A motif involving a high value piece being attacked, moving out the way, and allowing a lower value piece behind it to be captured or attacked, the inverse of a pin.</string>
134134+ <string name="smotheredMate">Smothered mate</string>
135135+ <string name="smotheredMateDescription">A checkmate delivered by a knight in which the mated king is unable to move because it is surrounded (or smothered) by its own pieces.</string>
136136+ <string name="superGM">Super GM games</string>
137137+ <string name="superGMDescription">Puzzles from games played by the best players in the world.</string>
138138+ <string name="trappedPiece">Trapped piece</string>
139139+ <string name="trappedPieceDescription">A piece is unable to escape capture as it has limited moves.</string>
140140+ <string name="underPromotion">Underpromotion</string>
141141+ <string name="underPromotionDescription">Promotion to a knight, bishop, or rook.</string>
142142+ <string name="veryLong">Very long puzzle</string>
143143+ <string name="veryLongDescription">Four moves or more to win.</string>
144144+ <string name="xRayAttack">X-Ray attack</string>
145145+ <string name="xRayAttackDescription">A piece attacks or defends a square, through an enemy piece.</string>
146146+ <string name="zugzwang">Zugzwang</string>
147147+ <string name="zugzwangDescription">The opponent is limited in the moves they can make, and all moves worsen their position.</string>
148148+ <string name="mix">Healthy mix</string>
149149+ <string name="mixDescription">A bit of everything. You don't know what to expect, so be ready for anything! Just like in real games.</string>
150150+ <string name="playerGames">Player games</string>
151151+ <string name="playerGamesDescription">View puzzles generated from your games, or from another player's games</string>
152152+ <string name="puzzleDownloadInformation">These puzzles are in the public domain, and can be downloaded from %s.</string>
153153+</resources>
···2424func (Puzzle) TableName() string {
2525 return "puzzles"
2626}
2727+2828+// PuzzleTheme is a normalized (theme, puzzle) join row used for indexed theme
2929+// filtering, avoiding a LIKE scan over the puzzles' JSON themes column. The
3030+// composite primary key is ordered (theme, puzzle_id) so it doubles as the
3131+// theme-lookup index and gives uniqueness for OnConflict on re-import.
3232+type PuzzleTheme struct {
3333+ Theme string `gorm:"column:theme;primaryKey"`
3434+ PuzzleID string `gorm:"column:puzzle_id;primaryKey"`
3535+}
3636+3737+// TableName overrides the default GORM pluralization.
3838+func (PuzzleTheme) TableName() string {
3939+ return "puzzle_themes"
4040+}
4141+4242+// Theme is the reference mapping of a Lichess theme key to its display name and
4343+// description, seeded from the bundled strings XML.
4444+type Theme struct {
4545+ Key string `gorm:"column:key;primaryKey"`
4646+ Name string `gorm:"column:name"`
4747+ Description string `gorm:"column:description"`
4848+}
4949+5050+// TableName overrides the default GORM pluralization.
5151+func (Theme) TableName() string {
5252+ return "themes"
5353+}
···44 "errors"
55 "net/http"
66 "strconv"
77- "strings"
8798 "github.com/labstack/echo/v5"
109 "gorm.io/gorm"
···6160 return c.JSON(http.StatusOK, toPuzzleView(p))
6261}
63626363+// getThemes implements the org.lichess.puzzle.getThemes XRPC query: it returns
6464+// every puzzle theme with its display name and description from the themes
6565+// reference table. The keys are the values accepted by getRandom's themes filter.
6666+func (s *Server) getThemes(c *echo.Context) error {
6767+ var themes []models.Theme
6868+ if err := s.db.Order("key").Find(&themes).Error; err != nil {
6969+ c.Logger().Error("getThemes db lookup failed", "err", err)
7070+ return xrpcError(c, http.StatusInternalServerError, "InternalServerError", "failed to load themes")
7171+ }
7272+7373+ views := make([]*lichess.PuzzleDefs_ThemeView, 0, len(themes))
7474+ for _, t := range themes {
7575+ views = append(views, &lichess.PuzzleDefs_ThemeView{
7676+ Key: t.Key,
7777+ Name: t.Name,
7878+ Description: t.Description,
7979+ })
8080+ }
8181+8282+ return c.JSON(http.StatusOK, &lichess.PuzzleGetThemes_Output{Themes: views})
8383+}
8484+6485// getRandomPuzzle implements the org.lichess.getRandomPuzzle XRPC query. It returns a
6586// single random puzzle, optionally filtered by a target rating (within ±ratingBand) and
6687// by themes (matching at least one). With no parameters it returns a truly random puzzle.
···80101 }
8110282103 // themes: optional, repeatable (?themes=a&themes=b); match puzzles containing at
8383- // least one. Themes are stored as a JSON-array text column, so each token is quoted
8484- // to avoid substring false-positives (e.g. "mate" inside "mateIn2").
104104+ // least one. Filtering uses the normalized puzzle_themes join table via an EXISTS
105105+ // probe, an index-only lookup on its (theme, puzzle_id) primary key.
85106 if themes := c.QueryParams()["themes"]; len(themes) > 0 {
8686- clauses := make([]string, 0, len(themes))
8787- args := make([]any, 0, len(themes))
8888- for _, t := range themes {
8989- clauses = append(clauses, "themes LIKE ?")
9090- args = append(args, "%\""+t+"\"%")
9191- }
9292- q = q.Where("("+strings.Join(clauses, " OR ")+")", args...)
107107+ q = q.Where(
108108+ "EXISTS (SELECT 1 FROM puzzle_themes pt WHERE pt.puzzle_id = puzzles.puzzle_id AND pt.theme IN ?)",
109109+ themes,
110110+ )
93111 filtered = true
94112 }
95113