Files
Home-of-CS/content/post/dev_20251216/index.md
T
2026-04-23 13:39:28 +08:00

2114 lines
77 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
+++
date = '2025-12-16T16:46:30+08:00'
draft = false
title = '计算概论A 2025年秋 大作业(更新)'
tags = ['计算概论']
license = 'MIT Licence'
description = '供计算概论A助教验收'
+++
## 文件下载
已编译的二进制文件:[oasa25](oasa25)(需要先`chmod +x oasa25`才能运行)
Swift源代码:[oasa25.swift](oasa25.swift)
Botzone简单交互:[oasa25.cpp](oasa25.cpp)
> 注意:请使用macOS 14.0及以上版本运行上面的二进制文件。
## 基本信息
- 程序名称:oasa25
- 编写语言:Swift 5
- 编译环境:
- IDEXcode Version 26.0.1 (17A400)
- macOSTahoe 26.0.1 (25A362)
- 设备:MacBook Air (13英寸, M3, 2024年)
- 借助AIDeepSeek
- 主要用途:
- 在原来代码的基础上进行优化;
- 讲解Swift语法;
- 修复语法错误;
- 协助给程序取名;
- "oasa"(“おアサ”)是“大きいアサインメント”(“大作业”)的缩略;
- "25"是我的数学分析I期中考试成绩。
- 程序特色:
- 使用上:
- 友好的GUI界面;
- 包含分栏显示;
- 很多图标;
- 二次确认,防止误操作;
- 完善的存档保存/另存为/读取功能;
- 清晰的状态显示;
- 支持历史记录查看;
- 合法路径绿色高亮显示,被阻挡的路径红色高亮显示;
- 支持自由调节蒙特卡洛算法评分参数;
- 终端输出蒙特卡洛方法日志;
- 技术上:
- 使用SwiftUI的现成控件实现GUI界面,减少所需代码;
- 使用多个状态变量,状态划分清晰;
- 代码分成不同`struct`,分工实现不同功能;
- 变量名、函数名等清晰易读(感谢DeepSeek);
- 并行处理,提高运行效率;
- 实现蒙特卡洛算法;
- 缺点:不管怎么调,算法都有点傻傻的,只比随机算法好一点点,在Botzone上被打得毫无还手之力(期末了😭难debug)。
## 编译方法
```bash
swiftc -parse-as-library <swift源代码路径> -o oasa25
```
由于使用SwiftUI,需要使用macOS编译,如果助教老师使用的不是macOS,要不线下使用我的电脑验收吧。
## 程序结构(详见源代码)
- `import`部分:
- `Combine`:用于实现`ObservableObject`类型的`GameState`
- `SwiftUI`:提供程序的UI控件;
- `UniformTypeIdentifiers`:用于存取文件时对文件类型的规定;
- `enum GamePhase:`:设定三个游戏阶段的值;
- 选择棋子阶段`selectPiece`
- 移动棋子阶段`movePiece`
- 放置障碍物阶段`placeArrow`
- `struct HistoryEntry`:历史记录数据结构;
- id+字符串;
- `struct gameData`:规定游戏存档的结构;
- 棋盘数据`chessBoard`(二维数组);
- 回合数据`blackRound`Bool值);
- 历史记录数据`history`HistoryEntry数组);
- 选中棋子位置`selectedPieceRow``selectedPieceCol`(整数);
- 阶段数据`gamePhase`
- 上次计算的可用路径`availableMoves`(二维数组);
- 读档后无需再次计算;
- 游戏结束状态`isGameOver`Bool值);
- 自动下棋选项`blackStrategy``whiteStrategy`(字符串);
- 当前回合数`roundNum`(整数);
- 蒙特卡洛方法计算结果`mcList`(数组);
- 控制面板参数值`blackControlFactor``blackSafetyFactor``blackSurroundFactor``blackCenterFactor``whiteControlFactor``whiteSafetyFactor``whiteSurroundFactor``whiteCenterFactor`(均为双精度浮点类型);
- `class GameState`:游戏状态环境对象
- 声明并初始化各个状态变量(详细列表如上);
- `struct GameManager`:游戏逻辑管理器;
- `func saveGame`:借助Swift的`NSSavePanel()`功能进行存档的保存(同时会调用自定义的`saveChessBoardToJSON()`);
- `func loadGame`:借助Swift的`NSOpenPanel()`功能进行存档的读取(同时会调用自定义的`loadChessBoardFromJSON()`);
- `func saveChessBoardToJSON`:借助Swift的`JSONEncoder()`,保存当前棋局为`.json`存档;
- `func loadChessBoardFromJSON`:借助Swift的`JSONDecoder()`,从`.json`存档中读取各个变量,还原棋局;
- `func initializeChessBoard`:新建游戏时的棋盘初始化;
- `func addHistory`:将最近一步操作插入到`history`字符串组的索引`0`位置的一个简单函数;
- `func calculateAvailableMoves`:计算可移动的位置,用于给设定按钮状态和染色提供数据;
- `func getAllAvailablePieces`:获取所有可用的棋子位置,用于设定按钮状态;
- `func getAllCounterPieces`:除了参数相反,和上面函数功能相同;
- `func getAllAvailableMoves`:获取所有可移动/可放置障碍物的位置,染色;
- `func checkWinCondition``func checkAndHandleWinCondition`:检查胜利;
- `func handleSquareTap`:处理格子点击;
- 蒙特卡洛算法实现部分
- `private func generateMcListForCurrentTurn`:更新`mcList`状态变量
- `private func findBestMoveWithMonteCarlo`:通过给定第一行动回合+往下随机8个回合(并重走30次)计算平均得分,为上述函数提供计算结果;
- `private func getAllPossibleMoves`:模拟完整的一个回合的可行操作方案;
- `private func simulateRandomGame`:模拟完整一个回合的随机下棋过程;
- `private func getRandomMove`:随机选取一个回合的下棋方案(返回值:`piece: (Int, Int), target: (Int, Int), arrow: (Int, Int)`
- `private func isGameOver`:检查游戏是否结束(模拟下棋版);
- `private func calculateFinalScore`
- 若中途获胜:返回`1.0``-1.0`
- 否则使用下面函数的返回值;
- `private func generateRandomMcList`:生成随机走法列表(备用);
- 因为Swift要防止使用空值,所以需要一个这样的函数来保证每种情况都有值,实际上不会被调用;
- `private func evaluateBoard`:3项评估分数求和+归一化到[-1,1];
- `private func calculateSimuMoves`:计算可以移动的方案(不修改GameState版);
- `private func mcSelect`:基于已生成的`mcList`执行选择;
- `private func mcMove`:基于已生成的`mcList`执行移动;
- `private func mcPlace`:基于已生成的`mcList`放置障碍物;
- 综合的自动下棋:`func autoOperate`
- 随机下棋:
- `private func randomSelect`
- `private func randomMove`
- `private func randomPlace`
- 局势评估算法:
- `private func controlScore`:控制分=倍率*所有棋子可移动格子数目之和;
- `private func safetyScore`:安全分=每个棋子求和:
- +倍率*距离边界的最小距离;
- 若被完全包围:-包围惩罚;
- 否则:+倍率*所有棋子可移动格子数目之和;(怎么感觉和上面控制分有点重复)
- `private func centerScore`:中心分=每个棋子求和:
- 若在中心:+倍率*5
- +(10-离中心点距离)
- `struct ChessSquareView`:棋盘格子视图;
- 实现单个棋盘格子的显示;
- `func getSquareColor`:染色;
- `func isInteractive`:设定按钮状态;
- `struct ChessBoardRowView`:棋盘行视图;
- 实现棋盘单行的显示;
- 调用`struct ChessSquareView`
- `struct ChessBoardView`:棋盘视图;
- 实现棋盘的完整显示;
- 调用`struct ChessBoardRowView`
- `struct MenuButtonView`:菜单按钮视图;
- 作为定义主菜单按钮的模板,避免代码的大量重复;
- `struct SidebarView`:左侧栏视图;
- `var gameInfoView`:显示当前存档路径、当前回合、当前阶段、历史操作;
- `var phaseDescription`:把阶段的代号转换成文字描述;
- `var welcomeView`:显示第一局游戏开始前的引导信息;
- `struct ParameterSliderView`:负责单一滑块组件;
- `struct ParameterSettingsView`:生成完整滑块界面;
- `struct RightView`:右侧栏视图;
- 自动下棋控制面板,支持调节参数;
- `struct MainMenuView`:主菜单视图;
- 实现主菜单的显示;
- 多次调用`struct MenuButtonView`,实现各个菜单按钮;
- 依据状态变量的不同,显示不同的按钮;
- `struct ContentView`:主视图。
- 划分窗口结构:
- 调用`SidebarView`,实现左侧栏的状态显示;
- 调用`MainMenuView`,实现左下角的菜单控制;
- 调用`ChessBoardView`,实现右侧的棋盘显示。
- 调用`RightView`
- 定义函数`private func setWindowTitle`
- 使用`.onAppear`在程序启动时设置窗口标题。
## 源代码展示
下载[oasa25.swift](oasa25.swift)。
```swift
import Combine
import SwiftUI
import UniformTypeIdentifiers
// MARK: -
enum GamePhase: Int, Codable {
case selectPiece = 0
case movePiece = 1
case placeArrow = 2
}
// MARK: -
struct HistoryEntry: Identifiable, Codable, Hashable {
let id: UUID
let action: String
init(action: String) {
self.id = UUID()
self.action = action
}
//
var description: String { action }
}
// MARK: -
struct GameData: Codable {
let chessBoard: [[Int]]
let blackRound: Bool
let history: [HistoryEntry] // HistoryEntry
let selectedPieceRow: Int?
let selectedPieceCol: Int?
let gamePhase: GamePhase
let availableMoves: [[Int]]
let isGameOver: Bool //
let blackStrategy: String
let whiteStrategy: String
let roundNum: Int
let mcList: [Int]
let blackControlFactor: Double
let blackSafetyFactor: Double
let blackSurroundFactor: Double
let blackCenterFactor: Double
let whiteControlFactor: Double
let whiteSafetyFactor: Double
let whiteSurroundFactor: Double
let whiteCenterFactor: Double
}
// MARK: -
class GameState: ObservableObject {
@Published var withFile = false
@Published var chessBoard = Array(
repeating: Array(repeating: 0, count: 8),
count: 8
)
@Published var currentUrl: String = ""
@Published var unsavable = true
@Published var isPlaying = false
@Published var blackRound = true
@Published var history: [HistoryEntry] = [] // HistoryEntry
@Published var availableMoves: [[Int]] = Array(
repeating: Array(repeating: 0, count: 8),
count: 8
)
@Published var selectedPiece: (Int, Int)? = nil
@Published var gamePhase: GamePhase = .selectPiece
@Published var whiteAutoOperate = false
@Published var blackAutoOperate = false
@Published var isGameOver: Bool = false //
@Published var blackStrategy: String = "纯随机下棋"
@Published var whiteStrategy: String = "纯随机下棋"
@Published var roundNum: Int = 0
@Published var mcList: [Int] = [] // [,,,,,,,]
@Published var blackControlFactor: Double = 1
@Published var blackSafetyFactor: Double = 1
@Published var blackSurroundFactor: Double = 10
@Published var blackCenterFactor: Double = 1
@Published var whiteControlFactor: Double = 1
@Published var whiteSafetyFactor: Double = 1
@Published var whiteSurroundFactor: Double = 10
@Published var whiteCenterFactor: Double = 1
}
// MARK: -
struct GameManager {
let gameState: GameState
// MARK: -
func saveGame() {
let savePanel = NSSavePanel()
savePanel.title = "保存游戏"
savePanel.message = "选择保存游戏的目录和文件名"
savePanel.allowedContentTypes = [UTType.json]
savePanel.nameFieldStringValue = "新游戏.json"
savePanel.allowsOtherFileTypes = false
savePanel.begin { response in
if response == .OK, let url = savePanel.url {
self.gameState.currentUrl = url.path
if self.saveChessBoardToJSON(url: url) {
self.gameState.unsavable = true
self.gameState.withFile = true
}
}
}
}
func loadGame() {
let openPanel = NSOpenPanel()
openPanel.title = "读取游戏"
openPanel.message = "选择游戏存档文件"
openPanel.allowedContentTypes = [UTType.json]
openPanel.allowsMultipleSelection = false
openPanel.begin { response in
if response == .OK, let url = openPanel.url {
if self.loadChessBoardFromJSON(url: url) {
self.gameState.currentUrl = url.path
self.gameState.withFile = true
self.gameState.isPlaying = true
self.gameState.unsavable = true
}
}
}
}
func saveChessBoardToJSON(url: URL) -> Bool {
do {
let encoder = JSONEncoder()
// 使GameData
let gameData = GameData(
chessBoard: gameState.chessBoard,
blackRound: gameState.blackRound,
history: gameState.history, //
selectedPieceRow: gameState.selectedPiece?.0,
selectedPieceCol: gameState.selectedPiece?.1,
gamePhase: gameState.gamePhase,
availableMoves: gameState.availableMoves,
isGameOver: gameState.isGameOver, //
blackStrategy: gameState.blackStrategy,
whiteStrategy: gameState.whiteStrategy,
roundNum: gameState.roundNum,
mcList: gameState.mcList,
blackControlFactor: gameState.blackControlFactor,
blackSafetyFactor: gameState.blackSafetyFactor,
blackSurroundFactor: gameState.blackSurroundFactor,
blackCenterFactor: gameState.blackSafetyFactor,
whiteControlFactor: gameState.whiteControlFactor,
whiteSafetyFactor: gameState.whiteSafetyFactor,
whiteSurroundFactor: gameState.whiteSurroundFactor,
whiteCenterFactor: gameState.whiteCenterFactor
)
let data = try encoder.encode(gameData)
try data.write(to: url)
return true
} catch {
print("保存JSON失败: \(error)")
return false
}
}
func loadChessBoardFromJSON(url: URL) -> Bool {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let loadedData = try decoder.decode(GameData.self, from: data)
gameState.chessBoard = loadedData.chessBoard
gameState.blackRound = loadedData.blackRound
gameState.history = loadedData.history //
if let row = loadedData.selectedPieceRow,
let col = loadedData.selectedPieceCol
{
gameState.selectedPiece = (row, col)
} else {
gameState.selectedPiece = nil
}
gameState.gamePhase = loadedData.gamePhase
gameState.availableMoves = loadedData.availableMoves
gameState.isGameOver = loadedData.isGameOver //
return true
} catch {
print("读取JSON失败: \(error)")
return false
}
}
func initializeChessBoard() {
gameState.history = []
gameState.selectedPiece = nil
gameState.gamePhase = .selectPiece
gameState.availableMoves = Array(
repeating: Array(repeating: 0, count: 8),
count: 8
)
gameState.chessBoard = Array(
repeating: Array(repeating: 0, count: 8),
count: 8
)
gameState.isGameOver = false //
gameState.blackAutoOperate = false //
gameState.whiteAutoOperate = false //
// Amazon
// 1Amazon 2Amazon 3 4
gameState.chessBoard[0][2] = 1
gameState.chessBoard[0][5] = 1
gameState.chessBoard[2][0] = 1
gameState.chessBoard[2][7] = 1
gameState.chessBoard[7][2] = 2
gameState.chessBoard[7][5] = 2
gameState.chessBoard[5][0] = 2
gameState.chessBoard[5][7] = 2
}
func addHistory(_ action: String) {
let player = gameState.blackRound ? "黑方" : "白方"
let historyEntry = HistoryEntry(
action: "\(player)\(gameState.roundNum)回合 \(action)"
)
gameState.history.insert(historyEntry, at: 0)
}
// MARK: -
func calculateAvailableMoves(from position: (Int, Int)) {
gameState.availableMoves = Array(
repeating: Array(repeating: 0, count: 8),
count: 8
)
let (row, col) = position
let directions = [
(-1, -1), (-1, 0), (-1, 1),
(0, -1), (0, 1),
(1, -1), (1, 0), (1, 1),
]
for direction in directions {
var currentRow = row + direction.0
var currentCol = col + direction.1
var foundObstacle = false
while currentRow >= 0 && currentRow < 8 && currentCol >= 0
&& currentCol < 8
{
if !foundObstacle {
if gameState.chessBoard[currentRow][currentCol] != 0 {
foundObstacle = true
gameState.availableMoves[currentRow][currentCol] = 2
} else {
gameState.availableMoves[currentRow][currentCol] = 1
}
} else {
gameState.availableMoves[currentRow][currentCol] = 2
}
currentRow += direction.0
currentCol += direction.1
}
}
}
func getAllAvailablePieces() -> [(Int, Int)] {
let currentPlayer = gameState.blackRound ? 1 : 2
var availablePieces: [(Int, Int)] = []
for row in 0..<8 {
for col in 0..<8 {
if gameState.chessBoard[row][col] == currentPlayer {
availablePieces.append((row, col))
}
}
}
return availablePieces
}
func getAllCounterPieces() -> [(Int, Int)] {
let currentPlayer = gameState.blackRound ? 2 : 1
var availablePieces: [(Int, Int)] = []
for row in 0..<8 {
for col in 0..<8 {
if gameState.chessBoard[row][col] == currentPlayer {
availablePieces.append((row, col))
}
}
}
return availablePieces
}
func getAllAvailableMoves() -> [(Int, Int)] {
var availableMovesList: [(Int, Int)] = []
for row in 0..<8 {
for col in 0..<8 {
if gameState.availableMoves[row][col] == 1 {
availableMovesList.append((row, col))
}
}
}
return availableMovesList
}
func checkWinCondition() -> (isGameOver: Bool, winner: String?) {
//
if gameState.isGameOver {
let winner = gameState.blackRound ? "黑方" : "白方"
return (true, winner)
}
//
let opponentPieces = getAllCounterPieces()
//
for piece in opponentPieces {
//
calculateAvailableMoves(from: piece)
let availableMoves = getAllAvailableMoves()
//
if !availableMoves.isEmpty {
return (false, nil)
}
}
//
let winner = gameState.blackRound ? "黑方" : "白方"
return (true, winner)
}
func checkAndHandleWinCondition() -> Bool {
let winResult = checkWinCondition()
if winResult.isGameOver && !gameState.isGameOver {
//
gameState.isGameOver = true
//
addHistory("获胜")
//
gameState.blackAutoOperate = false
gameState.whiteAutoOperate = false
return true
}
return false
}
func handleSquareTap(row: Int, column: Int) {//
//
if gameState.isGameOver {
return
}
let currentPlayer = gameState.blackRound ? 1 : 2
if (gameState.blackRound && gameState.blackAutoOperate)
|| (!gameState.blackRound && gameState.whiteAutoOperate)
{
return
}
switch gameState.gamePhase {
case .selectPiece:
if gameState.blackRound {
gameState.roundNum += 1
}
if gameState.chessBoard[row][column] == currentPlayer {
gameState.selectedPiece = (row, column)
calculateAvailableMoves(from: (row, column))
gameState.gamePhase = .movePiece
addHistory("选择了棋子 (\(row),\(column))")
}
case .movePiece:
guard let selected = gameState.selectedPiece else { return }
if gameState.availableMoves[row][column] == 1 {
gameState.chessBoard[row][column] =
gameState.chessBoard[selected.0][selected.1]
gameState.chessBoard[selected.0][selected.1] = 0
gameState.selectedPiece = (row, column)
calculateAvailableMoves(from: (row, column))
gameState.gamePhase = .placeArrow
addHistory("移动棋子到 (\(row),\(column))")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if self.gameState.blackRound
&& self.gameState.blackAutoOperate
{
self.autoOperate(player: true)
} else if !self.gameState.blackRound
&& self.gameState.whiteAutoOperate
{
self.autoOperate(player: false)
}
}
} else if gameState.chessBoard[row][column] == currentPlayer {
gameState.selectedPiece = (row, column)
calculateAvailableMoves(from: (row, column))
addHistory("重新选择了棋子 (\(row),\(column))")
}
case .placeArrow:
if gameState.availableMoves[row][column] == 1 {
gameState.chessBoard[row][column] = gameState.blackRound ? 3 : 4
addHistory("放置障碍物在 (\(row),\(column))")
// 🎯
let isGameOver = checkAndHandleWinCondition()
if isGameOver {
//
gameState.selectedPiece = nil
gameState.availableMoves = Array(
repeating: Array(repeating: 0, count: 8),
count: 8
)
gameState.gamePhase = .selectPiece
gameState.unsavable = false
// blackRound
} else {
//
gameState.selectedPiece = nil
gameState.availableMoves = Array(
repeating: Array(repeating: 0, count: 8),
count: 8
)
gameState.gamePhase = .selectPiece
gameState.blackRound.toggle()
gameState.unsavable = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if self.gameState.blackRound
&& self.gameState.blackAutoOperate
{
self.autoOperate(player: true)
} else if !self.gameState.blackRound
&& self.gameState.whiteAutoOperate
{
self.autoOperate(player: false)
}
}
}
}
}
}
// MARK: -
private func generateMcListForCurrentTurn() {
let currentBoard = gameState.chessBoard
let isBlack = gameState.blackRound
//
gameState.mcList = []
//
gameState.mcList.append(gameState.roundNum)
gameState.mcList.append(isBlack ? 1 : 0) // 1:, 0:
//
if let bestMove = findBestMoveWithMonteCarlo(
board: currentBoard,
isBlack: isBlack
) {
//
gameState.mcList.append(bestMove.piece.0)
gameState.mcList.append(bestMove.piece.1)
gameState.mcList.append(bestMove.target.0)
gameState.mcList.append(bestMove.target.1)
gameState.mcList.append(bestMove.arrow.0)
gameState.mcList.append(bestMove.arrow.1)
print(
"\(isBlack ? "" : "")方蒙特卡洛生成走法: 棋子(\(bestMove.piece)) -> 移动(\(bestMove.target)) -> 箭(\(bestMove.arrow))"
)
} else {
// 使
generateRandomMcList()
}
}
private func findBestMoveWithMonteCarlo(board: [[Int]], isBlack: Bool) -> (
piece: (Int, Int), target: (Int, Int), arrow: (Int, Int)
)? {
let allMoves = getAllPossibleMoves(board: board, isBlack: isBlack)
if allMoves.isEmpty {
return nil
}
var bestMove:
(piece: (Int, Int), target: (Int, Int), arrow: (Int, Int))?
var bestScore: Double = -Double.infinity
let simulationsPerMove = 30 //
//
DispatchQueue.concurrentPerform(iterations: min(10, allMoves.count)) {
index in
guard index < allMoves.count else { return }
let move = allMoves[index]
var totalScore: Double = 0.0
for _ in 0..<simulationsPerMove {
let score = simulateRandomGame(
from: move,
board: board,
isBlack: isBlack
)
totalScore += score
}
let averageScore = totalScore / Double(simulationsPerMove)
// 使
if averageScore > bestScore {
bestScore = averageScore
bestMove = move
}
}
print("\(isBlack ? "" : "")方分数:\(bestScore)")
return bestMove
}
private func getAllPossibleMoves(board: [[Int]], isBlack: Bool) -> [(
piece: (Int, Int), target: (Int, Int), arrow: (Int, Int)
)] {
var allMoves:
[(piece: (Int, Int), target: (Int, Int), arrow: (Int, Int))] = []
let playerPiece = isBlack ? 1 : 2
//
for row in 0..<8 {
for col in 0..<8 {
if board[row][col] == playerPiece {
//
let movePositions = calculateSimuMoves(
from: (row, col),
board: board
)
for movePos in movePositions {
//
var tempBoard = board
tempBoard[movePos.0][movePos.1] = playerPiece
tempBoard[row][col] = 0
//
let arrowPositions = calculateSimuMoves(
from: movePos,
board: tempBoard
)
for arrowPos in arrowPositions {
allMoves.append(
(
piece: (row, col), target: movePos,
arrow: arrowPos
)
)
}
}
}
}
}
return allMoves
}
private func simulateRandomGame(
from move: (piece: (Int, Int), target: (Int, Int), arrow: (Int, Int)),
board: [[Int]],
isBlack: Bool
) -> Double {
// 1.
var simBoard = board
let playerPiece = isBlack ? 1 : 2
let arrowType = isBlack ? 3 : 4
//
simBoard[move.target.0][move.target.1] = playerPiece
simBoard[move.piece.0][move.piece.1] = 0
//
simBoard[move.arrow.0][move.arrow.1] = arrowType
// 2.
var currentIsBlack = !isBlack //
let maxSimulationSteps = 8 // 8
for _ in 0..<maxSimulationSteps {
//
if isGameOver(board: simBoard, isBlackTurn: currentIsBlack) {
//
return calculateFinalScore(
board: simBoard,
originalIsBlack: isBlack
)
}
//
guard
let randomMove = getRandomMove(
board: simBoard,
isBlack: currentIsBlack
)
else {
break
}
//
let currentPiece = currentIsBlack ? 1 : 2
let currentArrow = currentIsBlack ? 3 : 4
simBoard[randomMove.target.0][randomMove.target.1] = currentPiece
simBoard[randomMove.piece.0][randomMove.piece.1] = 0
simBoard[randomMove.arrow.0][randomMove.arrow.1] = currentArrow
//
currentIsBlack.toggle()
}
// 3.
return evaluateBoard(board: simBoard, originalIsBlack: isBlack)
}
private func getRandomMove(board: [[Int]], isBlack: Bool) -> (
piece: (Int, Int), target: (Int, Int), arrow: (Int, Int)
)? {
let allMoves = getAllPossibleMoves(board: board, isBlack: isBlack)
return allMoves.randomElement()
}
//
private func isGameOver(board: [[Int]], isBlackTurn: Bool) -> Bool {
let opponentIsBlack = !isBlackTurn
let opponentPiece = opponentIsBlack ? 1 : 2
//
for row in 0..<8 {
for col in 0..<8 {
if board[row][col] == opponentPiece {
let moves = calculateSimuMoves(
from: (row, col),
board: board
)
if !moves.isEmpty {
return false //
}
}
}
}
return true //
}
private func calculateFinalScore(board: [[Int]], originalIsBlack: Bool)
-> Double
{
//
let currentPlayerCanMove =
getRandomMove(board: board, isBlack: originalIsBlack) != nil
let opponentCanMove =
getRandomMove(board: board, isBlack: !originalIsBlack) != nil
if !opponentCanMove && currentPlayerCanMove {
return 1.0 //
} else if opponentCanMove && !currentPlayerCanMove {
return -1.0 //
}
//
return evaluateBoard(board: board, originalIsBlack: originalIsBlack)
}
//
private func generateRandomMcList() {
let availablePieces = getAllAvailablePieces()
guard let randomPiece = availablePieces.randomElement() else { return }
calculateAvailableMoves(from: randomPiece)
let availableMovesList = getAllAvailableMoves()
guard let randomMove = availableMovesList.randomElement() else {
return
}
//
var tempBoard = gameState.chessBoard
let playerPiece = gameState.blackRound ? 1 : 2
tempBoard[randomMove.0][randomMove.1] = playerPiece
tempBoard[randomPiece.0][randomPiece.1] = 0
let arrowMoves = calculateSimuMoves(from: randomMove, board: tempBoard)
guard let randomArrow = arrowMoves.randomElement() else { return }
// mcList
gameState.mcList = [
gameState.roundNum,
gameState.blackRound ? 1 : 0,
randomPiece.0, randomPiece.1,
randomMove.0, randomMove.1,
randomArrow.0, randomArrow.1,
]
}
private func evaluateBoard(board: [[Int]], originalIsBlack: Bool) -> Double
{
// 使
let blackScore =
controlScore(
forBlack: true,
board: board,
factor: gameState.blackControlFactor
)
+ safetyScore(
forBlack: true,
board: board,
factor: gameState.blackSafetyFactor,
factor_surround: gameState.blackSurroundFactor
)
+ centerScore(
forBlack: true,
board: board,
factor: gameState.blackCenterFactor
)
let whiteScore =
controlScore(
forBlack: false,
board: board,
factor: gameState.whiteControlFactor
)
+ safetyScore(
forBlack: false,
board: board,
factor: gameState.whiteSafetyFactor,
factor_surround: gameState.whiteSurroundFactor
)
+ centerScore(
forBlack: false,
board: board,
factor: gameState.whiteCenterFactor
)
let diff = Double(blackScore - whiteScore)
let normalized = tanh(diff / 100.0) // [-1, 1]
return originalIsBlack ? normalized : -normalized
}
// GameState
private func calculateSimuMoves(from: (Int, Int), board: [[Int]]) -> [(
Int, Int
)] {
var moves: [(Int, Int)] = []
let (row, col) = from
let directions = [
(-1, -1), (-1, 0), (-1, 1),
(0, -1), (0, 1),
(1, -1), (1, 0), (1, 1),
]
for direction in directions {
var currentRow = row + direction.0
var currentCol = col + direction.1
while currentRow >= 0 && currentRow < 8 && currentCol >= 0
&& currentCol < 8
{
if board[currentRow][currentCol] != 0 {
break //
}
moves.append((currentRow, currentCol))
currentRow += direction.0
currentCol += direction.1
}
}
return moves
}
private func mcSelect() {
// mcList
if gameState.mcList.isEmpty || gameState.mcList[0] != gameState.roundNum
{
generateMcListForCurrentTurn()
}
// mcList
guard gameState.mcList.count >= 8,
gameState.mcList[1] == (gameState.blackRound ? 1 : 0)
else {
// 使
randomSelect()
return
}
// mcList
let pieceRow = gameState.mcList[2]
let pieceCol = gameState.mcList[3]
//
gameState.selectedPiece = (pieceRow, pieceCol)
calculateAvailableMoves(from: (pieceRow, pieceCol))
gameState.gamePhase = .movePiece
addHistory("蒙特卡洛方法 选择了棋子 (\(pieceRow),\(pieceCol))")
}
private func mcMove(availableMovesList: [(Int, Int)], selected: (Int, Int))
{
// mcList
guard gameState.mcList.count >= 8,
gameState.mcList[0] == gameState.roundNum,
gameState.mcList[2] == selected.0,
gameState.mcList[3] == selected.1
else {
// 使
randomMove(
availableMovesList: availableMovesList,
selected: selected
)
return
}
// mcList
let targetRow = gameState.mcList[4]
let targetCol = gameState.mcList[5]
//
if availableMovesList.contains(where: { $0 == (targetRow, targetCol) })
{
//
gameState.chessBoard[targetRow][targetCol] =
gameState.chessBoard[selected.0][selected.1]
gameState.chessBoard[selected.0][selected.1] = 0
gameState.selectedPiece = (targetRow, targetCol)
calculateAvailableMoves(from: (targetRow, targetCol))
gameState.gamePhase = .placeArrow
addHistory("蒙特卡洛方法 移动棋子到 (\(targetRow),\(targetCol))")
} else {
// 使
randomMove(
availableMovesList: availableMovesList,
selected: selected
)
}
}
private func mcPlace(obstacleType: Int) {
// mcList
guard gameState.mcList.count >= 8,
gameState.mcList[0] == gameState.roundNum
else {
// 使
randomPlace(obstacleType: obstacleType)
return
}
// mcList
let arrowRow = gameState.mcList[6]
let arrowCol = gameState.mcList[7]
//
let availableArrows = getAllAvailableMoves()
if availableArrows.contains(where: { $0 == (arrowRow, arrowCol) }) {
//
gameState.chessBoard[arrowRow][arrowCol] = obstacleType
addHistory("蒙特卡洛方法 放置障碍物在 (\(arrowRow),\(arrowCol))")
} else {
// 使
randomPlace(obstacleType: obstacleType)
}
}
// MARK: -
func autoOperate(player isBlack: Bool) {
if gameState.isGameOver || isBlack != gameState.blackRound {
return
}
guard
gameState.blackRound == isBlack
&& (isBlack
? gameState.blackAutoOperate : gameState.whiteAutoOperate)
else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
let obstacleType = isBlack ? 3 : 4
switch self.gameState.gamePhase {
case .selectPiece:
//
if gameState.blackRound {
gameState.roundNum += 1
}
switch isBlack
? self.gameState.blackStrategy
: self.gameState.whiteStrategy
{
case "纯随机下棋":
randomSelect()
case "蒙特卡洛方法":
mcSelect()
default:
print("未选中策略,棋局冻结")
return
}
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
//
if self.gameState.isGameOver {
return
}
self.autoOperate(
player: self.gameState.blackRound
)
}
// if !foundValidPiece {
// //
// let winner = isBlack ? "" : ""
// self.addHistory(" \(winner)")//
// _ = self.checkAndHandleWinCondition()
// }
case .movePiece:
guard let selected = self.gameState.selectedPiece else {
return
}
let availableMovesList = self.getAllAvailableMoves()
switch isBlack
? self.gameState.blackStrategy
: self.gameState.whiteStrategy
{
case "纯随机下棋":
randomMove(
availableMovesList: availableMovesList,
selected: selected
)
case "蒙特卡洛方法":
mcMove(
availableMovesList: availableMovesList,
selected: selected
)
default:
print("未选中策略,棋局冻结")
return
}
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
//
if self.gameState.isGameOver {
return
}
self.autoOperate(player: isBlack)
}
case .placeArrow:
switch isBlack
? self.gameState.blackStrategy
: self.gameState.whiteStrategy
{
case "纯随机下棋":
randomPlace(obstacleType: obstacleType)
case "蒙特卡洛方法":
mcPlace(obstacleType: obstacleType)
default:
print("未选中策略,棋局冻结")
return
}
// 🎯
let isGameOver = self.checkAndHandleWinCondition()
if isGameOver {
//
self.gameState.selectedPiece = nil
self.gameState.availableMoves = Array(
repeating: Array(repeating: 0, count: 8),
count: 8
)
self.gameState.gamePhase = .selectPiece
self.gameState.unsavable = false
// blackRound
} else {
//
self.gameState.selectedPiece = nil
self.gameState.availableMoves = Array(
repeating: Array(repeating: 0, count: 8),
count: 8
)
self.gameState.gamePhase = .selectPiece
self.gameState.blackRound.toggle()
self.gameState.unsavable = false
//
DispatchQueue.main.asyncAfter(
deadline: .now() + 0.3
) {
//
if self.gameState.isGameOver {
return
}
if self.gameState.blackRound
&& self.gameState.blackAutoOperate
{
self.autoOperate(player: true)
} else if !self.gameState.blackRound
&& self.gameState.whiteAutoOperate
{
self.autoOperate(player: false)
}
}
}
}
}
}
// MARK: 3
private func randomSelect() {
//
let availablePieces = self.getAllAvailablePieces()
let shuffledPieces = availablePieces.shuffled()
for piece in shuffledPieces {
self.calculateAvailableMoves(from: piece)
let availableMovesList = self.getAllAvailableMoves()
if !availableMovesList.isEmpty {
self.gameState.selectedPiece = piece //
self.gameState.gamePhase = .movePiece //
self.addHistory(
"自动随机下棋 选择了棋子 (\(piece.0),\(piece.1))"
)
break
}
}
//
}
private func randomMove(
availableMovesList: [(Int, Int)],
selected: (Int, Int)
) {
//
if let randomMove = availableMovesList.randomElement() {
self.gameState.chessBoard[randomMove.0][randomMove.1] =
self.gameState.chessBoard[selected.0][selected.1] //
self.gameState.chessBoard[selected.0][selected.1] = 0 //
self.gameState.selectedPiece = (
randomMove.0, randomMove.1
) //
self.calculateAvailableMoves(
from: (randomMove.0, randomMove.1)
)
self.gameState.gamePhase = .placeArrow
self.addHistory(
"自动随机下棋 移动棋子到 (\(randomMove.0),\(randomMove.1))"
)
} else {
self.addHistory("自动随机下棋 当前棋子无有效移动,重新选择")
self.gameState.gamePhase = .selectPiece
self.gameState.selectedPiece = nil
self.gameState.availableMoves = Array(
repeating: Array(repeating: 0, count: 8),
count: 8
)
}
//
}
private func randomPlace(obstacleType: Int) {
//
let availableArrows = self.getAllAvailableMoves()
if let randomArrow = availableArrows.randomElement() {
self.gameState.chessBoard[randomArrow.0][
randomArrow.1
] =
obstacleType
self.addHistory(
"自动随机下棋 放置障碍物在 (\(randomArrow.0),\(randomArrow.1))"
)
}
//
}
// MARK:
private func controlScore(forBlack: Bool, board: [[Int]], factor: Double)
-> Double
{ //
let targetPiece = forBlack ? 1 : 2
var score = 0.0
//
for row in 0..<8 {
for col in 0..<8 {
if board[row][col] == targetPiece {
score +=
factor
* Double(
calculateSimuMoves(from: (row, col), board: board)
.count
)
}
}
}
return score
}
private func safetyScore(
forBlack: Bool,
board: [[Int]],
factor: Double,
factor_surround: Double
) -> Double {
let targetPiece = forBlack ? 1 : 2
var safety = 0.0
for row in 0..<8 {
for col in 0..<8 {
if board[row][col] == targetPiece {
//
let moves = calculateSimuMoves(
from: (row, col),
board: board
)
if moves.isEmpty {
safety -= factor_surround //
} else {
safety += factor * Double(moves.count)
}
//
let edgeDistance = min(row, 7 - row, col, 7 - col)
safety += factor * Double(edgeDistance) //
}
}
}
return safety
}
private func centerScore(forBlack: Bool, board: [[Int]], factor: Double)
-> Double
{
let targetPiece = forBlack ? 1 : 2
var centerScore = 0.0
let centerCells = [(3, 3), (3, 4), (4, 3), (4, 4)]
//
for center in centerCells {
if board[center.0][center.1] == targetPiece {
centerScore += factor * 5
}
}
//
for row in 0..<8 {
for col in 0..<8 {
if board[row][col] == targetPiece {
let dx = abs(Double(row) - 3.5)
let dy = abs(Double(col) - 3.5)
let distance = sqrt(dx * dx + dy * dy)
centerScore += factor * (10 - distance) //
}
}
}
return centerScore
}
}
// MARK: -
struct ChessSquareView: View {
let row: Int
let column: Int
let value: Int
let action: () -> Void
let blackRound: Bool
let gamePhase: GamePhase
let availableMoves: [[Int]]
@State private var showContent = true
var body: some View {
ZStack {
Rectangle()
.fill(getSquareColor())
.frame(width: 80, height: 80)
.overlay(
Rectangle().stroke(Color.black.opacity(0.2), lineWidth: 1)
)
Button(action: action) {
ZStack {
Rectangle()
.fill(Color.blue.opacity(0.01))
.frame(width: 75, height: 75)
if value == 1 {
Image(systemName: "person.fill")
.font(.system(size: 30))
.opacity(showContent ? 1 : 0)
} else if value == 2 {
Image(systemName: "person")
.font(.system(size: 30))
.opacity(showContent ? 1 : 0)
} else if value == 3 {
Image(systemName: "nosign.app.fill")
.font(.system(size: 30))
.opacity(showContent ? 1 : 0)
} else if value == 4 {
Image(systemName: "nosign.app")
.font(.system(size: 30))
.opacity(showContent ? 1 : 0)
}
}
}
.disabled(!isInteractive())
.buttonStyle(PlainButtonStyle())
}
.onChange(of: value) { _, _ in
withAnimation(.easeInOut(duration: 0.3)) {
showContent = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
withAnimation(.easeInOut(duration: 0.3)) {
showContent = true
}
}
}
}
.onAppear {
withAnimation(.easeInOut(duration: 0.3)) {
showContent = true
}
}
}
private func getSquareColor() -> Color {
if availableMoves[row][column] == 1 {
return Color.green.opacity(0.5)
} else if availableMoves[row][column] == 2 {
return Color.red.opacity(0.5)
} else {
return (row + column) % 2 == 0
? Color.white : Color.gray.opacity(0.3)
}
}
private func isInteractive() -> Bool {
let currentPlayer = blackRound ? 1 : 2
switch gamePhase {
case .selectPiece:
return value == currentPlayer
case .movePiece:
return availableMoves[row][column] == 1 || value == currentPlayer
case .placeArrow:
return availableMoves[row][column] == 1
}
}
}
// MARK: -
struct ChessBoardRowView: View {
let row: Int
let chessBoard: [[Int]]
let onSquareTap: (Int, Int) -> Void
let blackRound: Bool
let gamePhase: GamePhase
let availableMoves: [[Int]]
var body: some View {
HStack(spacing: 0) {
ForEach(0..<8, id: \.self) { column in
ChessSquareView(
row: row,
column: column,
value: chessBoard[row][column],
action: { onSquareTap(row, column) },
blackRound: blackRound,
gamePhase: gamePhase,
availableMoves: availableMoves
)
}
}
}
}
// MARK: -
struct ChessBoardView: View {
let chessBoard: [[Int]]
let onSquareTap: (Int, Int) -> Void
let blackRound: Bool
let gamePhase: GamePhase
let availableMoves: [[Int]]
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
Rectangle()
.fill(Color.clear)
.frame(width: 30, height: 30)
ForEach(0..<8, id: \.self) { column in
Text(String(column))
.font(.system(size: 16, weight: .medium))
.frame(width: 80, height: 30)
.foregroundColor(.primary)
}
}
HStack(spacing: 0) {
VStack(spacing: 0) {
ForEach(0..<8, id: \.self) { row in
Text(String(row))
.font(.system(size: 16, weight: .medium))
.frame(width: 30, height: 80)
.foregroundColor(.primary)
}
}
VStack(spacing: 0) {
ForEach(0..<8, id: \.self) { row in
ChessBoardRowView(
row: row,
chessBoard: chessBoard,
onSquareTap: onSquareTap,
blackRound: blackRound,
gamePhase: gamePhase,
availableMoves: availableMoves
)
}
}
.overlay(Rectangle().stroke(Color.black, lineWidth: 2))
.padding()
}
}
.frame(width: 800)
}
}
// MARK: -
struct MenuButtonView: View {
let title: String
let icon: String
let action: () -> Void
let disabled: Bool
var body: some View {
Button(action: action) {
Label(title, systemImage: icon)
.font(.system(size: 15))
.frame(maxWidth: 150)
}
.disabled(disabled)
.buttonStyle(.bordered)
}
}
// MARK: -
struct SidebarView: View {
@EnvironmentObject var gameState: GameState
var body: some View {
VStack {
if gameState.isPlaying {
gameInfoView
} else {
Text("请先打开一个存档,或开启新游戏……")
.font(.system(size: 25, weight: .medium))
Spacer()
}
}
.frame(width: 320)
}
private var gameInfoView: some View {
VStack {
Spacer()
.frame(height: 5)
HStack {
Image(systemName: "folder")
.font(.system(size: 10))
.foregroundStyle(.secondary)
if gameState.withFile {
Text("当前存档文件:\(gameState.currentUrl)")
.font(.system(size: 10))
.foregroundStyle(.secondary)
} else {
Text("新游戏:尚未存档")
.font(.system(size: 10))
.foregroundStyle(.secondary)
}
}
HStack {
Image(systemName: "flag.filled.and.flag.crossed")
.font(.system(size: 25))
.foregroundStyle(.primary)
Text("当前行动方:")
.font(.system(size: 25, weight: .medium))
if gameState.blackRound {
HStack {
Image(systemName: "person.fill")
.font(.system(size: 25))
.foregroundStyle(.primary)
Text("黑方")
.font(.system(size: 25, weight: .medium))
}
} else {
HStack {
Image(systemName: "person")
.font(.system(size: 25))
.foregroundStyle(.primary)
Text("白方")
.font(.system(size: 25, weight: .medium))
}
}
}
HStack {
Image(systemName: "number")
.font(.system(size: 20))
.foregroundStyle(.primary)
Text("当前回合数:\(gameState.roundNum)")
.font(.system(size: 20, weight: .medium))
}
HStack {
Image(systemName: "flag.circle")
.font(.system(size: 15))
Text("当前行动阶段:")
.font(.system(size: 15))
switch gameState.gamePhase {
case .selectPiece:
HStack {
if gameState.blackRound {
Image(systemName: "person.fill")
.font(.system(size: 15))
} else {
Image(systemName: "person")
.font(.system(size: 15))
}
Text("选择棋子")
.font(.system(size: 15))
}
case .movePiece:
HStack {
if gameState.blackRound {
Image(systemName: "arrow.right.circle.fill")
.font(.system(size: 15))
} else {
Image(systemName: "arrow.right.circle")
.font(.system(size: 15))
}
Text("移动棋子")
.font(.system(size: 15))
}
case .placeArrow:
HStack {
if gameState.blackRound {
Image(systemName: "nosign.app.fill")
.font(.system(size: 15))
} else {
Image(systemName: "nosign.app")
.font(.system(size: 15))
}
Text("放置障碍物")
.font(.system(size: 15))
}
}
}
HStack {
Image(systemName: "clock")
.font(.system(size: 15))
Text("历史操作:")
.font(.system(size: 15))
}
// List使
List(gameState.history) { entry in
Text(entry.description)
.font(.system(size: 12))
}
.frame(height: 240)
}
}
}
//
struct ParameterSliderView: View {
@Binding var value: Double
let title: String
let description: String
let range: ClosedRange<Double>
let step: Double
var body: some View {
VStack(alignment: .leading, spacing: 8) {
//
HStack {
Text(title)
.font(.headline)
Spacer()
Text("\(value, specifier: "%.1f")")
.font(.headline)
.foregroundColor(.blue)
}
//
Slider(value: $value, in: range, step: step)
.accentColor(.blue)
//
Text(description)
.font(.caption)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.vertical, 8)
}
}
//
struct ParameterSettingsView: View {
@Binding var controlFactor: Double
@Binding var safetyFactor: Double
@Binding var surroundFactor: Double
@Binding var centerFactor: Double
let isBlack: Bool
var body: some View {
VStack(alignment: .leading, spacing: 16) {
//
HStack {
Image(
systemName: isBlack
? "arrowshape.left.arrowshape.right.fill"
: "arrowshape.left.arrowshape.right"
)
.font(.system(size: 16))
Text("\(isBlack ? "黑方" : "白方")蒙特卡洛参数")
.font(.headline)
}
.padding(.bottom, 8)
//
ParameterSliderView(
value: $controlFactor,
title: "控制分数倍率",
description: "调高使算法更重视可用移动数目",
range: 1...10,
step: 0.1
)
ParameterSliderView(
value: $safetyFactor,
title: "安全分数倍率",
description: "调高使算法更重视不被困住",
range: 1...10,
step: 0.1
)
ParameterSliderView(
value: $surroundFactor,
title: "包围惩罚系数",
description: "棋子被完全包围时的惩罚值",
range: 5...100,
step: 0.5
)
ParameterSliderView(
value: $centerFactor,
title: "中心分数倍率",
description: "调高使算法更重视占领棋盘中心",
range: 1...10,
step: 0.1
)
//
HStack(spacing: 12) {
Button("均衡型") {
controlFactor = 1.0
safetyFactor = 1.0
surroundFactor = 10.0
centerFactor = 1.0
}
.buttonStyle(.bordered)
.font(.caption)
Button("进攻型") {
controlFactor = 1.6
safetyFactor = 0.6
surroundFactor = 6.0
centerFactor = 1.6
}
.buttonStyle(.bordered)
.font(.caption)
Button("防守型") {
controlFactor = 0.6
safetyFactor = 1.6
surroundFactor = 16.0
centerFactor = 0.6
}
.buttonStyle(.bordered)
.font(.caption)
}
.padding(.top, 8)
}
.padding()
.background(Color.gray.opacity(0.05))
.cornerRadius(12)
}
}
// MARK: -
struct RightView: View {
@EnvironmentObject var gameState: GameState
let gameManager: GameManager
let options = ["纯随机下棋", "蒙特卡洛方法"]
var body: some View {
VStack {
Spacer()
.frame(height: 5)
if gameState.isPlaying {
//
VStack(spacing: 8) {
HStack {
Image(systemName: "command")
.font(.system(size: 25))
.foregroundStyle(.primary)
Text("自动下棋控制面板")
.font(.system(size: 25))
.foregroundStyle(.primary)
}
.padding(.top, 12)
Divider()
.padding(.horizontal)
}
.background(Color(nsColor: .windowBackgroundColor))
ScrollView {
//
HStack {
Image(systemName: "command.circle.fill")
.font(.system(size: 15))
Toggle("黑方自动下棋", isOn: $gameState.blackAutoOperate)
.disabled(gameState.isGameOver)
.onChange(of: gameState.blackAutoOperate) {
_,
newValue in
if newValue && gameState.blackRound {
gameManager.autoOperate(player: true)
}
}
}
.padding(.horizontal)
HStack {
Image(systemName: "gear.circle.fill")
.font(.system(size: 15))
Picker("自动下棋策略", selection: $gameState.blackStrategy) {
ForEach(options, id: \.self) { option in
Text(option).tag(option)
}
}
.disabled(gameState.isGameOver)
.frame(maxWidth: 250)
}
.padding(.horizontal)
//
if gameState.blackStrategy == "蒙特卡洛方法"
&& !gameState.isGameOver
{
ParameterSettingsView(
controlFactor: $gameState.blackControlFactor,
safetyFactor: $gameState.blackSafetyFactor,
surroundFactor: $gameState.blackSurroundFactor,
centerFactor: $gameState.blackCenterFactor,
isBlack: true
)
.padding(.horizontal)
}
Divider()
.padding(.horizontal)
//
HStack {
Image(systemName: "command.circle")
.font(.system(size: 15))
Toggle("白方自动下棋", isOn: $gameState.whiteAutoOperate)
.disabled(gameState.isGameOver)
.onChange(of: gameState.whiteAutoOperate) {
_,
newValue in
if newValue && !gameState.blackRound {
gameManager.autoOperate(player: false)
}
}
}
.padding(.horizontal)
HStack {
Image(systemName: "gear.circle")
Picker("自动下棋策略", selection: $gameState.whiteStrategy) {
ForEach(options, id: \.self) { option in
Text(option).tag(option)
}
}
.disabled(gameState.isGameOver)
.frame(maxWidth: 250)
}
.padding(.horizontal)
//
if gameState.whiteStrategy == "蒙特卡洛方法"
&& !gameState.isGameOver
{
ParameterSettingsView(
controlFactor: $gameState.whiteControlFactor,
safetyFactor: $gameState.whiteSafetyFactor,
surroundFactor: $gameState.whiteSurroundFactor,
centerFactor: $gameState.whiteCenterFactor,
isBlack: false
)
.padding(.horizontal)
}
Spacer()
}
} else {
Text("开始游戏以展示自动操作选项……")
.font(.system(size: 25, weight: .medium))
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding()
Spacer()
}
}
.frame(width: 280)
}
}
// MARK: -
struct MainMenuView: View {
@EnvironmentObject var gameState: GameState
let gameManager: GameManager
@State private var showNewGameDoubleCheck = false
@State private var showLoadDoubleCheck = false
@State private var showQuitDoubleCheck = false
var body: some View {
VStack {
Spacer()
.frame(height: 50)
HStack {
Image(systemName: "list.bullet")
.font(.system(size: 20))
.foregroundStyle(.primary)
Text("Amazon棋 菜单")
.font(.system(size: 20))
.foregroundStyle(.primary)
}
HStack {
Image(systemName: "info")
.font(.system(size: 20))
.foregroundStyle(.secondary)
VStack {
Text("2025秋 计算概论A 大作业")
.font(.system(size: 10))
.foregroundStyle(.secondary)
Text("陈奕辰 数学科学学院 2500010834")
.font(.system(size: 10))
.foregroundStyle(.secondary)
}
}
Spacer()
.frame(height: 15)
if gameState.unsavable {
MenuButtonView(
title: "新游戏",
icon: "plus",
action: newGame,
disabled: false
)
} else {
MenuButtonView(
title: "新游戏",
icon: "plus",
action: { showNewGameDoubleCheck = true },
disabled: false
)
}
Spacer().frame(height: 15)
if gameState.isPlaying {
if gameState.unsavable {
MenuButtonView(
title: "已保存",
icon: "square.and.arrow.down",
action: quickSave,
disabled: true
)
} else {
MenuButtonView(
title: "快速保存",
icon: "square.and.arrow.down",
action: quickSave,
disabled: !gameState.withFile
)
Spacer().frame(height: 15)
MenuButtonView(
title: "另存为",
icon: "document.badge.plus",
action: saveGame,
disabled: false
)
}
Spacer().frame(height: 15)
}
if gameState.unsavable {
MenuButtonView(
title: "读取",
icon: "folder",
action: loadGame,
disabled: false
)
Spacer().frame(height: 15)
MenuButtonView(
title: "退出全部",
icon: "xmark",
action: quitGame,
disabled: false
)
} else {
MenuButtonView(
title: "读取",
icon: "folder",
action: { showLoadDoubleCheck = true },
disabled: false
)
Spacer().frame(height: 15)
MenuButtonView(
title: "退出全部",
icon: "xmark",
action: { showQuitDoubleCheck = true },
disabled: false
)
}
}
.alert("舍弃当前棋局并创建新棋局?", isPresented: $showNewGameDoubleCheck) {
Button("返回", role: .cancel) {}
Button("继续", role: .destructive) { newGame() }
} message: {
Text("当前棋局尚未保存,是否继续创建新游戏?")
}
.alert("舍弃当前棋局并读取?", isPresented: $showLoadDoubleCheck) {
Button("返回", role: .cancel) {}
Button("继续", role: .destructive) { loadGame() }
} message: {
Text("当前棋局尚未保存,是否继续读取其他棋局?")
}
.alert("舍弃当前棋局并退出?", isPresented: $showQuitDoubleCheck) {
Button("返回", role: .cancel) {}
Button("继续", role: .destructive) { quitGame() }
} message: {
Text("当前棋局尚未保存,是否继续退出?")
}
Spacer().frame(height: 20)
}
private func newGame() {
gameManager.initializeChessBoard()
gameState.isPlaying = true
gameState.unsavable = false
gameState.withFile = false
gameState.blackRound = true
}
private func saveGame() {
gameManager.saveGame()
}
private func loadGame() {
gameManager.loadGame()
}
private func quickSave() {
let url = URL(fileURLWithPath: gameState.currentUrl)
_ = gameManager.saveChessBoardToJSON(url: url)
gameState.unsavable = true
}
private func quitGame() {
NSApplication.shared.terminate(nil)
}
}
// MARK: -
struct ContentView: View {
@StateObject private var gameState = GameState()
private var gameManager: GameManager {
GameManager(gameState: gameState)
}
var body: some View {
HStack(spacing: 0) {
VStack {
SidebarView()
.environmentObject(gameState)
Rectangle()
.fill(Color.gray.opacity(0.3))
.frame(height: 1)
MainMenuView(gameManager: gameManager)
.environmentObject(gameState)
}
Rectangle()
.fill(Color.gray.opacity(0.3))
.frame(width: 1)
.padding()
ChessBoardView(
chessBoard: gameState.chessBoard,
onSquareTap: { row, column in
gameManager.handleSquareTap(row: row, column: column)
},
blackRound: gameState.blackRound,
gamePhase: gameState.gamePhase,
availableMoves: gameState.availableMoves
)
Rectangle()
.fill(Color.gray.opacity(0.3))
.frame(width: 1)
.padding()
RightView(gameManager: gameManager)
.environmentObject(gameState)
}
.onAppear {
setWindowTitle()
}
}
private func setWindowTitle() {
if let window = NSApplication.shared.windows.first {
window.title = "这个大アサ的作者数分I期中考了25分高分"
}
}
}
@main
struct oasa25App:App
{
var body: some Scene
{
WindowGroup
{
ContentView()
}
}
}
```
## 注
和我之前发的[计算概论A 2025年秋 大作业](http://cirrus.org.cn/p/计算概论a-2025年秋-大作业/)差不多,只是把.app文件编译成了一个单独的二进制文件。