Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ neoForge {
// Include resources generated by data generators.
sourceSets.main.resources { srcDir 'src/generated/resources' }

configurations {
jnaRuntime {
canBeConsumed = false
canBeResolved = true
visible = false
}
}

dependencies {
// Example mod dependency with JEI
Expand Down Expand Up @@ -145,8 +152,11 @@ dependencies {
// —— 不再 jarJar 嵌入。原因:Java 22 模块系统下,自嵌入的 jna jar 会形成
// "com.sun.jna" 自动模块,与其他 mod(如 Iris/Sodium 或 NeoForge 间接带)
// 提供的 jna 形成两个同名模块冲突,触发 ResolutionException。
// 缺 JNA 时由 OpenCLBackend.canExecute() 检测并 catch(Throwable) 回退到 CPU。
// 缺 JNA 时由 OpenCLBackend 构造 catch(Throwable) 回退到 CPU。
compileOnly 'net.java.dev.jna:jna:5.14.0'
// 训练 CLI / 单元测试需要真正加载 OpenCL;玩家运行时仍由环境提供 JNA,不打进 mod JAR。
testRuntimeOnly 'net.java.dev.jna:jna:5.14.0'
jnaRuntime 'net.java.dev.jna:jna:5.14.0'
}

test {
Expand All @@ -159,7 +169,7 @@ tasks.register('trainGoAI', JavaExec) {
description = 'Train the Go MCTS neural evaluator with self-play'
mainClass = 'com.wzz.game_console.client.screens.games.gogame.GoTrainingMain'
args((project.findProperty('trainArgs') ?: '').toString().trim().split("\\s+").findAll { !it.isEmpty() })
classpath = sourceSets.main.runtimeClasspath
classpath = sourceSets.main.runtimeClasspath + configurations.jnaRuntime
maxHeapSize = '4g'
}

Expand All @@ -176,15 +186,15 @@ tasks.register('adversarialTrain', JavaExec) {
description = '对抗训练:己方 MCTS AI vs 外部 KataGo 引擎'
mainClass = 'com.wzz.game_console.client.screens.games.gogame.GoAdversarialMain'
args((project.findProperty('advArgs') ?: '').toString().trim().split("\\s+").findAll { !it.isEmpty() })
classpath = sourceSets.main.runtimeClasspath
classpath = sourceSets.main.runtimeClasspath + configurations.jnaRuntime
maxHeapSize = '4g'
}

tasks.register('gpuProbe', JavaExec) {
group = 'training'
description = '探测 OpenCL GPU 设备是否可用'
mainClass = 'com.wzz.game_console.client.screens.games.gogame.GoGpuProbe'
classpath = sourceSets.main.runtimeClasspath
classpath = sourceSets.main.runtimeClasspath + configurations.jnaRuntime
}

// This block of code expands all declared replace properties in the specified resource targets.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

@OnlyIn(Dist.CLIENT)
Expand All @@ -33,6 +34,8 @@ public class GameSelectorScreen extends Screen {
private record GameEntry(String name, String icon, String description, String category, Supplier<Screen> factory) {}

private final List<GameEntry> games = new ArrayList<>();
private List<GameEntry> filteredGames = List.of();
private String filteredCategory;
private int currentPage = 0;
private int gamesPerPage = 8;
private long tickCount = 0;
Expand All @@ -49,6 +52,7 @@ private record GameEntry(String name, String icon, String description, String ca
/** 导入消息(临时显示) */
private String importMessage = null;
private long importMessageTime = 0;
private final AtomicBoolean importInProgress = new AtomicBoolean();

public GameSelectorScreen() {
super(Component.literal("Game Console 游戏机"));
Expand Down Expand Up @@ -105,12 +109,19 @@ private void initializeGames() {
}

private List<GameEntry> getFilteredGames() {
if ("全部".equals(filterCategory)) return games;
if (filterCategory.equals(filteredCategory)) return filteredGames;

filteredCategory = filterCategory;
if ("全部".equals(filterCategory)) {
filteredGames = List.copyOf(games);
return filteredGames;
}
List<GameEntry> filtered = new ArrayList<>();
for (GameEntry g : games) {
if (g.category.equals(filterCategory)) filtered.add(g);
}
return filtered;
filteredGames = List.copyOf(filtered);
return filteredGames;
}

@Override
Expand Down Expand Up @@ -266,6 +277,7 @@ private int getCategoryColor(String category) {

/** 打开文件对话框导入外部游戏设置 JSON */
private void importSettingsFromFile() {
if (!importInProgress.compareAndSet(false, true)) return;
// ★ Bug修复:FileDialog.setVisible(true) 是模态阻塞调用,在 MC 主线程直接打开
// 会冻结整个游戏(停帧、无响应)。改为在后台 daemon 线程弹窗,
// 用户选完后 mc.execute 回主线程执行实际导入与提示(importMessage 由 render 读取)
Expand Down Expand Up @@ -297,9 +309,9 @@ private void importSettingsFromFile() {
}

Path srcPath = srcFile.toPath();
// 回到 MC 主线程执行导入与界面提示
// 文件读取、解析和写盘留在后台;仅界面状态在 MC 主线程更新。
boolean success = GameSettings.importFromFile(srcPath);
Minecraft.getInstance().execute(() -> {
boolean success = GameSettings.importFromFile(srcPath);
importMessage = success ? "设置导入成功!" : "导入失败:文件格式不正确";
importMessageTime = System.currentTimeMillis();
});
Expand All @@ -312,6 +324,7 @@ private void importSettingsFromFile() {
} finally {
// ★ 修复 AWT Frame 泄漏:dispose 移入 finally,异常/用户取消路径也会释放
if (frame != null) frame.dispose();
importInProgress.set(false);
}
}, "GameConsole-SettingsImport");
picker.setDaemon(true);
Expand Down Expand Up @@ -348,12 +361,20 @@ public boolean mouseClicked(double mx, double my, int btn) {
return true;
}

// ─── 游戏卡片点击 ───
// ─── 游戏卡片点击:按本次点击坐标判断,不能使用上一帧的悬停结果 ───
List<GameEntry> filtered = getFilteredGames();
if (hoveredIndex >= 0 && hoveredIndex < filtered.size()) {
GameEntry entry = filtered.get(hoveredIndex);
Minecraft.getInstance().setScreen(entry.factory.get());
return true;
int cardW = Math.min(280, width - 40);
int cardX = cx - cardW / 2;
if (mx >= cardX && mx <= cardX + cardW) {
int startIdx = currentPage * gamesPerPage;
int endIdx = Math.min(startIdx + gamesPerPage, filtered.size());
for (int i = startIdx; i < endIdx; i++) {
int cardY = 68 + (i - startIdx) * 28;
if (my >= cardY && my <= cardY + 26) {
Minecraft.getInstance().setScreen(filtered.get(i).factory.get());
return true;
}
}
}

// ─── 翻页按钮 ───
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ private void startGame() {

public void tick() {
this.tickCount++;
if (!showExitConfirm) GameRenderHelper.tickParticles(this.particles);
if (this.lanMode == 0 && !this.localTwoPlayer && !showExitConfirm) {
if (this.state == State.PLAYING && !this.playerTurn && this.winner == 0) {
this.tickAiTurn();
Expand Down Expand Up @@ -589,7 +590,7 @@ private void renderPlaying(GuiGraphics g, int mx, int my) {
}
}

GameRenderHelper.tickAndRenderParticles(g, this.particles);
GameRenderHelper.renderParticles(g, this.particles);
GameRenderHelper.drawTopHUD(g, this.width, this.height);
String modeTag = this.localTwoPlayer ? " [本地双人]" : " [" + this.difficulty.label + "]";
String turnText;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ private void spawnFood() {
@Override public void tick() {
tickCount++;
floats.removeIf(f -> { f.update(); return !f.isAlive(); });
if (!showExitConfirm) GameRenderHelper.tickParticles(particles);
if (state != State.PLAYING || showExitConfirm) return; // 弹窗期间暂停游戏
tickCounter++;
int speed = Math.max(1, 4 - score / 15);
Expand Down Expand Up @@ -196,7 +197,7 @@ private void renderPlaying(GuiGraphics g) {
}

// 粒子
GameRenderHelper.tickAndRenderParticles(g, particles);
GameRenderHelper.renderParticles(g, particles);
// 浮动文字
for (GameRenderHelper.FloatingText ft : floats) ft.render(g, font);

Expand Down Expand Up @@ -232,4 +233,4 @@ private void renderGameOver(GuiGraphics g, int mx, int my) {
}

@Override public boolean isPauseScreen() { return false; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ private void initBoard() {
// ══════════════ TICK ══════════════
@Override public void tick() {
tickN++;
if (!showExitConfirm) GameRenderHelper.tickParticles(particles);
if (lanMode != LAN_NONE) return;
// ★ 关键修复:AI执黑(forWhite=false),仅黑方回合才触发
if (state==S.PLAYING && vsAI && !whiteTurn && !aiThinking && !promoPending) {
Expand Down Expand Up @@ -727,7 +728,7 @@ private void renderBoard(GuiGraphics g, int mx, int my) {
if (c==0) g.drawString(font,String.valueOf(8-r),sx+2,sy+2,light?0xFFB58863:0xFFF0D9B5);
if (r==7) g.drawString(font,String.valueOf((char)('a'+c)),sx+cellSize-6,sy+cellSize-10,light?0xFFB58863:0xFFF0D9B5);
}
GameRenderHelper.tickAndRenderParticles(g,particles);
GameRenderHelper.renderParticles(g,particles);
GameRenderHelper.drawTopHUD(g,width,height);
String ts=whiteTurn?"♔ 白方走棋":"♚ 黑方走棋";
if (vsAI&&!whiteTurn&&aiThinking) ts="AI 思考中...";
Expand Down Expand Up @@ -796,4 +797,4 @@ private void renderOver(GuiGraphics g, int mx, int my) {
GameRenderHelper.drawSecondaryButton(g,font,"ESC - 返回",cx2-70,cy2+44,140,18,mx,my);
}
@Override public boolean isPauseScreen() { return false; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ public static void main(String[] args) throws Exception {
long seed = longOption(options, "seed", 0x5EEDL);
Path weights = Path.of(required(options, "weights"));

String gpuVal = options.get("gpu");
if (gpuVal != null) {
System.setProperty("go.gpu", gpuVal);
}
NeuralEvaluator.GpuStatus gpu = NeuralEvaluator.detectGpu();
System.out.println("[GPU] " + gpu.describe());
NeuralEvaluator evaluator = new NeuralEvaluator();
if (Files.exists(weights)) {
evaluator.load(weights);
Expand All @@ -54,13 +60,16 @@ public static void main(String[] args) throws Exception {
try {
for (int gen = 0; gen < generations; gen++) {
double frac = generations > 1 ? (double) gen / (generations - 1) : 0.0;
double currentLR = Math.max(learningRate * 0.5 * (1.0 + Math.cos(Math.PI * frac)), 1e-6);
double currentLR = Math.max(learningRate * 0.5 * (1.0 + Math.cos(Math.PI * frac)),
learningRate * 0.1);
GoAdversarialTrainer.Result result = trainer.runGeneration(
games, parallelism, epochs, currentLR, seed + gen);
System.out.printf("generation=%d lr=%.6f games=%d completed=%d samples=%d ourWins=%d replay=%d meanLoss=%.8f%n",
gen + 1, currentLR, result.games, result.completedGames,
result.samples, result.ourWins, trainer.getReplayBufferSize(), result.meanLoss);
}
evaluator.save(weights);
System.out.println("saved=" + weights.toAbsolutePath());
} catch (RuntimeException | Error t) {
// ★ 崩溃保存:多代训练中途崩(OOM/GTP 异常/引擎崩溃)时,
// 已训练完的各代权重不能随进程一起丢掉
Expand All @@ -71,10 +80,9 @@ public static void main(String[] args) throws Exception {
System.err.println("save-on-crash failed: " + saveErr);
}
throw t;
} finally {
evaluator.release();
}

evaluator.save(weights);
System.out.println("saved=" + weights.toAbsolutePath());
}

private static Map<String, String> parse(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public final class GoAdversarialTrainer {
public static final class Config {
public int searchTimeMillis = 300;
public int maxIterations = 500;
public int maxMoves = 300;
public int maxMoves = 450;
public int batchSize = 128;
public double l2 = 1.0e-5;
public double gradientClip = 5.0;
Expand Down Expand Up @@ -97,8 +97,9 @@ public void clearReplayBuffer() {
* @return 训练结果
*/
public Result runGeneration(int games, int parallelism, int epochs, double learningRate, long seed) {
if (games < 0 || epochs < 0 || learningRate <= 0)
if (games < 0 || epochs < 0 || !Double.isFinite(learningRate) || learningRate <= 0)
throw new IllegalArgumentException("Invalid generation parameters");
NeuralEvaluator.validateTrainingParameters(learningRate, config.l2, config.gradientClip, config.momentum);
synchronized (generationLock) {
return runGenerationLocked(games, parallelism, epochs, learningRate, seed);
}
Expand All @@ -108,7 +109,6 @@ private Result runGenerationLocked(int games, int parallelism, int epochs, doubl
if (games == 0 || Thread.currentThread().isInterrupted()) return new Result(games, 0, 0, 0, evaluator, 0);

int workers = Math.max(1, Math.min(parallelism <= 0 ? 1 : parallelism, games));
final NeuralEvaluator.ModelWeights snapshot = evaluator.snapshot();
ExecutorService pool = Executors.newFixedThreadPool(workers);
List<Future<GameResult>> futures = new ArrayList<>(workers);
try {
Expand All @@ -117,7 +117,7 @@ private Result runGenerationLocked(int games, int parallelism, int epochs, doubl
for (; nextGame < workers; nextGame++) {
final int gameIndex = nextGame;
futures.add(pool.submit(() ->
playAdversarialGame(snapshot, seed + 0x9E3779B97F4A7C15L * gameIndex)));
playAdversarialGame(evaluator, seed + 0x9E3779B97F4A7C15L * gameIndex)));
}
int completed = 0;
int ourWins = 0;
Expand Down Expand Up @@ -158,7 +158,7 @@ private Result runGenerationLocked(int games, int parallelism, int epochs, doubl
if (nextGame < games) {
final int gameIndex = nextGame++;
futures.add(pool.submit(() ->
playAdversarialGame(snapshot, seed + 0x9E3779B97F4A7C15L * gameIndex)));
playAdversarialGame(evaluator, seed + 0x9E3779B97F4A7C15L * gameIndex)));
}
}

Expand Down Expand Up @@ -211,13 +211,13 @@ private static final class GameResult {
/**
* 一局对抗:MCTS AI vs KataGo,随机先后手。
*/
private GameResult playAdversarialGame(NeuralEvaluator.ModelWeights model, long seed) {
private GameResult playAdversarialGame(NeuralEvaluator sharedEvaluator, long seed) {
List<Sample> samples = new ArrayList<>();
Random rnd = new Random(seed);
boolean ourIsBlack = rnd.nextBoolean();

// 创建己方 AI
MCTSGoAI ourAI = new MCTSGoAI(config.searchTimeMillis, config.maxIterations, 1, model);
MCTSGoAI ourAI = new MCTSGoAI(config.searchTimeMillis, config.maxIterations, 1, sharedEvaluator, false);
ourAI.setRandomSeed(seed ^ 0x12345678);

Process process = null;
Expand Down Expand Up @@ -576,4 +576,4 @@ private static GoPlayer[][] applySymmetry(GoPlayer[][] board, int[] perm) {
}

private static double clamp(double value) { return Math.max(-1.0, Math.min(1.0, value)); }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
public final class GoGpuProbe {
public static void main(String[] args) {
System.out.println("=== OpenCL GPU 探针 ===");
try (OpenCLBackend backend = new OpenCLBackend()) {
if (backend.isAvailable()) {
try {
NeuralEvaluator.GpuStatus status = NeuralEvaluator.detectGpu();
System.out.println(status.describe());
if (status.available) {
System.out.println("SUCCESS: OpenCL GPU 加速可用");
System.out.println("设备: " + backend.getDeviceName());
System.out.println("设备: " + status.deviceName);
} else {
System.out.println("FAILED: OpenCL 不可用,将回退 CPU");
}
Expand Down
Loading
Loading