Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddb309f6db | ||
|
|
2db4048ed2 | ||
|
|
2f53c2f0ed | ||
|
|
7562ded92a | ||
|
|
392b9ec212 | ||
|
|
769c7819e0 | ||
|
|
c048529654 | ||
|
|
e8eb90a085 | ||
|
|
fc167ae2b0 | ||
|
|
088f05e0d2 | ||
|
|
04dcae7993 |
@@ -0,0 +1,97 @@
|
||||
# .gitea/workflows/deploy.yml
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: deploy-${{ gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
# Gitea's runner images are lean, so the toolchain is installed below rather
|
||||
# than assumed. The glibc that libchess_engine.so links against comes from
|
||||
# this image, not from the runner host — keep it matched to the prod server.
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install toolchain
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
cmake build-essential rsync openssh-client
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore JoshHeaps.Net/JoshHeaps.Net.csproj
|
||||
|
||||
- name: Build native chess engine (libchess_engine.so)
|
||||
run: |
|
||||
cmake -S native/chess_engine -B native/chess_engine/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build native/chess_engine/build
|
||||
cp native/chess_engine/build/libchess_engine.so JoshHeaps.Net/Resources/
|
||||
|
||||
# Publishing the project rather than the solution keeps chess_engine.vcxproj
|
||||
# (Windows-only MSBuild C++ targets) and the UiTests assemblies out of it.
|
||||
- name: Publish
|
||||
run: dotnet publish JoshHeaps.Net/JoshHeaps.Net.csproj -c Release -o ./publish
|
||||
|
||||
- name: Verify payload
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f publish/Resources/libchess_engine.so \
|
||||
|| { echo "libchess_engine.so missing from publish output"; exit 1; }
|
||||
test -x publish/Resources/stockfish-ubuntu-x86-64-sse41-popcnt \
|
||||
|| { echo "stockfish is not executable"; exit 1; }
|
||||
|
||||
- name: Prepare SSH
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SSH_KEY }}
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_PORT: ${{ secrets.SSH_PORT }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PORT="${SSH_PORT:-22}"
|
||||
install -m 700 -d ~/.ssh
|
||||
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -p "$PORT" "$SSH_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
# --exclude chess-data: never let --delete remove the learned-engine training
|
||||
# data, which lives in the deploy dir unless ChessEngine__WeightsPath is set.
|
||||
- name: Rsync to server
|
||||
env:
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_PORT: ${{ secrets.SSH_PORT }}
|
||||
SSH_USER: ${{ secrets.SSH_USER }}
|
||||
TARGET_DIR: ${{ secrets.TARGET_DIR }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PORT="${SSH_PORT:-22}"
|
||||
rsync -az --delete --exclude 'chess-data' \
|
||||
-e "ssh -p $PORT -i ~/.ssh/deploy_key -o StrictHostKeyChecking=yes" \
|
||||
publish/ "$SSH_USER@$SSH_HOST:$TARGET_DIR/"
|
||||
|
||||
- name: Reload and restart service
|
||||
env:
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_PORT: ${{ secrets.SSH_PORT }}
|
||||
SSH_USER: ${{ secrets.SSH_USER }}
|
||||
SERVICE_NAME: ${{ secrets.SERVICE_NAME }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PORT="${SSH_PORT:-22}"
|
||||
ssh -p "$PORT" -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
|
||||
"sudo systemctl daemon-reload \
|
||||
&& sudo systemctl restart '$SERVICE_NAME' \
|
||||
&& systemctl --no-pager status '$SERVICE_NAME' --lines=0"
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
@@ -10,6 +10,7 @@ public class BlogService : IBlogService
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<BlogService> _logger;
|
||||
private readonly TimeSpan _cacheTtl = TimeSpan.FromMinutes(5);
|
||||
private readonly TimeSpan _staleGrace = TimeSpan.FromHours(1);
|
||||
|
||||
private readonly ConcurrentDictionary<string, CacheEntry> _cache = new();
|
||||
|
||||
@@ -65,7 +66,9 @@ public class BlogService : IBlogService
|
||||
|
||||
private async Task<T?> GetCachedAsync<T>(string key, Func<Task<T?>> factory) where T : class
|
||||
{
|
||||
if (_cache.TryGetValue(key, out var entry) && entry.ExpiresAt > DateTime.UtcNow)
|
||||
_cache.TryGetValue(key, out var entry);
|
||||
|
||||
if (entry is not null && entry.ExpiresAt > DateTime.UtcNow)
|
||||
return (T?)entry.Value;
|
||||
|
||||
var result = await factory();
|
||||
@@ -73,15 +76,26 @@ public class BlogService : IBlogService
|
||||
if (result is not null)
|
||||
{
|
||||
_cache[key] = new CacheEntry(result, DateTime.UtcNow.Add(_cacheTtl));
|
||||
return result;
|
||||
}
|
||||
else if (entry is not null)
|
||||
|
||||
return entry is null ? null : ServeStale<T>(key, entry);
|
||||
}
|
||||
|
||||
// Stale entries are dropped once the grace window closes so that a dead API fails the same
|
||||
// way for every key. Serving them indefinitely let a cached post list render next to 404s on
|
||||
// the posts themselves, which reads as a site bug rather than an outage.
|
||||
private T? ServeStale<T>(string key, CacheEntry entry) where T : class
|
||||
{
|
||||
if (entry.ExpiresAt.Add(_staleGrace) > DateTime.UtcNow)
|
||||
{
|
||||
// API unreachable — serve stale cache
|
||||
_logger.LogWarning("Serving stale cache for key {Key}", key);
|
||||
_logger.LogWarning("Blog API unreachable, serving stale cache for key {Key}", key);
|
||||
return (T?)entry.Value;
|
||||
}
|
||||
|
||||
return result;
|
||||
_cache.TryRemove(key, out _);
|
||||
_logger.LogError("Blog API unreachable for over {StaleGrace}, dropping stale cache for key {Key}", _staleGrace, key);
|
||||
return null;
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
|
||||
@@ -100,11 +100,26 @@ public sealed class Stockfish : IChessEngine
|
||||
if (line == token) break;
|
||||
}
|
||||
|
||||
// If Stockfish doesn't exit on "quit" (crashed, zombie, ignores stdin close), don't hang
|
||||
// here forever — that would block GameStore's removal task and leave the finished game
|
||||
// stuck in the store, cluttering the watch page indefinitely.
|
||||
private static readonly TimeSpan _exitTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
Send("quit");
|
||||
_stdin.Close();
|
||||
await _p.WaitForExitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await _p.WaitForExitAsync().WaitAsync(_exitTimeout);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
if (!_p.HasExited)
|
||||
_p.Kill(entireProcessTree: true);
|
||||
}
|
||||
|
||||
_p.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,9 +103,13 @@ CHESS_API void CHESS_CALL trainer_destroy(TrainerHandle trainer);
|
||||
/* Record one played position (post-move FEN) into the accumulator. */
|
||||
CHESS_API void CHESS_CALL trainer_record(TrainerHandle trainer, const char* fen);
|
||||
|
||||
/* Apply a finished game's outcome to the global weights and save: rewards the winner's
|
||||
* occupied squares / features and punishes the loser's, scaled by `weight` (e.g. 0.5 for a
|
||||
* material-imbalance draw). winner: 0 = white, 1 = black. */
|
||||
/* Apply a finished game's outcome to the global weights and save. Each touched cell (a piece
|
||||
* on a square, or a feature) tracks a presence-weighted running average of the game's
|
||||
* "advantage" — the game's outcome (scaled by `weight`, e.g. 0.5 for a material-imbalance
|
||||
* draw) minus how much of that outcome the winner's average material edge already explains —
|
||||
* so mg/eg/featW end up as a fractional win-rate (percent of games won with that piece there,
|
||||
* minus percent lost), not an unbounded plus/minus tally, and a piece doesn't get credit
|
||||
* merely for existing on the winning side of a material blowout. winner: 0 = white, 1 = black. */
|
||||
CHESS_API void CHESS_CALL trainer_apply(TrainerHandle trainer, int winner, double weight);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <string>
|
||||
@@ -115,11 +116,25 @@ struct ChessEngine {
|
||||
/* The process-global learned weights: the single source of truth, loaded from disk once and
|
||||
* updated in place by training. Engine handles snapshot it at creation; the visualization
|
||||
* snapshots it on demand. Guarded by g_weightsMutex for updates/saves (eval reads its own
|
||||
* per-handle copy, so it never touches this concurrently). */
|
||||
* per-handle copy, so it never touches this concurrently).
|
||||
*
|
||||
* mg/eg/featW are DERIVED, not accumulated: each is always exactly OUT_SCALE * (score/visits)
|
||||
* for its cell (see trainer_apply), recomputed whenever that cell's stats change. visits is
|
||||
* the presence-weighted count of games that touched the cell (how much to trust the average);
|
||||
* score is the presence-weighted sum of each game's material-adjusted advantage (see
|
||||
* trainer_apply) — so score/visits is literally "fraction of (presence-weighted) games won
|
||||
* with this piece here, minus fraction lost," bounded to [-1,1] by construction. */
|
||||
struct LearnedWeights {
|
||||
int mg[chess::PIECE_TYPE_NB][64] = {};
|
||||
int eg[chess::PIECE_TYPE_NB][64] = {};
|
||||
int featW[FEATURE_NB] = {};
|
||||
|
||||
double mgVisits[chess::PIECE_TYPE_NB][64] = {};
|
||||
double mgScore [chess::PIECE_TYPE_NB][64] = {};
|
||||
double egVisits[chess::PIECE_TYPE_NB][64] = {};
|
||||
double egScore [chess::PIECE_TYPE_NB][64] = {};
|
||||
double featVisits[FEATURE_NB] = {};
|
||||
double featScore [FEATURE_NB] = {};
|
||||
};
|
||||
|
||||
static LearnedWeights g_weights;
|
||||
@@ -127,13 +142,15 @@ static std::mutex g_weightsMutex;
|
||||
static std::string g_weightsPath;
|
||||
|
||||
/* Per-game training accumulator (one per learned CPU-vs-CPU game). Records, per ply, where
|
||||
* each side's pieces sat (split into midgame/endgame by phase) and each side's feature
|
||||
* activations; trainer_apply turns the totals into weight nudges. Squares are white-relative
|
||||
* each side's pieces sat (split into midgame/endgame by phase), each side's feature
|
||||
* activations, and the white-minus-black material balance; trainer_apply turns the totals
|
||||
* into a presence-weighted update to the running per-cell averages. Squares are white-relative
|
||||
* (black indexes sq ^ 56), so a side's tally lines up with the shared white-relative table. */
|
||||
struct Trainer {
|
||||
double mgOcc[chess::COLOR_NB][chess::PIECE_TYPE_NB][64] = {};
|
||||
double egOcc[chess::COLOR_NB][chess::PIECE_TYPE_NB][64] = {};
|
||||
double featAcc[chess::COLOR_NB][FEATURE_NB] = {};
|
||||
double matDiffAcc = 0.0; /* sum over plies of (white material - black material), cp */
|
||||
int plies = 0;
|
||||
};
|
||||
|
||||
@@ -172,9 +189,12 @@ static int parse_variant(const char* options) {
|
||||
}
|
||||
|
||||
/* On-disk format: 6*64 mg ints (PAWN..KING, squares 0..63), then 6*64 eg ints, then
|
||||
* FEATURE_NB feature ints, whitespace-separated. A missing file or short read leaves the
|
||||
* rest neutral (0), so an absent weights file just means "train from a blank slate".
|
||||
* Caller holds g_weightsMutex. */
|
||||
* FEATURE_NB feature ints, then the backing visits/score doubles for each (mgVisits, mgScore,
|
||||
* egVisits, egScore, featVisits, featScore) in the same shapes, all whitespace-separated. A
|
||||
* missing file or short read leaves the rest neutral (0), so an absent weights file just means
|
||||
* "train from a blank slate" — and a file written by the pre-fractional-learning format (which
|
||||
* ends after the feature ints) loads its mg/eg/featW fine and simply starts every cell's
|
||||
* visits/score history fresh from the next game that touches it. Caller holds g_weightsMutex. */
|
||||
static void load_global_weights(const char* path) {
|
||||
g_weights = LearnedWeights{}; /* reset to neutral before loading */
|
||||
|
||||
@@ -190,9 +210,29 @@ static void load_global_weights(const char* path) {
|
||||
if (!(f >> g_weights.eg[pt][sq])) return;
|
||||
for (int i = 0; i < FEATURE_NB; ++i)
|
||||
if (!(f >> g_weights.featW[i])) return;
|
||||
|
||||
for (int pt = chess::PAWN; pt <= chess::KING; ++pt)
|
||||
for (int sq = 0; sq < 64; ++sq)
|
||||
if (!(f >> g_weights.mgVisits[pt][sq])) return;
|
||||
for (int pt = chess::PAWN; pt <= chess::KING; ++pt)
|
||||
for (int sq = 0; sq < 64; ++sq)
|
||||
if (!(f >> g_weights.mgScore[pt][sq])) return;
|
||||
for (int pt = chess::PAWN; pt <= chess::KING; ++pt)
|
||||
for (int sq = 0; sq < 64; ++sq)
|
||||
if (!(f >> g_weights.egVisits[pt][sq])) return;
|
||||
for (int pt = chess::PAWN; pt <= chess::KING; ++pt)
|
||||
for (int sq = 0; sq < 64; ++sq)
|
||||
if (!(f >> g_weights.egScore[pt][sq])) return;
|
||||
for (int i = 0; i < FEATURE_NB; ++i)
|
||||
if (!(f >> g_weights.featVisits[i])) return;
|
||||
for (int i = 0; i < FEATURE_NB; ++i)
|
||||
if (!(f >> g_weights.featScore[i])) return;
|
||||
}
|
||||
|
||||
/* Persist g_weights to g_weightsPath in the format load_global_weights reads. Caller holds the lock. */
|
||||
/* Persist g_weights to g_weightsPath in the format load_global_weights reads. Full round-trip
|
||||
* precision on the doubles (visits/score are running sums that keep accumulating across many
|
||||
* games, so truncating them would drift the derived mg/eg/featW over time). Caller holds the
|
||||
* lock. */
|
||||
static void save_global_weights() {
|
||||
if (g_weightsPath.empty()) return;
|
||||
std::ofstream f(g_weightsPath);
|
||||
@@ -203,6 +243,18 @@ static void save_global_weights() {
|
||||
for (int pt = chess::PAWN; pt <= chess::KING; ++pt)
|
||||
for (int sq = 0; sq < 64; ++sq) f << g_weights.eg[pt][sq] << (sq == 63 ? '\n' : ' ');
|
||||
for (int i = 0; i < FEATURE_NB; ++i) f << g_weights.featW[i] << (i == FEATURE_NB - 1 ? '\n' : ' ');
|
||||
|
||||
f << std::setprecision(17);
|
||||
for (int pt = chess::PAWN; pt <= chess::KING; ++pt)
|
||||
for (int sq = 0; sq < 64; ++sq) f << g_weights.mgVisits[pt][sq] << (sq == 63 ? '\n' : ' ');
|
||||
for (int pt = chess::PAWN; pt <= chess::KING; ++pt)
|
||||
for (int sq = 0; sq < 64; ++sq) f << g_weights.mgScore[pt][sq] << (sq == 63 ? '\n' : ' ');
|
||||
for (int pt = chess::PAWN; pt <= chess::KING; ++pt)
|
||||
for (int sq = 0; sq < 64; ++sq) f << g_weights.egVisits[pt][sq] << (sq == 63 ? '\n' : ' ');
|
||||
for (int pt = chess::PAWN; pt <= chess::KING; ++pt)
|
||||
for (int sq = 0; sq < 64; ++sq) f << g_weights.egScore[pt][sq] << (sq == 63 ? '\n' : ' ');
|
||||
for (int i = 0; i < FEATURE_NB; ++i) f << g_weights.featVisits[i] << (i == FEATURE_NB - 1 ? '\n' : ' ');
|
||||
for (int i = 0; i < FEATURE_NB; ++i) f << g_weights.featScore[i] << (i == FEATURE_NB - 1 ? '\n' : ' ');
|
||||
}
|
||||
|
||||
/* Maps the 1..20 difficulty to a search depth. Kept modest: the search has no
|
||||
@@ -382,13 +434,18 @@ static int evaluate(const chess::Position& pos) {
|
||||
* compute_features() is the single source of feature activations, used by BOTH the eval here
|
||||
* and the trainer, so the two can never disagree. Constants below are the only tunables. */
|
||||
|
||||
/* Per-game-outcome learning rates and clamps. Squares accumulate occupancy (plies on a
|
||||
* square, summed); features accumulate normalized per-ply activation (averaged, divided by a
|
||||
* nominal scale so high-magnitude mobility doesn't dwarf the small pawn-structure terms). */
|
||||
static constexpr double SQUARE_LR = 0.5;
|
||||
static constexpr int SQ_CLAMP = 250;
|
||||
static constexpr double FEAT_LR = 2.0;
|
||||
static constexpr int FEAT_CLAMP = 500;
|
||||
/* Per-game-outcome learning constants. Squares and features both learn a presence/activation-
|
||||
* weighted AVERAGE advantage in [-1,1] (see trainer_apply) — OUT_SCALE just converts that
|
||||
* average into a centipawn-ish int for the eval/table to add directly. FEAT_SCALE normalizes
|
||||
* each feature's raw per-ply activation to a comparable magnitude before it's used as a weight
|
||||
* (so high-count mobility doesn't dwarf the small pawn-structure terms), same as before.
|
||||
* MATERIAL_ELO_SCALE is the classic centipawn-to-win-probability curve
|
||||
* (expected = 1/(1+10^(-cp/400))) used to compute how much of a result material already
|
||||
* explains, so training credits placement/features only for the part of the result that
|
||||
* material didn't already predict. */
|
||||
static constexpr int SQ_OUT_SCALE = 250;
|
||||
static constexpr int FEAT_OUT_SCALE = 500;
|
||||
static constexpr double MATERIAL_ELO_SCALE = 400.0;
|
||||
static constexpr double FEAT_SCALE[FEATURE_NB] = { 4, 6, 8, 14, 2, 1, 1, 2 };
|
||||
|
||||
/* Game phase in [0,1] from remaining non-pawn material (PeSTO weights N=B=1, R=2, Q=4; max
|
||||
@@ -823,7 +880,9 @@ CHESS_API void CHESS_CALL trainer_record(TrainerHandle t, const char* fen) {
|
||||
chess::Position pos = chess::Position::from_fen(fen);
|
||||
double phase = game_phase(pos);
|
||||
|
||||
/* Per-square occupancy, split into midgame/endgame by phase, white-relative. */
|
||||
/* Per-square occupancy, split into midgame/endgame by phase, white-relative; and the
|
||||
* white-minus-black material balance at this ply, used at trainer_apply time to strip
|
||||
* "material already explains this" out of the training signal. */
|
||||
chess::Bitboard occ = pos.pieces();
|
||||
while (occ) {
|
||||
chess::Square s = chess::pop_lsb(occ);
|
||||
@@ -833,6 +892,7 @@ CHESS_API void CHESS_CALL trainer_record(TrainerHandle t, const char* fen) {
|
||||
int relSq = (c == chess::WHITE) ? int(s) : (int(s) ^ 56);
|
||||
t->mgOcc[c][pt][relSq] += (1.0 - phase);
|
||||
t->egOcc[c][pt][relSq] += phase;
|
||||
t->matDiffAcc += (c == chess::WHITE ? 1 : -1) * piece_value(pt);
|
||||
}
|
||||
|
||||
/* Per-side feature activations. */
|
||||
@@ -847,34 +907,66 @@ CHESS_API void CHESS_CALL trainer_record(TrainerHandle t, const char* fen) {
|
||||
t->plies++;
|
||||
}
|
||||
|
||||
/* Applies a finished game's outcome as a presence-weighted running average per cell, instead
|
||||
* of an unbounded incremental nudge:
|
||||
*
|
||||
* score[cell] / visits[cell] == "fraction of (presence-weighted) games won with this piece
|
||||
* on this square, minus fraction lost"
|
||||
*
|
||||
* `weight` is the caller's confidence in the result (1.0 for a checkmate, 0.5 for a
|
||||
* material-imbalance draw, as before). On top of that, the update is scaled by how much of the
|
||||
* result ISN'T already explained by material: `expected` is the win probability the winner's
|
||||
* average material edge over the game implies (the same centipawn->win-probability curve
|
||||
* engines use for Elo estimates); `1 - expected` is the "surprise" left over once material is
|
||||
* accounted for. A win where the winner was already up a rook the whole game is unsurprising
|
||||
* (expected ~= 1, ~no update) — the eval already knows material matters via piece_value(), so
|
||||
* square/feature tables shouldn't get credit for "a piece existed"; a win pulled off (or a draw
|
||||
* held) while materially behind is highly informative and gets a correspondingly large update.
|
||||
* This same magnitude applies to BOTH the winner's reward and the loser's punishment: by the
|
||||
* symmetry of win probabilities (P(loser wins) = 1 - P(winner wins)), the loser's own advantage
|
||||
* — actual(0) minus their expected win probability — has exactly the same magnitude, `1 -
|
||||
* expected`, just negative. */
|
||||
CHESS_API void CHESS_CALL trainer_apply(TrainerHandle t, int winner, double weight) {
|
||||
if (!t) return;
|
||||
if (!t || t->plies <= 0) return;
|
||||
|
||||
double avgMaterialDiff = t->matDiffAcc / t->plies; /* white - black, cp */
|
||||
double winnerMaterialDiff = (winner == chess::WHITE) ? avgMaterialDiff : -avgMaterialDiff;
|
||||
double expected = 1.0 / (1.0 + std::pow(10.0, -winnerMaterialDiff / MATERIAL_ELO_SCALE));
|
||||
double advantageMag = weight * (1.0 - expected);
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_weightsMutex);
|
||||
|
||||
/* pass 0 = winner (reward, +1); pass 1 = loser (punish, -1). */
|
||||
/* pass 0 = winner (positive advantage); pass 1 = loser (negative advantage). */
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
chess::Color side = chess::Color((pass == 0 ? winner : (winner ^ 1)) & 1);
|
||||
int sign = pass == 0 ? 1 : -1;
|
||||
double advantage = pass == 0 ? advantageMag : -advantageMag;
|
||||
|
||||
for (int pt = chess::PAWN; pt <= chess::KING; ++pt)
|
||||
for (int sq = 0; sq < 64; ++sq) {
|
||||
if (t->mgOcc[side][pt][sq] != 0.0) {
|
||||
int d = sign * int(std::lround(SQUARE_LR * t->mgOcc[side][pt][sq] * weight));
|
||||
g_weights.mg[pt][sq] = std::clamp(g_weights.mg[pt][sq] + d, -SQ_CLAMP, SQ_CLAMP);
|
||||
double mgFrac = t->mgOcc[side][pt][sq] / t->plies; /* fraction of the game spent here, [0,1] */
|
||||
if (mgFrac != 0.0) {
|
||||
g_weights.mgVisits[pt][sq] += mgFrac;
|
||||
g_weights.mgScore [pt][sq] += mgFrac * advantage;
|
||||
g_weights.mg[pt][sq] = int(std::lround(
|
||||
SQ_OUT_SCALE * g_weights.mgScore[pt][sq] / g_weights.mgVisits[pt][sq]));
|
||||
}
|
||||
if (t->egOcc[side][pt][sq] != 0.0) {
|
||||
int d = sign * int(std::lround(SQUARE_LR * t->egOcc[side][pt][sq] * weight));
|
||||
g_weights.eg[pt][sq] = std::clamp(g_weights.eg[pt][sq] + d, -SQ_CLAMP, SQ_CLAMP);
|
||||
double egFrac = t->egOcc[side][pt][sq] / t->plies;
|
||||
if (egFrac != 0.0) {
|
||||
g_weights.egVisits[pt][sq] += egFrac;
|
||||
g_weights.egScore [pt][sq] += egFrac * advantage;
|
||||
g_weights.eg[pt][sq] = int(std::lround(
|
||||
SQ_OUT_SCALE * g_weights.egScore[pt][sq] / g_weights.egVisits[pt][sq]));
|
||||
}
|
||||
}
|
||||
|
||||
if (t->plies > 0)
|
||||
for (int i = 0; i < FEATURE_NB; ++i) {
|
||||
double avg = t->featAcc[side][i] / t->plies; /* per-ply average, normalized */
|
||||
int d = sign * int(std::lround(FEAT_LR * (avg / FEAT_SCALE[i]) * weight));
|
||||
g_weights.featW[i] = std::clamp(g_weights.featW[i] + d, -FEAT_CLAMP, FEAT_CLAMP);
|
||||
}
|
||||
for (int i = 0; i < FEATURE_NB; ++i) {
|
||||
double activation = (t->featAcc[side][i] / t->plies) / FEAT_SCALE[i]; /* per-ply average, normalized */
|
||||
if (activation == 0.0) continue;
|
||||
g_weights.featVisits[i] += std::abs(activation);
|
||||
g_weights.featScore [i] += activation * advantage;
|
||||
g_weights.featW[i] = int(std::lround(
|
||||
FEAT_OUT_SCALE * g_weights.featScore[i] / g_weights.featVisits[i]));
|
||||
}
|
||||
}
|
||||
|
||||
save_global_weights();
|
||||
|
||||
Reference in New Issue
Block a user