diff --git a/src/renderer/components/AboutTabModal.vue b/src/renderer/components/AboutTabModal.vue index 7e26e0b3..d218113f 100644 --- a/src/renderer/components/AboutTabModal.vue +++ b/src/renderer/components/AboutTabModal.vue @@ -130,7 +130,7 @@ export default { display: flex; justify-content: center; align-items: center; - z-index: 10; + z-index: 10000; } .backdrop { position: absolute; @@ -147,6 +147,9 @@ export default { background: var(--second-bg-color); box-shadow: 2px 2px 20px 1px var(--second-bg-color); overflow-x: 10px; + width: 600px; + max-height: 80vh; + overflow-y: auto; } .item { margin: 5px 0; diff --git a/src/renderer/components/AnalysisContainer.vue b/src/renderer/components/AnalysisContainer.vue index 57eb2331..9646105c 100644 --- a/src/renderer/components/AnalysisContainer.vue +++ b/src/renderer/components/AnalysisContainer.vue @@ -93,23 +93,6 @@ /> - -
LiGround + +
+ +
+ + +
+
+ +
+
+ +
+ + + +
+ + -
- + + +
+ + + +
- + + + + + + + + + + -
- PvE - -
-
- PvE - + + +
+
+
+

PGN Browser

+ +
+
+ +
+
+ + +
diff --git a/src/renderer/store.js b/src/renderer/store.js index 725137f9..77dceebc 100644 --- a/src/renderer/store.js +++ b/src/renderer/store.js @@ -1,7 +1,7 @@ import Vue from 'vue' import Vuex from 'vuex' import ffish from 'ffish' -import { engine } from './engine' +import { engine, Engine } from './engine' import allEngines from './store/engines' import moveAudio from './assets/audio/Move.mp3' @@ -110,6 +110,7 @@ export const store = new Vuex.Store({ initialized: false, active: false, PvE: false, + PvEPlayerIsWhite: true, // true when the human player controls White in PvE mode PvEParam: 'go movetime 1000', PvEValue: 'time', PvEInput: 1000, @@ -129,6 +130,12 @@ export const store = new Vuex.Store({ legalMoves: '', destinations: {}, variant: 'chess', + + // Engine-vs-Engine state + EvE: false, + EvEConfig: null, + engineWhiteInstance: null, + engineBlackInstance: null, variantOptions: new TwoWayMap({ // all the currently supported options are listed here, variantOptions.get returns the right side, variantOptions.revGet returns the left side of the dict Standard: 'chess', Crazyhouse: 'crazyhouse', @@ -201,7 +208,7 @@ export const store = new Vuex.Store({ muteButton: false, fenply: 1, internationalVariants: [ - '+ Add Custom', 'chess', 'crazyhouse', 'horde', 'kingofthehill', '3check', 'racingkings', 'antichess', 'atomic' + '+ Add Custom', 'chess', 'crazyhouse', 'horde', 'kingofthehill', '3check', 'racingkings', 'antichess', 'atomic', 'fischerandom' ], seaVariants: [ '+ Add Custom', 'makruk' @@ -273,6 +280,9 @@ export const store = new Vuex.Store({ PvE (state, payload) { state.PvE = payload }, + PvEPlayerIsWhite (state, payload) { + state.PvEPlayerIsWhite = payload + }, PvEParam (state, payload) { state.PvEParam = payload }, @@ -282,6 +292,19 @@ export const store = new Vuex.Store({ PvEInput (state, payload) { state.PvEInput = payload }, + // EvE mutations + EvE (state, payload) { + state.EvE = payload + }, + EvEConfig (state, payload) { + state.EvEConfig = payload + }, + engineWhiteInstance (state, payload) { + state.engineWhiteInstance = payload + }, + engineBlackInstance (state, payload) { + state.engineBlackInstance = payload + }, quicktourIndexIncr (state) { state.QuickTourIndex++ }, @@ -756,15 +779,33 @@ export const store = new Vuex.Store({ context.commit('active', true) }, goEnginePvE (context) { + // Send PvE engine command, start the clock, mark engine as active engine.send(context.getters.PvEParam) context.commit('setEngineClock') + context.commit('active', true) }, PvEMakeMove (context, payload) { + // Triggered when the engine emits 'bestmove'. Apply the move only if: + // 1. PvE mode is active 2. engine is to move now const state = context.state - if (state.active && state.PvE && !state.turn) { - context.dispatch('push', { move: payload, prev: context.getters.currentMove[0] }) + const playerIsWhite = context.state.PvEPlayerIsWhite + const engineIsWhite = !playerIsWhite + const turnIsWhite = state.turn + const engineToMoveNow = (turnIsWhite && engineIsWhite) || (!turnIsWhite && !engineIsWhite) + + if (state.active && state.PvE && engineToMoveNow) { + // Dispatch push and handle failure (invalid uci for current position) + context.dispatch('push', { move: payload, prev: context.getters.currentMove[0] }).then(() => { + }).catch((err) => { + // If engine returned a move invalid for the current position, log and restart engine on the + // current position so it recalculates for the correct state. + console.error('[PvEMakeMove] Engine provided invalid move for current position:', payload, err) + context.dispatch('position') + context.dispatch('goEnginePvE') + }) } }, + setActiveTrue (context) { context.commit('active', true) }, @@ -774,9 +815,147 @@ export const store = new Vuex.Store({ enginesActive (context, payload) { context.commit('enginesActive', payload) }, - PvEtrue (context) { + PvEtrue (context, payload = {}) { + // Enable PvE mode and remember which side the human player controls. + // payload.playerIsWhite = true means the human is White (legacy behavior). + const playerIsWhite = payload && typeof payload.playerIsWhite !== 'undefined' ? payload.playerIsWhite : true context.commit('PvE', true) + context.commit('PvEPlayerIsWhite', playerIsWhite) + context.commit('active', true) + + const engineIsWhite = !playerIsWhite + const turnIsWhite = context.getters.turn + const engineToMoveNow = (turnIsWhite && engineIsWhite) || (!turnIsWhite && !engineIsWhite) + if (engineToMoveNow) { + engine.send('stop') + context.dispatch('position') + context.dispatch('goEnginePvE') + } }, + // Start an Engine vs Engine match. Payload must include engine names and limiter configs: + // { whiteEngine, blackEngine, whiteLimiter: { enabled, type, value }, blackLimiter: {...} } + async EvEtrue (context, payload = {}) { + try { + const whiteName = payload.whiteEngine + const blackName = payload.blackEngine + if (!whiteName || !blackName) { + throw new Error('Both whiteEngine and blackEngine must be provided') + } + + const whiteInfo = context.state.allEngines[whiteName] + const blackInfo = context.state.allEngines[blackName] + if (!whiteInfo || !blackInfo) { + throw new Error('Could not find engine binaries for provided names') + } + + // create engine instances + const white = new Engine() + const black = new Engine() + + // run both engines + await Promise.all([ + white.run(whiteInfo.binary, whiteInfo.cwd), + black.run(blackInfo.binary, blackInfo.cwd) + ]) + + context.commit('engineWhiteInstance', white) + context.commit('engineBlackInstance', black) + context.commit('EvEConfig', payload) + context.commit('EvE', true) + context.commit('enginesActive', [true, true]) + context.commit('active', true) + + // helper to produce a `go` command from limiter + function limiterToGo (limiter) { + if (!limiter || !limiter.enabled) return 'go movetime 1000' + switch (limiter.type) { + case 'time': return `go movetime ${parseInt(limiter.value, 10)}` + case 'nodes': return `go nodes ${parseInt(limiter.value, 10) * 1000000}` + case 'depth': return `go depth ${parseInt(limiter.value, 10)}` + default: return `go movetime ${parseInt(limiter.value, 10) || 1000}` + } + } + + // send position and go to a specific engine instance + const sendPositionAndGo = (inst, lim) => { + try { + inst.send(`position fen ${context.getters.fen}`) + inst.send(limiterToGo(lim)) + } catch (err) { + console.error('[EvE] Failed to send position/go:', err) + } + } + + // bestmove handlers + const whiteHandler = async ucimove => { + // only apply if it's White to move + const turnIsWhite = context.getters.turn + if (!context.state.EvE || !turnIsWhite) return + try { + await context.dispatch('push', { move: ucimove, prev: context.getters.currentMove[0] }) + // after white move, trigger black + const cfg = context.state.EvEConfig || {} + sendPositionAndGo(context.state.engineBlackInstance, cfg.blackLimiter) + } catch (err) { + console.error('[EvEMakeMove] White provided invalid move:', ucimove, err) + // try to restart the black engine calculation on current position + context.dispatch('position') + sendPositionAndGo(context.state.engineBlackInstance, context.state.EvEConfig && context.state.EvEConfig.blackLimiter) + } + } + + const blackHandler = async ucimove => { + const turnIsWhite = context.getters.turn + if (!context.state.EvE || turnIsWhite) return + try { + await context.dispatch('push', { move: ucimove, prev: context.getters.currentMove[0] }) + // after black move, trigger white + const cfg = context.state.EvEConfig || {} + sendPositionAndGo(context.state.engineWhiteInstance, cfg.whiteLimiter) + } catch (err) { + console.error('[EvEMakeMove] Black provided invalid move:', ucimove, err) + context.dispatch('position') + sendPositionAndGo(context.state.engineWhiteInstance, context.state.EvEConfig && context.state.EvEConfig.whiteLimiter) + } + } + + // attach listeners + white.on('bestmove', whiteHandler) + black.on('bestmove', blackHandler) + + // kick off the side to move now + const turnIsWhiteNow = context.getters.turn + if (turnIsWhiteNow) { + sendPositionAndGo(white, payload.whiteLimiter) + } else { + sendPositionAndGo(black, payload.blackLimiter) + } + } catch (err) { + console.error('[EvEtrue] Could not start EvE match:', err) + } + }, + + async EvEfalse (context) { + // stop EvE match and quit engines + context.commit('EvE', false) + context.commit('enginesActive', [false, false]) + try { + if (context.state.engineWhiteInstance) { + try { context.state.engineWhiteInstance.send('quit') } catch (e) {} + context.state.engineWhiteInstance.removeAllListeners && context.state.engineWhiteInstance.removeAllListeners() + context.commit('engineWhiteInstance', null) + } + if (context.state.engineBlackInstance) { + try { context.state.engineBlackInstance.send('quit') } catch (e) {} + context.state.engineBlackInstance.removeAllListeners && context.state.engineBlackInstance.removeAllListeners() + context.commit('engineBlackInstance', null) + } + } catch (err) { + console.error('[EvEfalse] Error stopping EvE engines:', err) + } + context.commit('active', false) + context.dispatch('resetEngineData') + }, stopEnginePvE (context) { engine.send('stop') }, @@ -801,11 +980,18 @@ export const store = new Vuex.Store({ context.dispatch('stopEngine') context.dispatch('position') context.dispatch('goEngine') - } else if (context.getters.active && context.getters.PvE && !context.getters.turn) { - context.dispatch('position') - context.dispatch('goEnginePvE') + } else if (context.getters.active && context.getters.PvE) { + const playerIsWhite = context.getters.PvEPlayerIsWhite + const engineIsWhite = !playerIsWhite + const turnIsWhite = context.getters.turn + const engineToMoveNow = (turnIsWhite && engineIsWhite) || (!turnIsWhite && !engineIsWhite) + if (engineToMoveNow) { + context.dispatch('position') + context.dispatch('goEnginePvE') + } } }, + position (context) { engine.send(`position fen ${context.getters.fen}`) const eve = new CustomEvent('position', { detail: { fen: context.getters.fen } }) @@ -1321,6 +1507,9 @@ export const store = new Vuex.Store({ PvE (state) { return state.PvE }, + PvEPlayerIsWhite (state) { + return state.PvEPlayerIsWhite + }, PvEParam (state) { return state.PvEParam },