-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
-
-
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
},