Skip to content

Commit

Permalink
Fog fen (#1742)
Browse files Browse the repository at this point in the history
* Use sf.get_fog_fen()

* Cached get_fog_fen()

* black

* Fix fog piece regression

* Typo fix

* Prevent leaking fogofwar info

* Completely get rid of fog FEN creation on client side
  • Loading branch information
gbtami authored Jan 4, 2025
1 parent 4501a34 commit 855675a
Show file tree
Hide file tree
Showing 18 changed files with 129 additions and 89 deletions.
6 changes: 4 additions & 2 deletions client/cgCtrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { FairyStockfish, Board, Notation } from 'ffish-es6';
import { boardSettings, BoardController } from '@/boardSettings';
import { CGMove, uci2cg } from '@/chess';
import { BoardName, PyChessModel } from '@/types';
import { Variant, VARIANTS, moddedVariant } from '@/variants';
import { fogFen, Variant, VARIANTS, moddedVariant } from '@/variants';

export abstract class ChessgroundController implements BoardController {
boardName: BoardName;
Expand All @@ -27,6 +27,7 @@ export abstract class ChessgroundController implements BoardController {

fullfen: string;
notation: cg.Notation;
fog: boolean;

constructor(el: HTMLElement, model: PyChessModel, fullfen: string, pocket0: HTMLElement, pocket1: HTMLElement, boardName: BoardName = '') {
this.boardName = boardName;
Expand All @@ -40,9 +41,10 @@ export abstract class ChessgroundController implements BoardController {
this.oppcolor = 'black';
this.fullfen = fullfen;
this.notation = this.variant.notation;
this.fog = model.variant === 'fogofwar';

const parts = this.fullfen.split(" ");
const fen_placement: cg.FEN = parts[0];
const fen_placement: cg.FEN = (this.fog) ? fogFen(parts[0]) : parts[0];

this.chessground = Chessground(el, {
fen: fen_placement as cg.FEN,
Expand Down
1 change: 0 additions & 1 deletion client/chess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Variant, variantGroups } from './variants';

export const WHITE = 0;
export const BLACK = 1;
export const DARK_FEN = "*~*~*~*~*~*~*~*~/*~*~*~*~*~*~*~*~/*~*~*~*~*~*~*~*~/*~*~*~*~*~*~*~*~/*~*~*~*~*~*~*~*~/*~*~*~*~*~*~*~*~/*~*~*~*~*~*~*~*~/*~*~*~*~*~*~*~*~ w KQkq - 0 1"

export const ranksUCI = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] as const;
export type UCIRank = typeof ranksUCI[number];
Expand Down
43 changes: 3 additions & 40 deletions client/gameCtrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import { WebsocketHeartbeatJs } from './socket/socket';

import { h, VNode } from 'snabbdom';
import * as Mousetrap from 'mousetrap';
import * as fen from 'chessgroundx/fen';
import * as cg from 'chessgroundx/types';
import * as util from 'chessgroundx/util';

import { _ } from './i18n';
import { patch } from './document';
import { Step, MsgChat, MsgFullChat, MsgSpectators, MsgShutdown,MsgGameNotFound } from './messages';
import { adjacent, DARK_FEN, uci2LastMove, moveDests, cg2uci, uci2cg, unpromotedRole, UCIMove } from './chess';
import { adjacent, uci2LastMove, moveDests, cg2uci, unpromotedRole, UCIMove } from './chess';
import { InputType } from '@/input/input';
import { GatingInput } from './input/gating';
import { PromotionInput } from './input/promotion';
Expand All @@ -21,7 +20,7 @@ import { sound } from './sound';
import { chatMessage, ChatController } from './chat';
import { selectMove } from './movelist';
import { Api } from "chessgroundx/api";
import { Variant } from "@/variants";
import { fogFen, Variant } from "./variants";
import { CheckCounterSvg, Counter } from './glyphs';

export abstract class GameController extends ChessgroundController implements ChatController {
Expand All @@ -37,7 +36,6 @@ export abstract class GameController extends ChessgroundController implements Ch
aiLevel: number;
rated: string;
corr : boolean;
fog: boolean;

base: number;
inc: number;
Expand Down Expand Up @@ -116,7 +114,6 @@ export abstract class GameController extends ChessgroundController implements Ch
this.brating = model["brating"];
this.rated = model["rated"];
this.corr = model["corr"] === 'True';
this.fog = this.variant.name === 'fogofwar';
this.mirrorBoard = false;

this.spectator = this.username !== this.wplayer && this.username !== this.bplayer;
Expand Down Expand Up @@ -221,40 +218,6 @@ export abstract class GameController extends ChessgroundController implements Ch
}
}

fogFen(currentFen: string): string {
// No king, no fog (game is over)
if (!currentFen.includes('k') || !currentFen.includes('K') || this.result !== '*') return currentFen;

if (this.spectator) return DARK_FEN;

// Squares visibility is always calculated from my color turn perspective
const parts = currentFen.split(' ');
this.ffishBoard.setFen([parts[0], this.mycolor[0], parts[2], parts[3]].join(' '));
const legalMoves = this.ffishBoard.legalMoves().split(" ");

const pieces = fen.read(currentFen, this.variant.board.dimensions).pieces;
const myPieceKeys = Array.from(pieces.keys()).filter((key) => pieces.get(key)!.color === this.mycolor);
const visibleKeys = new Set(myPieceKeys);

// Add dest squares to visibleKeys
legalMoves.map(uci2cg).forEach(move => {
visibleKeys.add(move.slice(2, 4) as cg.Key);
});

// We use promoted block pieces as fog to let them style differently in extension.css
const fog = {
color: this.oppcolor,
role: '_-piece' as cg.Role,
promoted: true
}
const darks: cg.Key[] = util.allKeys(this.variant.board.dimensions).filter((key) => !(visibleKeys.has(key)));
const darkPieces: [cg.Key, cg.Piece][] = darks.map((key) => [key, fog]);
const visiblePieces: [cg.Key, cg.Piece][] = Array.from(visibleKeys).filter((key) => pieces.get(key)).map((key) => [key, pieces.get(key)!]);
const newPieces: cg.Pieces = new Map(darkPieces.concat(visiblePieces));

return fen.writeBoard(newPieces, this.variant.board.dimensions);
}

abstract toggleSettings(): void;

abstract doSendMove(move: string): void;
Expand Down Expand Up @@ -355,7 +318,7 @@ export abstract class GameController extends ChessgroundController implements Ch

const fen = (this.mirrorBoard) ? this.getAliceFen(step.fen) : step.fen;
this.chessground.set({
fen: (this.fog) ? this.fogFen(fen) : fen,
fen: (this.fog) ? fogFen(fen) : fen,
turnColor: step.turnColor,
movable: {
color: step.turnColor,
Expand Down
4 changes: 2 additions & 2 deletions client/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type Games = Map<string, GameData>;
function gameView(games: Games, game: Game) {
const variant = VARIANTS[game.variant];
let lastMove, fen;
[lastMove, fen] = getLastMoveFen(variant.name, game.lastMove, game.fen, '*')
[lastMove, fen] = getLastMoveFen(variant.name, game.lastMove, game.fen)
return h(`minigame#${game.gameId}.${variant.boardFamily}.${variant.pieceFamily}.${variant.ui.boardMark}`, {
class: {
"with-pockets": !!variant.pocket,
Expand Down Expand Up @@ -97,7 +97,7 @@ export function renderGames(model: PyChessModel): VNode[] {
let cg, variantName;
[cg, variantName] = gameData;
let lastMove, fen;
[lastMove, fen] = getLastMoveFen(variantName, message.lastMove, message.fen, '*')
[lastMove, fen] = getLastMoveFen(variantName, message.lastMove, message.fen)
cg.set({
fen: fen,
lastMove: lastMove,
Expand Down
4 changes: 2 additions & 2 deletions client/nowPlaying.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function handleOngoingGameEvents(username: string, cgMap: {[gameId: strin
[cg, variantName] = cgMap[message.gameId];

let lastMove, fen;
[lastMove, fen] = getLastMoveFen(variantName, message.lastMove, message.fen, '*')
[lastMove, fen] = getLastMoveFen(variantName, message.lastMove, message.fen)

cg.set({
fen: fen,
Expand Down Expand Up @@ -95,7 +95,7 @@ export function gameViewPlaying(cgMap: {[gameId: string]: [Api, string]}, game:
const mycolor = (username === game.w) ? 'white' : 'black';

let lastMove, fen;
[lastMove, fen] = getLastMoveFen(variant.name, game.lastMove, game.fen, '*')
[lastMove, fen] = getLastMoveFen(variant.name, game.lastMove, game.fen)

return h(`a.${variant.boardFamily}.${variant.pieceFamily}.${variant.ui.boardMark}`, { attrs: { href: game.gameId } }, [
h(`div.cg-wrap.${variant.board.cg}`, {
Expand Down
2 changes: 1 addition & 1 deletion client/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function renderGames(model: PyChessModel, games: Game[]) {
teamSecond = game["us"][2] + "+" + game["us"][1];
}
let lastMove, fen;
[lastMove, fen] = getLastMoveFen(variant.name, game.lm, game.f, game.r)
[lastMove, fen] = getLastMoveFen(variant.name, game.lm, game.f)
return h('tr', [h('a', { attrs: { href : '/' + game["_id"] } }, [
h('td.board', { class: { "with-pockets": !!variant.pocket, "bug": isBug} },
isBug? renderGameBoardsBug(game, model["profileid"]): [
Expand Down
11 changes: 6 additions & 5 deletions client/roundCtrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { patch } from './document';
import { boardSettings } from './boardSettings';
import { Clock } from './clock';
import { sound } from './sound';
import { DARK_FEN, WHITE, BLACK, uci2LastMove, getCounting, isHandicap } from './chess';
import { fogFen } from "./variants";
import { WHITE, BLACK, uci2LastMove, getCounting, isHandicap } from './chess';
import { crosstableView } from './crosstable';
import { chatMessage, chatView } from './chat';
import { createMovelistButtons, updateMovelist, updateResult, selectMove } from './movelist';
Expand Down Expand Up @@ -884,7 +885,7 @@ export class RoundController extends GameController {
if (this.spectator) {
if (latestPly) {
this.chessground.set({
fen: (this.fog) ? DARK_FEN : this.fullfen,
fen: (this.fog) ? fogFen(this.fullfen) : this.fullfen,
turnColor: this.turnColor,
check: msg.check,
lastMove: (this.fog) ? undefined : lastMove,
Expand All @@ -906,7 +907,7 @@ export class RoundController extends GameController {
if (this.turnColor === this.mycolor) {
if (latestPly) {
this.chessground.set({
fen: (this.fog) ? this.fogFen(this.fullfen) : this.fullfen,
fen: (this.fog) ? fogFen(this.fullfen) : this.fullfen,
turnColor: this.turnColor,
movable: {
free: false,
Expand Down Expand Up @@ -941,10 +942,10 @@ export class RoundController extends GameController {
} else {
this.chessground.set({
// giving fen here will place castling rooks to their destination in chess960 variants
fen: (this.fog) ? this.fogFen(this.fullfen) : parts[0],
fen: (this.fog) ? fogFen(this.fullfen) : parts[0],
turnColor: this.turnColor,
check: msg.check,
lastMove: (this.fog) ? undefined : lastMove,
lastMove: lastMove,
});

// This have to be here, because in case of takeback
Expand Down
4 changes: 2 additions & 2 deletions client/tournament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ export class TournamentController implements ChatController {
hook: {
insert: vnode => {
let lastMove, fen;
[lastMove, fen] = getLastMoveFen(this.variant.name, game.lastMove, game.fen, '*')
[lastMove, fen] = getLastMoveFen(this.variant.name, game.lastMove, game.fen)
const cg = Chessground(vnode.elm as HTMLElement, {
fen: fen,
lastMove: lastMove,
Expand Down Expand Up @@ -620,7 +620,7 @@ export class TournamentController implements ChatController {
return;
};
let lastMove, fen;
[lastMove, fen] = getLastMoveFen(this.variant.name, msg.lastMove, msg.fen, msg.result)
[lastMove, fen] = getLastMoveFen(this.variant.name, msg.lastMove, msg.fen)

this.topGameChessground.set({
fen: fen,
Expand Down
17 changes: 8 additions & 9 deletions client/variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { h, InsertHook, VNode } from 'snabbdom';
import * as cg from 'chessgroundx/types';
import * as util from 'chessgroundx/util';

import { DARK_FEN, BoardMarkType, ColorName, CountingType, MaterialPointType, PieceSoundType, PromotionSuffix, PromotionType, TimeControlType, uci2LastMove } from './chess';
import { BoardMarkType, ColorName, CountingType, MaterialPointType, PieceSoundType, PromotionSuffix, PromotionType, TimeControlType, uci2LastMove } from './chess';
import { _ } from './i18n';
import { calculateDiff, Equivalence, MaterialDiff } from './material';

Expand Down Expand Up @@ -1165,12 +1165,11 @@ export function moddedVariant(variantName: string, chess960: boolean, pieces: cg
return variantName;
}

export function getLastMoveFen(variantName: string, lastMove: string, fen: string, result: string): [cg.Orig[] | undefined, string] {
switch (variantName) {
case 'fogofwar':
// Prevent leaking ongoing game info
return [undefined, (result === "*") ? DARK_FEN : fen];
default:
return [uci2LastMove(lastMove), fen];
}
export function getLastMoveFen(variantName: string, lastMove: string, fen: string): [cg.Orig[] | undefined, string] {
return [uci2LastMove(lastMove), variantName === 'fogofwar' ? fogFen(fen) : fen];
}

// Replace all brick ("*") pieces to be promoted ("*~") to let them CSS style as fog instead of duck
export function fogFen(currentFen: string): string {
return currentFen.replace(/\*/g, '*~');
}
5 changes: 3 additions & 2 deletions client/variantsIni.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,5 +286,6 @@ immobilityIllegal = true
king = -
commoner = k
castlingKingPiece = k
extinctionValue = loss
extinctionPieceTypes = k`
# extinction rules prevents to get valid moves for fog FENs ceated on server side
#extinctionValue = loss
#extinctionPieceTypes = k`
2 changes: 1 addition & 1 deletion server/bug/game_bug.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ async def game_ended(self, user, reason):
),
}

def get_board(self, full=False):
def get_board(self, full=False, persp_color=None):
[clocks_a, clocks_b] = self.gameClocks.get_clocks_for_board_msg(full)
if full:
steps = self.steps
Expand Down
1 change: 1 addition & 0 deletions server/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
LOOKING_GLASS_ALICE_FEN = "|r|n|b|q|k|b|n|r/|p|p|p|p|p|p|p|p/8/8/8/8/PPPPPPPP/RNBQKBNR w KQ - 0 1"
MANCHU_FEN = "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/9/9/M1BAKAB2 w - - 0 1"
MANCHU_R_FEN = "m1bakab1r/9/9/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1"
DARK_FEN = "********/********/********/********/********/********/********/******** w - - 0 1"

VARIANTS = (
"chess",
Expand Down
29 changes: 29 additions & 0 deletions server/fairy.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import logging
import re
import random
from functools import cache

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -412,6 +413,34 @@ def shuffle_start(variant):
return fen


@cache
def get_fog_fen(fen, persp_color):
parts = fen.split(" ")

fen_color = "w" if parts[1] == "w" else "b"
opp_color = "w" if persp_color == WHITE else "b"

# set the perspective color to sf.get_fog_fen()
parts[1] = parts[1].replace(fen_color, opp_color)

# remove castling rights of the player in fog
# because the resulting fog FEN may have no king
if persp_color == WHITE:
parts[2] = "".join((letter for letter in parts[2] if letter.isupper()))
else:
parts[2] = "".join((letter for letter in parts[2] if letter.islower()))
fen = " ".join(parts)

fen = sf.get_fog_fen(fen, "fogofwar")

# restore original FEN color
parts = fen.split(" ")
parts[1] = parts[1].replace(opp_color, fen_color)
fen = " ".join(parts)

return fen


def get_san_moves(variant, fen, mlist, chess960, notation):
if variant == "alice":
return sf_alice.get_san_moves(variant, fen, mlist, chess960, notation)
Expand Down
Loading

0 comments on commit 855675a

Please sign in to comment.