diff --git a/modules/game/src/main/EngineConfig.scala b/modules/game/src/main/EngineConfig.scala index a3cfd35663ca8..2276dd5287051 100644 --- a/modules/game/src/main/EngineConfig.scala +++ b/modules/game/src/main/EngineConfig.scala @@ -1,8 +1,8 @@ package lila.game import shogi.format.forsyth.Sfen -import shogi.variant.Variant -import shogi.Handicap +import shogi.variant.{ Standard, Variant } +import shogi.{ Handicap, Role } case class EngineConfig( level: Int, @@ -41,7 +41,7 @@ object EngineConfig { if ( variant.standard && level.fold(true)(_ > 1) && initialSfen .filterNot(_.initialOf(variant)) - .fold(true)(sf => Handicap.isHandicap(sf, variant)) + .fold(true)(sf => Handicap.isHandicap(sf, variant) || reachableFromStandardInitialPosition(sf)) ) YaneuraOu else Fairy } @@ -52,4 +52,15 @@ object EngineConfig { engine = Engine(sfen, variant, level.some) ) + def reachableFromStandardInitialPosition(sfen: Sfen): Boolean = + sfen.toSituation(Standard).exists { sit => + val default = Standard.pieces.values.map(_.role) + def countHands(r: Role): Int = + ~Standard.handRoles.find(_ == r).map(hr => sit.hands.sente(hr) + sit.hands.gote(hr)) + sit.playable(strict = true, withImpasse = true) && + Standard.allRoles.filterNot(r => Standard.unpromote(r).isDefined).forall { r => + default + .count(_ == r) == (sit.board.count(r) + ~Standard.promote(r).map(sit.board.count) + countHands(r)) + } + } } diff --git a/ui/@types/lishogi/index.d.ts b/ui/@types/lishogi/index.d.ts index 568a08c66c31d..09bb835f585ca 100644 --- a/ui/@types/lishogi/index.d.ts +++ b/ui/@types/lishogi/index.d.ts @@ -251,8 +251,6 @@ interface Variant { name: string; } -declare type EngineCode = 'yn' | 'fs'; - interface Paginator { currentPage: number; maxPerPage: number; diff --git a/ui/ceval/src/ctrl.ts b/ui/ceval/src/ctrl.ts index d6cd72ad83163..a542eeed0343a 100644 --- a/ui/ceval/src/ctrl.ts +++ b/ui/ceval/src/ctrl.ts @@ -1,12 +1,11 @@ import { Result } from '@badrap/result'; import { prop } from 'common/common'; -import { engineName } from 'common/engineName'; +import { EngineCode, engineCode, engineName } from 'common/engineName'; import { isImpasse } from 'common/impasse'; import { isAndroid, isIOS, isIPad } from 'common/mobile'; import { storedProp } from 'common/storage'; import throttle from 'common/throttle'; import { parseSfen } from 'shogiops/sfen'; -import { Position } from 'shogiops/variant/position'; import { defaultPosition } from 'shogiops/variant/variant'; import { Cache } from './cache'; import { CevalCtrl, CevalOpts, CevalTechnology, Hovering, PvBoard, Started, Step, Work } from './types'; @@ -58,22 +57,6 @@ function enabledAfterDisable() { return enabledAfter === disable; } -function isStandardMaterial(pos: Position) { - const board = pos.board, - hands = pos.hands.color('sente').combine(pos.hands.color('gote')); - return ( - board.role('pawn').size() + board.role('tokin').size() + hands.get('pawn') <= 18 && - board.role('lance').size() + board.role('promotedlance').size() + hands.get('lance') <= 4 && - board.role('knight').size() + board.role('promotedknight').size() + hands.get('knight') <= 4 && - board.role('silver').size() + board.role('promotedsilver').size() + hands.get('silver') <= 4 && - board.role('gold').size() + hands.get('gold') <= 4 && - board.role('rook').size() + board.role('dragon').size() + hands.get('rook') <= 2 && - board.role('bishop').size() + board.role('horse').size() + hands.get('bishop') <= 2 && - board.role('king').size() == 2 && - board.role('king').intersect(board.color('sente')).size() === 1 - ); -} - export default function (opts: CevalOpts): CevalCtrl { const storageKey = (k: string) => { return opts.storageKeyPrefix ? `${opts.storageKeyPrefix}.${k}` : k; @@ -87,7 +70,9 @@ export default function (opts: CevalOpts): CevalCtrl { const analysable = pos.isOk && !unsupportedVariants.includes(opts.variant.key); // select nnue > hce > none - const useYaneuraou = opts.variant.key === 'standard' && (!analysable || isStandardMaterial(pos.value)), + const useYaneuraou = + (!analysable && opts.variant.key === 'standard') || + engineCode('standard', opts.initialSfen) === EngineCode.YaneuraOu, fairySupports = !useYaneuraou && analysable; let supportsNnue = false, technology: CevalTechnology = 'none', diff --git a/ui/common/src/engineName.ts b/ui/common/src/engineName.ts index 00d3581eb4878..5922f2f5434e1 100644 --- a/ui/common/src/engineName.ts +++ b/ui/common/src/engineName.ts @@ -1,24 +1,26 @@ import { Rules } from 'shogiops'; import { isHandicap } from 'shogiops/handicaps'; -import { initialSfen } from 'shogiops/sfen'; +import { initialSfen, parseSfen } from 'shogiops/sfen'; const useJp = document.documentElement.lang === 'ja-JP'; +export const enum EngineCode { + YaneuraOu = 'yn', + Fairy = 'fs', +} + // modules/game/src/main/EngineConfig.scala -export function engineName( - rules: Rules, - sfen: Sfen | undefined, - level?: number, - withLevel?: boolean, - trans?: Trans -): string { - const code: EngineCode = - rules === 'standard' && +export function engineCode(rules: Rules, sfen: Sfen | undefined, level?: number): EngineCode { + return rules === 'standard' && (!level || level > 1) && - (!sfen || initialSfen(rules) === sfen || isHandicap({ sfen, rules })) - ? 'yn' - : 'fs'; - return engineNameFromCode(code, withLevel ? level : undefined, trans); + (!sfen || initialSfen(rules) === sfen || isHandicap({ sfen, rules }) || isStandardMaterial(sfen)) + ? EngineCode.YaneuraOu + : EngineCode.Fairy; +} + +export function engineName(rules: Rules, sfen: Sfen | undefined, level?: number, trans?: Trans): string { + const code = engineCode(rules, sfen, level); + return engineNameFromCode(code, level, trans); } export function engineNameFromCode(code?: EngineCode, level?: number, trans?: Trans): string { @@ -26,3 +28,21 @@ export function engineNameFromCode(code?: EngineCode, level?: number, trans?: Tr if (level && trans) return name + ' - ' + trans('levelX', level); else return name; } + +function isStandardMaterial(sfen: Sfen): boolean { + const pos = parseSfen('standard', sfen); + if (pos.isErr) return false; + const board = pos.value.board, + hands = pos.value.hands.color('sente').combine(pos.value.hands.color('gote')); + return ( + board.role('pawn').size() + board.role('tokin').size() + hands.get('pawn') <= 18 && + board.role('lance').size() + board.role('promotedlance').size() + hands.get('lance') <= 4 && + board.role('knight').size() + board.role('promotedknight').size() + hands.get('knight') <= 4 && + board.role('silver').size() + board.role('promotedsilver').size() + hands.get('silver') <= 4 && + board.role('gold').size() + hands.get('gold') <= 4 && + board.role('rook').size() + board.role('dragon').size() + hands.get('rook') <= 2 && + board.role('bishop').size() + board.role('horse').size() + hands.get('bishop') <= 2 && + board.role('king').size() == 2 && + board.role('king').intersect(board.color('sente')).size() === 1 + ); +} diff --git a/ui/game/src/interfaces.ts b/ui/game/src/interfaces.ts index a780b84a8925d..8deeb80f61b46 100644 --- a/ui/game/src/interfaces.ts +++ b/ui/game/src/interfaces.ts @@ -1,3 +1,5 @@ +import { EngineCode } from 'common/engineName'; + export interface GameData { game: Game; player: Player; diff --git a/ui/puzzle/src/interfaces.ts b/ui/puzzle/src/interfaces.ts index ef860244f436c..d4fe6fa90389e 100644 --- a/ui/puzzle/src/interfaces.ts +++ b/ui/puzzle/src/interfaces.ts @@ -1,6 +1,7 @@ import { CevalCtrl, NodeEvals } from 'ceval'; import { Prop } from 'common/common'; import { Deferred } from 'common/defer'; +import { EngineCode } from 'common/engineName'; import { StoredBooleanProp } from 'common/storage'; import { Api as SgApi } from 'shogiground/api'; import { Config as SgConfig } from 'shogiground/config';