Skip to content

Commit 716e8d4

Browse files
authored
Merge pull request #77 from Belzedar94/codex/complete-obscuro_fow_implementation.md
Add incremental belief updates and Laotzu variant
2 parents 2e2cfe9 + c7e94aa commit 716e8d4

File tree

16 files changed

+559
-99
lines changed

16 files changed

+559
-99
lines changed

FOG_OF_WAR_GUIDE.md

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,24 @@ Key differences from Dark Crazyhouse:
224224
- More strategic piece placement required
225225
- Surprise drops only possible in areas your pieces can already see
226226

227+
### Laotzu (Double FRC Dark Crazyhouse 2)
228+
229+
Laotzu randomizes each side's back rank independently using chess960 rules, then applies the Dark Crazyhouse 2 FoW drop rules:
230+
231+
```
232+
uci
233+
setoption name UCI_Variant value laotzu
234+
setoption name UCI_FoW value true
235+
setoption name UCI_IISearch value true
236+
position startpos
237+
go movetime 5000
238+
```
239+
240+
Notes:
241+
- Each `position startpos` call generates a fresh double-sided chess960 layout with proper fog and drop handling.
242+
- Castling rights follow chess960 encoding based on the randomized rooks.
243+
- Drops remain restricted to visible squares, as in Dark Crazyhouse 2.
244+
227245
## Analyzing FoW Positions
228246

229247
### Using Standard FEN
@@ -267,6 +285,11 @@ The engine will:
267285
2. Enumerate positions consistent with what the fog FEN shows (permuting hidden opponent pieces across unseen squares)
268286
3. Use that belief state to guide the Obscuro search before selecting a move
269287

288+
For deeper diagnostics while analyzing:
289+
- Use `go depth <n>` to control search depth instead of time.
290+
- Reissue `position fog_fen ...` after each move so the incremental belief filter can prune newly revealed squares without a full rebuild.
291+
- For Laotzu or other chess960-style FoW variants, send `position startpos` again to refresh the randomized layout before a new line of analysis.
292+
270293
## Viewing the Fog-of-War Board State
271294

272295
The engine internally tracks what each player can see. When making moves via UCI, the engine automatically:
@@ -344,37 +367,18 @@ The current implementation includes:
344367
- ✅ Multi-threaded search (1 CFR solver + 2 expanders)
345368
- ✅ fog_fen parsing wired into belief state enumeration
346369
- ✅ Belief state management (enumerates hidden opponent permutations up to 1024 states per observation)
370+
- ✅ Incremental belief filtering (keeps belief states in sync with new observations and rebuilds when needed)
371+
- ✅ Purification, gadgets, instrumentation, and memory controls
347372
- ✅ NNUE evaluation for all FoW variants
348-
- ⚠️ Action purification (placeholder implementation)
349-
- ⚠️ KLUSS order-2 neighborhood is still simplified
350-
- ⚠️ Resolve/Maxmargin gadget details are incomplete
351-
- 🔲 Instrumentation (Appendix B.4 metrics)
352-
353-
354373

355374
### Current Limitations
356375

357-
358-
359-
**What Works**:
360-
361-
- The engine runs FoW search and returns moves
362-
- UCI options are properly parsed and applied
363-
- Multi-threaded CFR solver and expanders run correctly
364-
- The fog_fen command parses and stores partial observations
365-
366-
**What Doesn't Work Yet**:
367-
368-
1. **Belief diversity limits**: Enumeration permutes hidden opponent pieces from the current position and caps at 1024 states; it does not yet model captures beyond the observed piece set or piece-in-hand drops for crazyhouse variants.
369-
370-
2. **KLUSS neighborhood**: The KLUSS computation is still a placeholder and does not freeze/unfreeze infosets per the paper's order-2 definition.
371-
372-
3. **Purification and gadgets**: Action purification and Resolve/Maxmargin gadget details remain simplified, so play quality may vary in tricky information sets.
376+
- Belief enumeration still ignores crazyhouse piece-in-hand speculation beyond observed inventory.
377+
- Castling/visibility corner cases (e.g., exotic variants) need additional coverage.
378+
- Performance tuning is ongoing; long searches may still be slow on very dense belief sets.
373379

374380
### Practical Usage
375381

