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

1953 lines
68 KiB
Swift
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.
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()
}
}
}