376-
**Current best use case**: Using `position fog_fen` to explore imperfect-information situations where hidden opponent pieces could be on multiple unseen squares. The engine will enumerate those possibilities and search them, but higher-level gadgets and purification are still simplified.
377-
378-
**Not yet suitable for**: Positions that rely on advanced KLUSS freezing/unfreezing logic or deep purification requirements (e.g., adversarial bluffing scenarios and crazyhouse drop speculation).
379-
380-
For development status and technical details, see `OBSCURO_FOW_IMPLEMENTATION.md`.
382+
- Use `position fog_fen` for partial observations; the engine will prune beliefs incrementally as play continues.
383+
- Run `tests/fow_incremental.sh` after building `src/stockfish` to smoke-test the FoW pipeline.
384+
- For deeper implementation details, see `OBSCURO_FOW_IMPLEMENTATION.md`.

OBSCURO_FOW_IMPLEMENTATION.md

Lines changed: 20 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -189,71 +189,37 @@ Resolve gadgets now build the paper's prior α(J) via `compute_resolve_prior()`
189189

190190

191191

192-
#### 5. Leaf Evaluation Integration (MEDIUM PRIORITY)
192+
#### 5. Leaf Evaluation Integration (DONE)
193193

194-
Depth-1 child evaluation is wired into Stockfish's evaluator and normalized to [-1, +1]; remaining work is focused on FoW-specific averaging over the belief set and caching repeated states.
195-
196-
197-
198-
**Implementation Tasks**:
199-
200-
1. Complete `Evaluator::evaluate()` to call Stockfish search
201-
202-
2. Implement `evaluate_belief_state()` that averages over positions
203-
204-
3. Add caching to avoid re-evaluating same positions
205-
206-
4. Handle terminal position detection
194+
Depth-1 child evaluation now uses a shallow Stockfish search (depth = 1) with terminal detection and normalization to [-1, +1]. `Evaluator::evaluate()` caches results by Zobrist key to avoid re-running searches on duplicate states, and `evaluate_belief_state()` walks the belief set to return the averaged score. Terminal states (mate/draw/stalemate and variant-specific endings) are intercepted before search to keep heuristics stable.
207195

208196

209197

210198
### Lower Priority Enhancements
211199

212200

213201

214-
#### 6. Instrumentation (Appendix B.4) (LOW PRIORITY)
202+
#### 6. Instrumentation (Appendix B.4) (DONE)
215203

216-
217-
218-
**What's Needed**:
204+
- Planner statistics now record exploitability approximation using the mean positive regret across infosets, action entropy at the root, peak node counts, and a timeline of node sizes.
205+
- Time is broken down across construction, search, and selection so the UCI `info string` can report where FoW time was spent.
219206

220-
- CFR convergence metrics (exploitability approximation)
221207

222-
- Tree size statistics over time
208+
#### 7. Memory Management (DONE)
223209

224-
- Action entropy tracking
225-
226-
- Time breakdown per component
210+
- Subgame nodes are pulled from and returned to a recycling pool, keeping allocations bounded.
211+
- Out-of-KLUSS branches are pruned eagerly and a soft node limit triggers pruning of deep or frozen leaves.
212+
- Belief states are compressed to a configurable cap before sampling, shrinking both memory footprint and sampling cost.
227213

228214

229215

230-
#### 7. Memory Management (LOW PRIORITY)
231-
232-
233-
234-
**What's Needed**:
235-
236-
- Tree pruning for old/unused nodes
216+
#### 8. Incremental Belief Updates (DONE)
237217

238-
- Node recycling pool
239-
240-
- Belief state compression
241-
242-
- Memory limits and cleanup
243-
244-
245-
246-
#### 8. Incremental Belief Updates (LOW PRIORITY)
247-
248-
249-
250-
**What's Needed**:
251-
252-
- When new observation arrives, filter existing belief set
253-
254-
- Much faster than re-enumeration from scratch
255-
256-
- Requires careful tracking of observation sequence
218+
- `BeliefState::update_incrementally()` now filters the current belief set against the
219+
latest observation, reuses cached positions when visibility shrinks, and triggers
220+
a full rebuild only when the observation expands or filtering collapses the set.
221+
- The planner wires incremental updates through `enableIncrementalBelief`, falling
222+
back to full reconstruction when disabled or when the belief set underflows.
257223

258224

259225

@@ -285,6 +251,11 @@ Depth-1 child evaluation is wired into Stockfish's evaluator and normalized to [
285251

286252
4. **Memory leak detection**: Run extended searches
287253

254+
Implemented smoke coverage:
255+
256+
- `tests/fow_incremental.sh` exercises fog_fen parsing, incremental belief filtering,
257+
and the FoW planner pipeline end-to-end.
258+
288259

289260

290261
#### Comparison Tests

src/imperfect/Belief.cpp

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,10 +371,27 @@ void BeliefState::rebuild_from_observations(const ObservationHistory& obsHist,
371371
// TODO: Implement full consistency checking with FEN parsing
372372
}
373373

374-
void BeliefState::update_incrementally(const Observation& newObs) {
375-
if (!variant)
374+
void BeliefState::update_incrementally(const ObservationHistory& obsHist, const Position& truePos) {
375+
if (!variant) {
376+
variant = truePos.variant();
377+
isChess960 = truePos.is_chess960();
378+
owningThread = truePos.this_thread();
379+
}
380+
381+
if (obsHist.empty()) {
382+
stateFens.clear();
383+
stateKeys.clear();
376384
return;
385+
}
377386

387+
const Observation& newObs = obsHist.last();
388+
389+
if (stateFens.empty()) {
390+
rebuild_from_observations(obsHist, truePos);
391+
return;
392+
}
393+
394+
size_t beforeSize = stateFens.size();
378395
auto it = stateFens.begin();
379396
while (it != stateFens.end()) {
380397
StateInfo st;
@@ -391,6 +408,16 @@ void BeliefState::update_incrementally(const Observation& newObs) {
391408
++it;
392409
}
393410
}
411+
412+
bool observationExpanded = obsHist.size() >= 2
413+
&& ( obsHist.last().visible != obsHist.observations()[obsHist.size() - 2].visible
414+
|| obsHist.last().seenOpponentPieces != obsHist.observations()[obsHist.size() - 2].seenOpponentPieces
415+
|| obsHist.last().epSquares != obsHist.observations()[obsHist.size() - 2].epSquares
416+
|| obsHist.last().castlingRights != obsHist.observations()[obsHist.size() - 2].castlingRights);
417+
418+
if (stateFens.empty() || observationExpanded || stateFens.size() < beforeSize / 4) {
419+
rebuild_from_observations(obsHist, truePos);
420+
}
394421
}
395422

396423
std::vector<std::string> BeliefState::sample_states(size_t n, uint64_t seed) const {
@@ -414,5 +441,27 @@ std::vector<std::string> BeliefState::sample_states(size_t n, uint64_t seed) con
414441
return sampled;
415442
}
416443

444+
void BeliefState::compress(size_t maxStates) {
445+
if (!maxStates || stateFens.size() <= maxStates)
446+
return;
447+
448+
stateFens.resize(maxStates);
449+
stateKeys.clear();
450+
451+
if (!variant) {
452+
stateFens.shrink_to_fit();
453+
return;
454+
}
455+
456+
for (const auto& fen : stateFens) {
457+
Position pos;
458+
StateInfo st;
459+
if (set_position_from_fen(pos, st, fen))
460+
stateKeys.insert(pos.key());
461+
}
462+
463+
stateFens.shrink_to_fit();
464+
}
465+
417466
} // namespace FogOfWar
418467
} // namespace Stockfish

src/imperfect/Belief.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,16 @@ class BeliefState {
7171
const Position& truePos);
7272

7373
/// update_incrementally() attempts incremental update; falls back to rebuild if needed
74-
void update_incrementally(const Observation& newObs);
74+
void update_incrementally(const ObservationHistory& obsHist, const Position& truePos);
7575

7676
/// Sample a subset of states for building the subgame
7777
/// Returns FEN strings of sampled positions
7878
std::vector<std::string> sample_states(size_t n, uint64_t seed = 0) const;
7979

80+
/// Compress the belief set to a maximum number of states while preserving
81+
/// deterministic ordering and removing stale capacity
82+
void compress(size_t maxStates);
83+
8084
/// Accessors
8185
size_t size() const { return stateFens.size(); }
8286
const std::vector<std::string>& all_states() const { return stateFens; }

0 commit comments

Comments
 (0)