docs(workflow): add comprehensive git workflow standards #1

Merged
chaulmark merged 9 commits from develop into main 2025-01-06 17:06:02 +00:00
43 changed files with 40718 additions and 927 deletions

71
.swiftlint.yml Normal file
View file

@ -0,0 +1,71 @@
# SwiftLint Configuration for ChessPrism
disabled_rules:
- trailing_whitespace
- line_length
- function_body_length
- file_length
- type_body_length
- identifier_name
opt_in_rules:
- empty_count
- closure_spacing
- force_unwrapping
- force_cast
- discouraged_direct_init
- discouraged_object_literal
included:
- ChessPrism
- ChessPrismTests
- ChessPrismUITests
excluded:
- Carthage
- Pods
- SwiftGen
- R.generated.swift
line_length: 200
warning: 150
type_name:
min_length: 3
max_length: 40
identifier_name:
min_length: 3
max_length: 40
excluded:
- id
- x
- y
- z
- i
- j
- k
reporter: "xcode"
analyzer_rules:
- explicit_self
- unused_import
- unused_declaration
- unused_setter_value
- unused_optional_binding
- unused_enumerated
- unused_capture_list
- unused_closure_parameter
- unused_private_declaration
- unused_type_parameter
- unused_optional_chain
- unused_optional_chain_result
- unused_optional_chain_result_void
- unused_optional_chain_result_non_void
- unused_optional_chain_result_optional
- unused_optional_chain_result_non_optional
- unused_optional_chain_result_optional_void
- unused_optional_chain_result_non_optional_void
- unused_optional_chain_result_optional_non_void
- unused_optional_chain_result_non_optional_non_void

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -389,11 +389,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = ChessPrism/ChessPrism.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"ChessPrism/Preview Content\"";
DEVELOPMENT_TEAM = RJHWWWSF6Q;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
@ -411,6 +412,7 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.2;
MARKETING_VERSION = 1.0;
"OTHER_CODE_SIGN_FLAGS[sdk=*]" = "--deep --force";
PRODUCT_BUNDLE_IDENTIFIER = com.chessprism.ChessPrism;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
@ -428,11 +430,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = ChessPrism/ChessPrism.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"ChessPrism/Preview Content\"";
DEVELOPMENT_TEAM = RJHWWWSF6Q;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -1,6 +1,7 @@
{
"images" : [
{
"filename" : "ChessPrism_Icon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
@ -12,6 +13,7 @@
"value" : "dark"
}
],
"filename" : "ChessPrism_Icon 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
@ -23,56 +25,67 @@
"value" : "tinted"
}
],
"filename" : "ChessPrism_Icon 2.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"filename" : "ChessPrism_Icon 3.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "ChessPrism_Icon 4.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "ChessPrism_Icon 6.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "ChessPrism_Icon 5.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "ChessPrism_Icon 8.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "ChessPrism_Icon 7.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "ChessPrism_Icon 10.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "ChessPrism_Icon 9.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "ChessPrism_Icon 12.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "ChessPrism_Icon 11.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"

View file

@ -0,0 +1,165 @@
import Foundation
import CoreImage
import Vision
/// Errors that can occur during board detection and processing
enum BoardDetectionError: Error {
case boardNotFound
case invalidBoardDimensions
case squareExtractionFailed
case imageProcessingFailed
}
final class BoardDetector {
// Share CIContext to avoid creating too many Metal command queues
private static let shared = CIContext()
private var context: CIContext { BoardDetector.shared }
func detectBoard(in image: CIImage) -> CGRect? {
let request = VNDetectRectanglesRequest()
request.minimumAspectRatio = 0.8
request.maximumAspectRatio = 1.2
request.minimumSize = 0.4
request.maximumObservations = 1
request.quadratureTolerance = 30
request.minimumConfidence = 0.9
let requestHandler = VNImageRequestHandler(ciImage: image, options: [:])
do {
try requestHandler.perform([request])
} catch {
print("ERROR: Rectangle detection failed - \(error)")
return nil
}
guard let observations = request.results,
!observations.isEmpty else {
return nil
}
let bestObservation = observations[0]
let imageSize = image.extent.size
let transform = CGAffineTransform(scaleX: imageSize.width, y: imageSize.height)
let detectedRect = bestObservation.boundingBox.applying(transform)
guard validateDetectedRect(detectedRect, in: image) else {
return nil
}
return detectedRect
}
/// Piece recognizer for analyzing extracted squares
private let pieceRecognizer: PieceRecognizer
/// Initialize with a piece recognizer
init(pieceRecognizer: PieceRecognizer) {
self.pieceRecognizer = pieceRecognizer
}
/// Extract a specific square from the board image
private func extractSquare(from image: CIImage, in rect: CGRect, at position: BoardPosition) throws -> CGImage {
let squareSize = rect.width / 8
let x = rect.minX + CGFloat(position.file) * squareSize
let y = rect.minY + CGFloat(position.rank) * squareSize
let squareRect = CGRect(x: x, y: y, width: squareSize, height: squareSize)
guard image.extent.contains(squareRect) else {
print("ERROR: Square bounds outside image extent at \(position.file),\(position.rank)")
throw BoardDetectionError.squareExtractionFailed
}
let croppedImage = image.cropped(to: squareRect)
guard let cgImage = context.createCGImage(croppedImage, from: croppedImage.extent) else {
print("ERROR: Failed to create square image at \(position.file),\(position.rank)")
throw BoardDetectionError.squareExtractionFailed
}
return cgImage
}
/// Analyze the board and return the chess position
func analyzeBoard(in image: CIImage) async throws -> ChessPosition {
print("=== ANALYZING BOARD ===")
guard let boardRect = detectBoard(in: image) else {
print("ERROR: Board detection failed")
throw BoardDetectionError.boardNotFound
}
let aspectRatio = boardRect.width / boardRect.height
guard boardRect.width > 0, boardRect.height > 0,
abs(1 - aspectRatio) < 0.1 else {
print("ERROR: Invalid board dimensions")
throw BoardDetectionError.invalidBoardDimensions
}
// Reset piece counts before starting new scan
pieceRecognizer.resetCounts()
var squares = [BoardPosition: SquareClassification]()
var pieceCount = 0
// Process each square
for rank in 0..<8 {
for file in 0..<8 {
let boardPosition = BoardPosition(file: file, rank: rank)
let squareImage = try extractSquare(from: image, in: boardRect, at: boardPosition)
// Recognize square content
let classification = try await pieceRecognizer.recognizeSquare(from: squareImage, row: rank, col: file)
squares[boardPosition] = classification
// Count pieces for logging
if !classification.isEmpty {
pieceCount += 1
}
}
}
// Generate FEN string and create position
let fenGenerator = FenGenerator()
let fen = fenGenerator.generateFen(from: squares)
guard let position = ChessPosition(fen: fen) else {
print("ERROR: Failed to create position from FEN")
throw BoardDetectionError.imageProcessingFailed
}
print("\nPieces recognized: \(pieceCount)")
print("\nGenerated FEN: \(fen)")
guard position.isValid else {
print("\nERROR: Invalid chess position")
throw BoardDetectionError.imageProcessingFailed
}
print("=== BOARD ANALYSIS COMPLETE ===")
return position
}
private func validateDetectedRect(_ rect: CGRect, in image: CIImage) -> Bool {
let imageSize = image.extent.size
guard image.extent.contains(rect) else {
print("ERROR: Detected rectangle outside image bounds")
return false
}
let aspectRatio = rect.width / rect.height
guard aspectRatio >= 0.9 && aspectRatio <= 1.1 else {
print("ERROR: Invalid board aspect ratio: \(aspectRatio)")
return false
}
let minDimension = min(imageSize.width, imageSize.height)
let boardSize = max(rect.width, rect.height)
let sizeRatio = boardSize / minDimension
guard sizeRatio >= 0.4 else {
print("ERROR: Board too small relative to image")
return false
}
return true
}
}

Binary file not shown.

View file

@ -4,7 +4,21 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.device.screen-capture</key>
<true/>
<key>com.apple.security.device.accessibility</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.ml.coreml</key>
<true/>
<key>com.apple.security.files.user-selected.executable</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

View file

@ -6,12 +6,55 @@
//
import SwiftUI
import ScreenCaptureKit
@main
struct ChessPrismApp: App {
@StateObject private var permissionManager = PermissionManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(permissionManager)
.onAppear {
permissionManager.requestPermissions()
}
.alert(isPresented: $permissionManager.showPermissionAlert) {
Alert(
title: Text("Permissions Required"),
message: Text(permissionManager.permissionAlertMessage),
primaryButton: .default(Text("Open Settings")) {
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy")!)
},
secondaryButton: .cancel()
)
}
}
}
}
class PermissionManager: ObservableObject {
@Published var showPermissionAlert = false
@Published var permissionAlertMessage = ""
func requestPermissions() {
requestScreenRecordingPermission()
requestAccessibilityPermission()
}
private func requestScreenRecordingPermission() {
let status = CGPreflightScreenCaptureAccess()
if !status {
permissionAlertMessage += "Screen recording permission is required to analyze chess positions.\n"
showPermissionAlert = true
}
}
private func requestAccessibilityPermission() {
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
if !AXIsProcessTrustedWithOptions(options) {
permissionAlertMessage += "Accessibility permission is required to interact with other applications.\n"
showPermissionAlert = true
}
}
}

View file

@ -1,24 +1,335 @@
//
// ContentView.swift
// ChessPrism
//
// Created by Chris Haulmark on 1/6/25.
//
import SwiftUI
import AppKit
struct ContentView: View {
// MARK: - Chess Position View Components
struct ChessPieceView: View {
let piece: ChessPiece
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
Text(pieceSymbol)
.font(.system(size: 24, weight: .bold))
.foregroundColor(piece.color == .white ? .white : .black)
}
private var pieceSymbol: String {
switch piece.type {
case .pawn: return ""
case .knight: return ""
case .bishop: return ""
case .rook: return ""
case .queen: return ""
case .king: return ""
}
}
}
struct ChessboardSquareView: View {
let isLightSquare: Bool
let piece: ChessPiece?
var body: some View {
ZStack {
Rectangle()
.fill(isLightSquare ? Color.white : Color.gray)
.frame(width: 40, height: 40)
if let piece = piece {
ChessPieceView(piece: piece)
}
}
}
}
struct ChessboardView: View {
let position: ChessPosition?
var body: some View {
VStack(spacing: 0) {
ForEach((0..<8).reversed(), id: \.self) { rank in
HStack(spacing: 0) {
ForEach(0..<8, id: \.self) { file in
let boardPosition = BoardPosition(file: file, rank: rank)
ChessboardSquareView(
isLightSquare: (rank + file) % 2 == 0,
piece: position?[boardPosition]
)
}
}
}
}
.border(Color.black, width: 1)
}
}
struct AnalysisStatusView: View {
let isAnalyzing: Bool
let confidence: Double
var body: some View {
VStack(spacing: 8) {
if isAnalyzing {
ProgressView("Analyzing position...")
} else {
Text("Recognition Confidence: \(Int(confidence * 100))%")
.foregroundColor(confidence > 0.7 ? .green : .orange)
}
}
.padding()
}
}
#Preview {
ContentView()
class CustomNSView: NSView {
private static let invisibleCursor: NSCursor = {
let image = NSImage(size: NSSize(width: 1, height: 1))
image.lockFocus()
NSColor.clear.set()
NSRect(x: 0, y: 0, width: 1, height: 1).fill()
image.unlockFocus()
return NSCursor(image: image, hotSpot: .zero)
}()
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
private func setupView() {
wantsLayer = true
layer?.backgroundColor = NSColor.clear.cgColor
}
override func resetCursorRects() {
super.resetCursorRects()
addCursorRect(bounds, cursor: Self.invisibleCursor)
}
}
struct HiddenCursorView: NSViewRepresentable {
func makeNSView(context: Context) -> CustomNSView {
let view = CustomNSView()
view.wantsLayer = true
return view
}
func updateNSView(_ nsView: CustomNSView, context: Context) {
nsView.setFrameSize(nsView.frame.size)
}
}
struct CaptureStatusButton: View {
let isCapturing: Bool
let isBoardDetected: Bool
var body: some View {
HStack {
Circle()
.fill(statusColor)
.frame(width: 12, height: 12)
Text(statusText)
.foregroundColor(statusColor)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 8)
.stroke(statusColor, lineWidth: 2)
)
}
private var statusColor: Color {
if isCapturing && isBoardDetected {
return .green
} else if isCapturing {
return .yellow
} else {
return .gray
}
}
private var statusText: String {
if isCapturing && isBoardDetected {
return "Capturing"
} else if isCapturing {
return "Waiting for board"
} else {
return "Not capturing"
}
}
}
struct CaptureView: View {
@ObservedObject var viewModel: ScreenCaptureViewModel
var body: some View {
HStack {
VStack {
Text("Full Capture")
if let image = viewModel.capturedImage {
Image(nsImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 400)
} else {
Text("No capture available")
.foregroundColor(.gray)
}
}
Divider()
VStack {
Text("Chessboard Preview")
if let image = viewModel.croppedBoardImage {
Image(nsImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 400)
.overlay(
HiddenCursorView()
.allowsHitTesting(true)
)
} else {
Text("No board detected")
.foregroundColor(.gray)
}
if viewModel.snapshotTaken {
Divider()
.padding(.vertical)
Text("Latest Snapshot")
if let snapshot = viewModel.latestSnapshot {
Image(nsImage: snapshot)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 400)
.padding()
.background(Color.black.opacity(0.1))
.cornerRadius(8)
}
}
}
}
.padding()
}
}
struct AnalysisView: View {
@ObservedObject var viewModel: ScreenCaptureViewModel
var body: some View {
VStack {
if let position = viewModel.currentPosition {
HStack(alignment: .top, spacing: 20) {
VStack(alignment: .leading) {
Text("Current Position")
.font(.headline)
ChessboardView(position: position)
.padding()
.background(Color.white)
.cornerRadius(8)
.shadow(radius: 2)
AnalysisStatusView(
isAnalyzing: viewModel.isAnalyzing,
confidence: viewModel.recognitionConfidence
)
}
VStack(alignment: .leading) {
Text("Position Details")
.font(.headline)
Text("FEN: \(position.fen)")
.font(.system(.body, design: .monospaced))
.padding(.vertical)
if viewModel.captureError == .recognitionFailed {
Text("Recognition Error")
.foregroundColor(.red)
.padding()
}
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
} else {
Text("No position detected")
.foregroundColor(.gray)
}
Spacer()
}
.padding()
}
}
struct ContentView: View {
@StateObject private var viewModel = ScreenCaptureViewModel()
@State private var selectedTab = 0
var body: some View {
VStack {
// Status and Scan controls
HStack {
CaptureStatusButton(
isCapturing: viewModel.isCapturing,
isBoardDetected: viewModel.isBoardDetected
)
Button(action: {
Task {
await viewModel.takeSnapshot()
}
}) {
Text("Scan")
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
}
.buttonStyle(.borderedProminent)
.disabled(!viewModel.isCapturing || !viewModel.isBoardDetected)
}
.onAppear {
viewModel.startMonitoring()
}
.onDisappear {
viewModel.stopMonitoring()
}
// Error display
if let error = viewModel.captureError {
Text(error.localizedDescription)
.foregroundColor(.red)
.padding()
}
// Main content area with tabs
TabView(selection: $selectedTab) {
CaptureView(viewModel: viewModel)
.tabItem {
Label("Capture", systemImage: "camera")
}
.tag(0)
AnalysisView(viewModel: viewModel)
.tabItem {
Label("Analysis", systemImage: "magnifyingglass")
}
.tag(1)
}
}
.frame(minWidth: 1000, minHeight: 700)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>ChessPrism needs access to the camera for AR features</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>ChessPrism needs access to photos for saving game screenshots</string>
</dict>
</plist>

View file

@ -0,0 +1,289 @@
import Foundation
/// Represents a chess piece color
enum PieceColor: String {
case white
case black
}
/// Represents a chess piece type
enum PieceType: String {
case pawn
case knight
case bishop
case rook
case queen
case king
/// FEN notation for the piece
var fenSymbol: String {
switch self {
case .pawn: return "p"
case .knight: return "n"
case .bishop: return "b"
case .rook: return "r"
case .queen: return "q"
case .king: return "k"
}
}
}
/// Represents a chess piece with its type and color
struct ChessPiece: Equatable {
let type: PieceType
let color: PieceColor
/// FEN notation for the piece (uppercase for white, lowercase for black)
var fenSymbol: String {
let symbol = type.fenSymbol
return color == .white ? symbol.uppercased() : symbol
}
}
/// Represents a position on the chess board
struct BoardPosition: Equatable, Hashable {
let file: Int // 0-7 for a-h
let rank: Int // 0-7 for 1-8
/// Initialize from file and rank indices (0-7)
init(file: Int, rank: Int) {
guard file >= 0, file < 8, rank >= 0, rank < 8 else {
fatalError("Invalid board position: file \(file), rank \(rank)")
}
self.file = file
self.rank = rank
}
/// Hash function implementation for Hashable conformance
func hash(into hasher: inout Hasher) {
hasher.combine(file)
hasher.combine(rank)
}
/// Initialize from algebraic notation (e.g., "e4")
init?(algebraic: String) {
guard algebraic.count == 2,
let file = algebraic.first?.asciiValue,
let rank = algebraic.last?.wholeNumberValue,
file >= UInt8(ascii: "a"), file <= UInt8(ascii: "h"),
rank >= 1, rank <= 8 else {
return nil
}
self.file = Int(file - UInt8(ascii: "a"))
self.rank = rank - 1
}
/// Convert to algebraic notation
var algebraic: String {
let fileChar = Character(UnicodeScalar(UInt8(ascii: "a") + UInt8(file)))
return "\(fileChar)\(rank + 1)"
}
/// Validate if the position is within bounds
var isValid: Bool {
file >= 0 && file < 8 && rank >= 0 && rank < 8
}
}
/// Represents a complete chess position
struct ChessPosition {
/// 8x8 grid representing the board state, nil means empty square
private var board: [[ChessPiece?]]
/// Initialize an empty board
init() {
board = Array(repeating: Array(repeating: nil, count: 8), count: 8)
}
/// Initialize from FEN string
init?(fen: String) {
self.init()
let components = fen.components(separatedBy: " ")
guard components.count >= 1 else { return nil }
let ranks = components[0].components(separatedBy: "/")
guard ranks.count == 8 else { return nil }
for (rankIndex, rank) in ranks.enumerated() {
var fileIndex = 0
for char in rank {
if let number = Int(String(char)) {
fileIndex += number
} else {
guard fileIndex < 8 else { return nil }
let color: PieceColor = char.isUppercase ? .white : .black
let lowerChar = char.lowercased()
guard let type = pieceTypeFromFen(String(lowerChar)) else { return nil }
board[7 - rankIndex][fileIndex] = ChessPiece(type: type, color: color)
fileIndex += 1
}
}
guard fileIndex == 8 else { return nil }
}
}
/// Convert position to FEN string (piece placement only)
var fen: String {
var result = ""
for rankIndex in (0...7).reversed() {
var emptyCount = 0
for fileIndex in 0...7 {
if let piece = board[rankIndex][fileIndex] {
if emptyCount > 0 {
result += String(emptyCount)
emptyCount = 0
}
result += piece.fenSymbol
} else {
emptyCount += 1
}
}
if emptyCount > 0 {
result += String(emptyCount)
}
if rankIndex > 0 {
result += "/"
}
}
return result
}
/// Get piece at position
subscript(position: BoardPosition) -> ChessPiece? {
get {
guard position.isValid else { return nil }
return board[position.rank][position.file]
}
set {
guard position.isValid else { return }
board[position.rank][position.file] = newValue
}
}
/// Get piece at algebraic position
subscript(algebraic: String) -> ChessPiece? {
get {
guard let position = BoardPosition(algebraic: algebraic) else { return nil }
return self[position]
}
set {
guard let position = BoardPosition(algebraic: algebraic) else { return }
self[position] = newValue
}
}
/// Validate if the position is legal
var isValid: Bool {
print("\n=== VALIDATING CHESS POSITION ===")
var whitePieces = [PieceType: Int]()
var blackPieces = [PieceType: Int]()
// Count all pieces
for rank in 0...7 {
for file in 0...7 {
if let piece = board[rank][file] {
if piece.color == .white {
whitePieces[piece.type, default: 0] += 1
} else {
blackPieces[piece.type, default: 0] += 1
}
// Check pawns on invalid ranks
if piece.type == .pawn && (rank == 0 || rank == 7) {
print("ERROR: Pawn found on first/last rank")
return false
}
}
}
}
// Print piece counts
print("White pieces:")
for (type, count) in whitePieces {
print("- \(type): \(count)")
}
print("Black pieces:")
for (type, count) in blackPieces {
print("- \(type): \(count)")
}
// Validate piece counts
let whiteTotal = whitePieces.values.reduce(0, +)
let blackTotal = blackPieces.values.reduce(0, +)
if whiteTotal > 16 {
print("ERROR: Too many white pieces (\(whiteTotal))")
return false
}
if blackTotal > 16 {
print("ERROR: Too many black pieces (\(blackTotal))")
return false
}
// Validate kings
if whitePieces[.king] ?? 0 != 1 {
print("ERROR: Invalid number of white kings (\(whitePieces[.king] ?? 0))")
return false
}
if blackPieces[.king] ?? 0 != 1 {
print("ERROR: Invalid number of black kings (\(blackPieces[.king] ?? 0))")
return false
}
// Validate pawns
if whitePieces[.pawn] ?? 0 > 8 {
print("ERROR: Too many white pawns (\(whitePieces[.pawn] ?? 0))")
return false
}
if blackPieces[.pawn] ?? 0 > 8 {
print("ERROR: Too many black pawns (\(blackPieces[.pawn] ?? 0))")
return false
}
// Validate other pieces
for pieceType in [PieceType.queen, .rook, .bishop, .knight] {
if whitePieces[pieceType] ?? 0 > 2 {
print("ERROR: Too many white \(pieceType)s (\(whitePieces[pieceType] ?? 0))")
return false
}
if blackPieces[pieceType] ?? 0 > 2 {
print("ERROR: Too many black \(pieceType)s (\(blackPieces[pieceType] ?? 0))")
return false
}
}
print("Position validation successful")
return true
}
/// Helper function to convert FEN piece symbol to PieceType
private func pieceTypeFromFen(_ symbol: String) -> PieceType? {
switch symbol {
case "p": return .pawn
case "n": return .knight
case "b": return .bishop
case "r": return .rook
case "q": return .queen
case "k": return .king
default: return nil
}
}
/// Initialize with the standard starting position
static var startingPosition: ChessPosition {
// swiftlint:disable:next force_unwrapping
ChessPosition(fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR")!
}
}

View file

@ -0,0 +1,54 @@
import Foundation
struct SquareClassification {
let pieceType: PieceType?
let pieceColor: PieceColor?
let isHighlighted: Bool
var isEmpty: Bool {
return pieceType == nil
}
static func empty(highlighted: Bool = false) -> SquareClassification {
return SquareClassification(pieceType: nil, pieceColor: nil, isHighlighted: highlighted)
}
init(pieceType: PieceType?, pieceColor: PieceColor?, isHighlighted: Bool = false) {
self.pieceType = pieceType
self.pieceColor = pieceColor
self.isHighlighted = isHighlighted
}
init?(label: String) {
// Match exact categories from trained model
switch label {
case "white_pawn":
self.init(pieceType: .pawn, pieceColor: .white)
case "white_knight":
self.init(pieceType: .knight, pieceColor: .white)
case "white_bishop":
self.init(pieceType: .bishop, pieceColor: .white)
case "white_rook":
self.init(pieceType: .rook, pieceColor: .white)
case "white_queen":
self.init(pieceType: .queen, pieceColor: .white)
case "white_king":
self.init(pieceType: .king, pieceColor: .white)
case "black_pawn":
self.init(pieceType: .pawn, pieceColor: .black)
case "black_knight":
self.init(pieceType: .knight, pieceColor: .black)
case "black_bishop":
self.init(pieceType: .bishop, pieceColor: .black)
case "black_rook":
self.init(pieceType: .rook, pieceColor: .black)
case "black_queen":
self.init(pieceType: .queen, pieceColor: .black)
case "black_king":
self.init(pieceType: .king, pieceColor: .black)
default:
// Any unrecognized label returns an empty square
self.init(pieceType: nil, pieceColor: nil, isHighlighted: false)
}
}
}

View file

@ -0,0 +1,72 @@
import Foundation
/// A class responsible for generating FEN strings from square classifications
final class FenGenerator {
/// Generate a FEN string from a set of square classifications
/// - Parameter squares: Dictionary mapping board positions to their classifications
/// - Returns: FEN string representing the position
func generateFen(from squares: [BoardPosition: SquareClassification]) -> String {
var fen = ""
var emptyCount = 0
// Process each rank from top to bottom (8 to 1)
for rank in (0...7).reversed() {
// Process each file from left to right (a to h)
for file in 0...7 {
let position = BoardPosition(file: file, rank: rank)
guard let square = squares[position] else {
// If square is missing, treat as empty
emptyCount += 1
continue
}
if square.isEmpty {
// Count consecutive empty squares
emptyCount += 1
} else if let color = square.pieceColor,
let type = square.pieceType {
// If we had empty squares before this piece, add the count
if emptyCount > 0 {
fen += String(emptyCount)
emptyCount = 0
}
// Add the piece symbol
let symbol = pieceSymbol(color: color, type: type)
fen += symbol
}
}
// Add any remaining empty squares at end of rank
if emptyCount > 0 {
fen += String(emptyCount)
emptyCount = 0
}
// Add rank separator (except for last rank)
if rank > 0 {
fen += "/"
}
}
return fen
}
/// Get the FEN symbol for a piece
/// - Parameters:
/// - color: Color of the piece
/// - type: Type of the piece
/// - Returns: FEN symbol (uppercase for white, lowercase for black)
private func pieceSymbol(color: PieceColor, type: PieceType) -> String {
let symbol: String
switch type {
case .king: symbol = "K"
case .queen: symbol = "Q"
case .rook: symbol = "R"
case .bishop: symbol = "B"
case .knight: symbol = "N"
case .pawn: symbol = "P"
}
return color == .white ? symbol : symbol.lowercased()
}
}

View file

@ -0,0 +1,49 @@
import Foundation
/// Represents a detected chess move
struct DetectedMove {
/// The source square where the piece moved from
let from: BoardPosition
/// The target square where the piece moved to
let to: BoardPosition
/// The piece that was moved
let piece: ChessPiece
}
/// A class responsible for detecting moves from highlighted squares
final class MoveDetector {
/// Detect a move from a set of square classifications
/// - Parameter squares: Dictionary mapping board positions to their classifications
/// - Returns: Detected move if one is found, nil otherwise
func detectMove(from squares: [BoardPosition: SquareClassification]) -> DetectedMove? {
var sourceSquare: BoardPosition?
var targetSquare: BoardPosition?
var movedPiece: ChessPiece?
// Find highlighted squares
for (position, classification) in squares {
guard classification.isHighlighted else { continue }
if classification.isEmpty {
// Empty highlighted square is the source
sourceSquare = position
} else if let color = classification.pieceColor,
let type = classification.pieceType {
// Piece on highlighted square is the target
targetSquare = position
movedPiece = ChessPiece(type: type, color: color)
}
}
// Return move if we found both squares and the piece
if let from = sourceSquare,
let to = targetSquare,
let piece = movedPiece {
return DetectedMove(from: from, to: to, piece: piece)
}
return nil
}
}

View file

@ -0,0 +1,160 @@
import Foundation
import Vision
import CoreML
import CoreImage
/// Errors that can occur during piece recognition
enum PieceRecognitionError: Error {
case invalidImageDimensions
case modelLoadError
case recognitionFailed(String)
case lowConfidence
case invalidInput
}
/// A class responsible for recognizing chess pieces from images
final class PieceRecognizer {
// MARK: - Properties
/// Vision model for piece classification
private let vnModel: VNCoreMLModel
/// Shared CIContext for image processing
private static let ciContext = CIContext()
/// Latest classification results
private var classificationResults: [VNClassificationObservation]?
/// Confidence threshold
private let confidenceThreshold: Float = 0.75
// MARK: - Initialization
init() throws {
print("=== INITIALIZING PIECE RECOGNIZER ===")
let bundle = Bundle.main
// Load model
guard let modelURL = bundle.url(forResource: "ChessPieceClassifier", withExtension: "mlmodelc") else {
print("ERROR: Model not found in bundle at \(bundle.bundlePath)")
throw PieceRecognitionError.modelLoadError
}
do {
let config = MLModelConfiguration()
config.computeUnits = .all
let model = try MLModel(contentsOf: modelURL, configuration: config)
self.vnModel = try VNCoreMLModel(for: model)
print("Model loaded successfully from: \(modelURL.path)")
} catch {
print("ERROR: Failed to load model - \(error)")
throw PieceRecognitionError.modelLoadError
}
}
// MARK: - Recognition Methods
func recognizeSquare(from image: CGImage, row: Int = 0, col: Int = 0) async throws -> SquareClassification {
// Validate image dimensions
guard image.width > 0, image.height > 0,
abs(1 - Float(image.width) / Float(image.height)) < 0.1 else {
print("ERROR: Invalid square dimensions \(image.width)x\(image.height)")
throw PieceRecognitionError.invalidImageDimensions
}
let handler = VNImageRequestHandler(cgImage: image)
var classificationError: Error?
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
let request = VNCoreMLRequest(model: vnModel) { request, error in
if let error = error {
classificationError = error
continuation.resume(throwing: error)
return
}
self.classificationResults = request.results as? [VNClassificationObservation]
continuation.resume()
}
request.imageCropAndScaleOption = .centerCrop
do {
try handler.perform([request])
} catch {
continuation.resume(throwing: error)
}
}
if let error = classificationError {
print("ERROR: Classification failed - \(error)")
throw PieceRecognitionError.recognitionFailed(error.localizedDescription)
}
guard let results = self.classificationResults,
let topResult = results.first else {
print("ERROR: No classification results")
throw PieceRecognitionError.recognitionFailed("No results")
}
// Print results
print("\nClassification results:")
for result in results.prefix(3) {
print("- \(result.identifier): \(result.confidence)")
}
// Check confidence threshold
if topResult.confidence < confidenceThreshold {
print("Low confidence (\(topResult.confidence)) for \(topResult.identifier)")
return .empty()
}
// Try to create a SquareClassification from the label
if let classification = SquareClassification(label: topResult.identifier) {
print("Classified as \(topResult.identifier) with confidence \(topResult.confidence)")
return classification
}
// Return empty square if classification fails
print("Classification failed for \(topResult.identifier) (\(topResult.confidence))")
return .empty()
}
/// Preprocess an image for recognition
/// - Parameter image: Input CGImage
/// - Returns: Preprocessed CGImage
func preprocessImage(_ image: CGImage) throws -> CGImage {
let ciImage = CIImage(cgImage: image)
// Apply preprocessing filters
// First pass: Enhance contrast and edges
var processed = ciImage
.applyingFilter("CIColorControls", parameters: [
"inputContrast": 1.3,
"inputBrightness": 0.0,
"inputSaturation": 1.0
])
.applyingFilter("CIUnsharpMask", parameters: [
"inputRadius": 2.0,
"inputIntensity": 0.8
])
// Second pass: Reduce noise and enhance details
processed = processed
.applyingFilter("CINoiseReduction", parameters: [
"inputNoiseLevel": 0.2,
"inputSharpness": 0.6
])
.applyingFilter("CIColorControls", parameters: [
"inputContrast": 1.2,
"inputBrightness": 0.0,
"inputSaturation": 1.0
])
// Convert back to CGImage
guard let outputImage = Self.ciContext.createCGImage(processed, from: processed.extent) else {
throw PieceRecognitionError.invalidInput
}
return outputImage
}
}

View file

@ -0,0 +1,77 @@
import Foundation
import AppKit
import CoreGraphics
import CoreMedia
import ScreenCaptureKit
import AVFoundation
class ScreenCapture: NSObject, SCStreamOutput {
func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
DispatchQueue.main.async {
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
let rep = NSCIImageRep(ciImage: ciImage)
let nsImage = NSImage(size: rep.size)
nsImage.addRepresentation(rep)
self.lastCapturedImage = nsImage
}
}
private var activeStream: SCStream?
private var lastCapturedImage: NSImage?
private var excludeCursor: Bool = false
func startCapture(excludeCursor: Bool = false) async throws {
// Always stop any existing capture before starting a new one
if activeStream != nil {
try await stopCapture()
}
self.excludeCursor = excludeCursor
guard #available(macOS 12.3, *) else { return }
let content = try await SCShareableContent.current
// Get the window from the content
guard let window = content.windows.first(where: { window in
let bundleID = window.owningApplication?.bundleIdentifier ?? ""
let isChessApp = bundleID == "com.chess.iphone"
let hasValidSize = window.frame.width > 100 && window.frame.height > 100
return isChessApp && window.isOnScreen && hasValidSize
}) else {
throw NSError(domain: "ChessPrism", code: 1, userInfo: [NSLocalizedDescriptionKey: "Chess window not found"])
}
let filter = SCContentFilter(desktopIndependentWindow: window)
let config = SCStreamConfiguration()
config.width = Int(window.frame.width)
config.height = Int(window.frame.height)
config.minimumFrameInterval = CMTime(value: 1, timescale: 30) // 30 FPS
config.queueDepth = 5 // Buffer up to 5 frames
config.showsCursor = !excludeCursor // Set cursor visibility
let stream = SCStream(filter: filter, configuration: config, delegate: nil)
try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: DispatchQueue.global(qos: .userInitiated))
// Store the stream before starting it
activeStream = stream
try await stream.startCapture()
}
func stopCapture() async throws {
if let stream = activeStream {
try await stream.stopCapture()
activeStream = nil
lastCapturedImage = nil
}
}
func getCurrentImage() -> NSImage? {
return lastCapturedImage
}
}

View file

@ -0,0 +1,294 @@
import Foundation
import AppKit
import CoreImage
import Combine
@MainActor
class ScreenCaptureViewModel: ObservableObject {
@Published var capturedImage: NSImage?
@Published var croppedBoardImage: NSImage?
@Published var captureError: CaptureError?
@Published var isCapturing = false
@Published var isBoardDetected = false
@Published var isAutoCapturing = true // Default to auto-capture mode
@Published var snapshotTaken = false // Track if snapshot was taken
@Published var latestSnapshot: NSImage? // Make snapshot accessible to view
@Published var currentPosition: ChessPosition? // Current chess position
@Published var recognitionConfidence: Double = 0.0 // Recognition confidence
@Published var isAnalyzing = false // Track analysis state
private let captureManager = ScreenCapture() // For actual capture
private let monitorManager = ScreenCapture() // For monitoring
private let pieceRecognizer: PieceRecognizer
private let boardDetector: BoardDetector
private var captureTask: Task<Void, Never>?
private var monitorTask: Task<Void, Never>?
// Share CIContext to avoid creating too many Metal command queues
private static let shared = CIContext()
private var context: CIContext { ScreenCaptureViewModel.shared }
private var detectedBoardRect: CGRect?
private var isMonitoring = false
enum CaptureError: LocalizedError {
case chessWindowNotFound
case boardDetectionFailed
case noBoardDetected
case snapshotFailed
case recognitionFailed
case invalidPosition
var errorDescription: String? {
switch self {
case .chessWindowNotFound:
return "Chess window not found"
case .boardDetectionFailed:
return "Failed to detect chess board"
case .noBoardDetected:
return "No chess board detected"
case .snapshotFailed:
return "Failed to take snapshot"
case .recognitionFailed:
return "Failed to recognize pieces"
case .invalidPosition:
return "Invalid chess position detected"
}
}
}
init() {
// Initialize piece recognizer
do {
pieceRecognizer = try PieceRecognizer()
boardDetector = BoardDetector(pieceRecognizer: pieceRecognizer)
} catch {
fatalError("Failed to initialize piece recognizer: \(error)")
}
}
func takeSnapshot() async {
print("=== SCAN BUTTON PRESSED ===")
do {
try await captureManager.stopCapture()
try await captureManager.startCapture(excludeCursor: true)
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
if let currentImage = captureManager.getCurrentImage() {
print("Captured image: \(currentImage.size)")
try await processImage(currentImage, analyzePosition: true)
if let boardImage = croppedBoardImage {
latestSnapshot = boardImage
snapshotTaken = true
if let position = currentPosition {
print("Successfully detected position")
} else {
print("ERROR: Failed to detect position")
captureError = .recognitionFailed
}
} else {
print("ERROR: Failed to crop board image")
captureError = .snapshotFailed
currentPosition = nil
}
} else {
print("ERROR: Failed to capture image")
captureError = .snapshotFailed
currentPosition = nil
}
try await captureManager.startCapture(excludeCursor: false)
} catch {
print("ERROR: Snapshot failed - \(error)")
captureError = .snapshotFailed
currentPosition = nil
try? await captureManager.startCapture(excludeCursor: false)
}
}
func startMonitoring() {
guard !isMonitoring else { return }
isMonitoring = true
monitorTask = Task {
monitorLoop: while !Task.isCancelled {
do {
// Start monitoring capture
try await monitorManager.startCapture()
while !Task.isCancelled {
if let image = monitorManager.getCurrentImage(),
let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) {
// Check for board
let ciImage = CIImage(cgImage: cgImage)
if boardDetector.detectBoard(in: ciImage) != nil {
// Board detected, start capture if not already capturing
if !isCapturing {
await startCapture()
}
}
}
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
}
} catch {
print("Monitor error: \(error)")
// If there's an error, wait briefly and try again
if !Task.isCancelled {
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
continue monitorLoop
}
}
}
isMonitoring = false
try? await monitorManager.stopCapture()
}
}
func stopMonitoring() {
monitorTask?.cancel()
monitorTask = nil
isMonitoring = false
stopCapture()
Task {
try? await monitorManager.stopCapture()
}
}
func startCapture() async {
guard !isCapturing else { return }
isCapturing = true
captureError = nil
snapshotTaken = false
latestSnapshot = nil
currentPosition = nil
recognitionConfidence = 0.0
do {
try await captureManager.startCapture()
} catch {
handleCaptureError(error)
isCapturing = false
return
}
captureTask = Task {
captureLoop: while !Task.isCancelled {
do {
if let image = captureManager.getCurrentImage() {
try await processImage(image, analyzePosition: false)
}
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
} catch is CancellationError {
break captureLoop
} catch {
// Just update the error state but continue capturing
if !Task.isCancelled {
handleCaptureError(error)
if isAutoCapturing && error as? CaptureError == .noBoardDetected {
// In auto-capture mode, stop capture but keep monitoring
stopCapture()
break captureLoop
}
}
}
}
if isCapturing {
isCapturing = false
}
}
}
func stopCapture() {
captureTask?.cancel()
captureTask = nil
isCapturing = false
captureError = nil
isBoardDetected = false
snapshotTaken = false
latestSnapshot = nil
currentPosition = nil
recognitionConfidence = 0.0
// Clean up capture session
Task {
try? await captureManager.stopCapture()
}
}
private func processImage(_ image: NSImage, analyzePosition: Bool = false) async throws {
guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
print("ERROR: Failed to convert image for processing")
throw CaptureError.boardDetectionFailed
}
let ciImage = CIImage(cgImage: cgImage)
if let boardRect = boardDetector.detectBoard(in: ciImage) {
isBoardDetected = true
self.detectedBoardRect = boardRect
let croppedImage = ciImage.cropped(to: boardRect)
updateImages(ciImage: ciImage, croppedImage: croppedImage)
if analyzePosition && !isAnalyzing {
isAnalyzing = true
do {
print("Analyzing board position...")
let position = try await boardDetector.analyzeBoard(in: ciImage)
currentPosition = position
recognitionConfidence = 1.0
isAnalyzing = false
} catch {
print("ERROR: Position analysis failed - \(error)")
isAnalyzing = false
currentPosition = nil
recognitionConfidence = 0.0
if error is BoardDetectionError {
throw CaptureError.recognitionFailed
} else {
throw error
}
}
}
} else {
isBoardDetected = false
self.detectedBoardRect = nil
currentPosition = nil
recognitionConfidence = 0.0
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
self.capturedImage = NSImage(cgImage: cgImage, size: .zero)
}
self.croppedBoardImage = nil
if isAutoCapturing {
throw CaptureError.noBoardDetected
}
}
}
private func updateImages(ciImage: CIImage, croppedImage: CIImage) {
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
let capturedNSImage = NSImage(cgImage: cgImage, size: .zero)
self.capturedImage = capturedNSImage
}
if let cgCroppedImage = context.createCGImage(croppedImage, from: croppedImage.extent) {
let croppedNSImage = NSImage(cgImage: cgCroppedImage, size: .zero)
self.croppedBoardImage = croppedNSImage
}
}
private func handleCaptureError(_ error: Error) {
if let captureError = error as? CaptureError {
self.captureError = captureError
} else {
self.captureError = .boardDetectionFailed
}
}
}

46
Package.swift Normal file
View file

@ -0,0 +1,46 @@
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "ChessPrism",
platforms: [
.iOS(.v16),
.macOS(.v13),
.visionOS(.v1)
],
products: [
.library(
name: "ChessPrism",
targets: ["ChessPrism"]
)
],
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.0.0"),
.package(url: "https://github.com/nalexn/ViewInspector", from: "0.9.7"),
.package(url: "https://github.com/ReactiveX/RxSwift", from: "6.6.0"),
.package(url: "https://github.com/SwiftGen/SwiftGenPlugin", from: "6.6.0")
],
targets: [
.target(
name: "ChessPrism",
dependencies: [
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "RxSwift", package: "RxSwift")
],
plugins: [
.plugin(name: "SwiftGenPlugin", package: "SwiftGenPlugin")
]
),
.testTarget(
name: "ChessPrismTests",
dependencies: [
"ChessPrism",
.product(name: "ViewInspector", package: "ViewInspector")
]
),
.testTarget(
name: "ChessPrismUITests",
dependencies: ["ChessPrism"]
)
]
)

37629
cline_docs/Info.txt Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,83 +1,34 @@
# Active Context
## Current Implementation Phase
Week 1: Project Setup (In Progress)
## Current Task
- Fixed model integration issues
- Updated code to match model categories
- Simplified classification system
## What we're working on now
1. Xcode project configuration
- Setting up SwiftUI project structure
- Configuring project settings for MacOS Silicon
- Adding basic app lifecycle handlers
2. Development environment setup
- Installing required dependencies (Metal, Vision)
- Configuring build settings
- Setting up version control
3. Project architecture
- Creating core module structure
- Implementing basic MVVM pattern
- Setting up dependency injection
## Recent Changes
1. SquareClassification.swift:
- Exact category mapping:
```swift
white_pawn, white_knight, white_bishop, white_rook, white_queen, white_king,
black_pawn, black_knight, black_bishop, black_rook, black_queen, black_king
```
- Default to empty square for unrecognized labels
- Removed background variations from labels
## Recent changes
- Created detailed implementation plan
- Set up development environment
- Installed Xcode and necessary tools
- Created initial Xcode project with SwiftUI template
- Configured Git repository
- Added basic project structure
- Configured required frameworks (Metal, Vision)
- Set up initial build configurations
2. PieceRecognizer.swift:
- Using VNCoreMLModel consistently
- Simplified error handling
- Removed unnecessary piece counting
- Using Vision framework for classification
## Next steps
1. Complete project setup
- Implement permission handlers
- Add screen recording permission request
- Implement accessibility permissions
- Add privacy descriptions in Info.plist
- Finalize project architecture
- Complete MVVM implementation
- Finalize dependency injection setup
- Set up core module interfaces
2. Begin screenshot module implementation
- Implement screen recording permissions flow
- Set up screenshot capture service
- Configure keyboard shortcut system
## Next Steps
1. Test model integration:
- Verify model loads correctly
- Check classification accuracy
- Monitor confidence levels
## Completed Milestones
- Initial project setup complete
- Development environment configured
- Core frameworks integrated
- Basic project structure established
## Technical Details
### Current Implementation
- Using Swift 5.9
- SwiftUI 4.0 for UI
- Metal 3.0 for rendering
- Vision 2.0 for image analysis
- CoreML 5.0 for machine learning
### Current Architecture
- MVVM pattern implementation
- Modular architecture
- Dependency injection
- Async/await concurrency model
### Current Configuration
- MacOS Ventura 13.0+
- Xcode 15.0+
- Git 2.40+
- CoreML Tools 5.0+
## Pending Tasks
1. Complete permission handling implementation
2. Finalize project architecture
3. Begin screenshot module development
4. Set up initial testing infrastructure
5. Configure CI/CD pipeline
## Important Notes
- Following detailed implementation plan
- Maintaining clean architecture principles
- Implementing modern Swift patterns
- Ensuring security and privacy compliance
- Maintaining documentation standards
## Current Issues
Fixed:
- Model pipeline error
- Category mismatches
- Classification handling

12
cline_docs/chessboard.txt Normal file
View file

@ -0,0 +1,12 @@
Current position:
8 bR .. .. bQ bK bB .. bR
7 .. bP .. bB .. bP bP ..
6 .. wQ .. bP bP .. .. ..
5 bP .. .. .. .. .. .. bP
4 .. .. .. wB .. bN .. ..
3 .. wB wP bN .. .. .. ..
2 wP wP .. .. .. wP wP wP
1 wR wN .. .. wK .. wN wR
a b c d e f g h

106
cline_docs/problem.md Normal file
View file

@ -0,0 +1,106 @@
# Chess Board Detection Problem
## Issue Description
The chess board detection system is currently only displaying the bottom 2-3 rows of the chess board in the "Chessboard" preview window, while the "Full Capture" window shows the complete chess board. The width of the detected board is correct, but the height is truncated.
## Visual Evidence
- Full Capture: Shows complete chess board with all pieces
- Chessboard Preview: Shows only bottom portion (approximately 2.5 rows) of the board
- Width appears correct in both views
- Height is significantly truncated in Chessboard Preview
## Technical Analysis
### Detection Pipeline
1. Screen capture works correctly (evidenced by Full Capture view)
2. Initial board detection appears to find correct width
3. Problem occurs during either:
- Rectangle detection phase
- Coordinate transformation
- Image cropping stage
### Coordinate System Complexity
1. Multiple coordinate systems involved:
- Vision framework (bottom-left origin)
- NSImage/CGImage (bottom-left origin)
- SwiftUI (top-left origin)
2. Current transformations may be:
- Incorrectly mapping between coordinate spaces
- Losing vertical positioning information
- Miscalculating crop region
### Detection Parameters
1. Current approach:
- Using 0.3-0.5 aspect ratio for detection
- 0.4 minimum size requirement
- Single observation limit
2. These parameters may be:
- Causing partial detection of board
- Missing full vertical extent
- Incorrectly identifying board boundaries
## Code Areas to Investigate
### BoardDetector.swift
1. detectBoard() function:
- Board extraction logic
- Coordinate calculations
- Rectangle positioning
2. Rectangle Detection:
```swift
request.minimumAspectRatio = 0.3
request.maximumAspectRatio = 0.5
request.minimumSize = 0.4
```
- May need adjustment for full board capture
### ScreenCaptureViewModel.swift
1. Image cropping logic:
- Coordinate transformation
- Crop region calculation
- Final image generation
## Potential Solutions to Explore
1. Detection Approach
- Adjust aspect ratio constraints
- Modify detection parameters
- Consider alternative detection methods
2. Coordinate Handling
- Review all coordinate transformations
- Validate coordinate space conversions
- Ensure proper origin point handling
3. Image Processing
- Verify crop region calculations
- Review image scaling operations
- Validate final image generation
4. Alternative Approaches
- Use full-frame detection
- Implement grid-based detection
- Consider machine learning approach
## Impact
- Critical functionality issue
- Blocks accurate board analysis
- Affects user experience
- Prevents proper game state detection
## Priority
HIGH - This issue blocks core functionality of the chess analysis system.
## Next Steps
1. Verify coordinate system transformations
2. Test different aspect ratio parameters
3. Implement logging for detection boundaries
4. Add visualization of detected regions
5. Consider alternative detection approaches
## Additional Notes
- The issue appears consistent across different games
- Width detection is working correctly
- Height truncation is consistent (showing ~2.5 rows)
- Full board is visible in capture, suggesting screen capture is working

View file

@ -1,136 +1,89 @@
# Product Context
## Product Vision
To create an intuitive, real-time chess teaching assistant that enhances the learning experience for both instructors and students during live gameplay.
## Project Purpose
ChessPrism is a macOS application designed to:
1. Capture chess.com game windows
2. Detect and analyze chess positions in real-time
3. Generate FEN strings for position analysis
4. Track moves and game progress
## Core Value Proposition
1. Real-time visual feedback during live games
2. Integrated analysis and teaching tools
3. Seamless integration with chess.com
4. Performance-optimized for MacOS Silicon
## Core Features
1. Screen Capture
- Automatic chess.com window detection
- Real-time board monitoring
- Configurable capture settings
## Target Users
### User Personas
1. Chess Instructors
- Needs: Real-time analysis, teaching tools, visual aids
- Goals: Effective teaching, student engagement
- Pain Points: Complex setup, delayed feedback
2. Board Analysis
- Accurate piece detection
- 12 piece categories:
* 6 white pieces (pawn to king)
* 6 black pieces (pawn to king)
- Position validation
- FEN string generation
2. Advanced Players
- Needs: Move analysis, threat detection
- Goals: Improve teaching skills, analyze games
- Pain Points: Limited teaching tools
3. Chess Streamers
- Needs: Visual overlays, real-time analysis
- Goals: Engaging content, clear explanations
- Pain Points: Complex overlay systems
4. Self-Learners
- Needs: Position evaluation, move alternatives
- Goals: Game improvement, pattern recognition
- Pain Points: Lack of real-time feedback
## Key Features
1. Real-time board position analysis
2. Visual move suggestions and explanations
3. Threat and defense visualization
4. Position evaluation and move alternatives
5. Customizable visual overlay system
## User Workflows
### Teaching Scenario
1. Instructor starts chess.com game
2. Application detects board position
3. Real-time visual feedback appears
4. Instructor explains moves using visual aids
5. Students see analysis and suggestions
### Self-Learning Scenario
1. Player starts game on chess.com
2. Application provides real-time feedback
3. Player sees move suggestions and threats
4. Application highlights tactical patterns
5. Player reviews game analysis post-match
3. Machine Learning
- Vision-based Core ML model
- Direct category mapping
- High confidence threshold (0.75)
- Fast inference time
## User Experience Goals
1. Intuitive and non-intrusive interface
2. Real-time responsiveness (<100ms latency)
3. Clear visual feedback system
4. Customizable teaching tools
5. Seamless integration with chess.com
1. Reliability
- Accurate piece detection
- Consistent board recognition
- Robust error handling
## Visual Feedback System
1. Move Visualization:
- Arrows for suggested moves
- Color-coded threat levels
- Highlighted squares
2. Performance
- Real-time analysis
- Low resource usage
- Smooth capture
2. Position Analysis:
- Evaluation bar
- Move alternatives
- Threat indicators
3. Usability
- Automatic window detection
- Minimal setup required
- Clear feedback
3. Teaching Aids:
- Defensive patterns
- Attack vectors
- Piece mobility
## Current Status
1. Working Features
- Screen capture system
- Board detection
- Piece recognition
- FEN generation
## Accessibility Features
1. Color-blind friendly themes
2. Keyboard navigation
3. Screen reader support
4. Adjustable overlay size
5. High-contrast modes
2. Recent Improvements
- Simplified classification system
- Direct category mapping
- Vision framework integration
- Improved error handling
## Success Metrics
1. Performance:
- <100ms analysis latency
- 99.9% board recognition accuracy
- 120fps overlay rendering
2. Usability:
- <5 minute setup time
- 90% instructor satisfaction rate
- <1% error rate in move suggestions
3. Adoption:
- 1000+ active users in first 6 months
- 90% retention rate after 30 days
- 4.5+ average rating on App Store
3. Known Limitations
- Requires chess.com's default board theme
- macOS 12.3+ requirement
- Screen capture permissions needed
## Competitive Advantages
1. Native MacOS Silicon optimization
2. Real-time performance with Metal acceleration
3. Integrated teaching tools
4. Chess.com specific optimizations
5. Privacy-focused design
## Future Enhancements
1. Short Term
- Monitor classification accuracy
- Fine-tune confidence threshold
- Improve error reporting
## Development Principles
1. User-centric design
2. Performance-first approach
3. Modular architecture
4. Continuous testing and refinement
5. Security and privacy compliance
2. Long Term
- Support for multiple board themes
- Game analysis integration
- Move suggestion system
## Product Roadmap
### Phase 1: Core Functionality (Weeks 1-6)
- Basic board recognition
- Stockfish integration
- Visual overlay foundation
## Technical Requirements
1. System
- macOS 12.3 or later
- Metal-capable GPU
- Screen recording permissions
### Phase 2: Teaching Tools (Weeks 7-10)
- Move visualization system
- Threat analysis
- Defensive patterns
2. Dependencies
- Vision framework
- Core ML
- ScreenCaptureKit
### Phase 3: Polish & Optimization (Weeks 11-14)
- Performance tuning
- UI/UX refinement
- Security implementation
## Key Milestones
1. Week 4: Functional board recognition
2. Week 6: Working Stockfish analysis
3. Week 8: Basic visual overlay system
4. Week 10: Complete visualization system
5. Week 12: Optimized performance
6. Week 14: Ready for distribution
3. Performance Targets
- 30 FPS capture
- Sub-second analysis
- Low CPU/GPU usage

139
cline_docs/sample.py Normal file
View file

@ -0,0 +1,139 @@
import os
import requests
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
from pathlib import Path
from io import BytesIO
import itertools
class ChessAssetProcessor:
def __init__(self):
self.base_dir = Path.cwd()
self.raw_dir = self.base_dir / 'raw'
self.training_dir = self.base_dir / 'training'
# URLs for pieces
self.piece_base_url = 'https://www.chess.com/chess-themes/pieces/neo/300'
# Piece configurations - matches the Swift enum exactly
self.pieces = {
'white': ['pawn', 'knight', 'bishop', 'rook', 'queen', 'king'],
'black': ['pawn', 'knight', 'bishop', 'rook', 'queen', 'king']
}
# Square colors - used for background variations only
self.square_colors = {
'light': '#eeeed2',
'dark': '#759656',
'light_highlighted': '#f6f68d',
'dark_highlighted': '#bdcc49'
}
# Border configurations
self.border_configs = [
{}, # No borders
{'top': True},
{'bottom': True},
{'left': True},
{'top': True, 'left': True},
{'bottom': True, 'left': True},
]
self._setup_directories()
def _setup_directories(self):
"""Create directory structure for training data"""
(self.raw_dir / 'pieces').mkdir(parents=True, exist_ok=True)
self.training_dir.mkdir(exist_ok=True)
# Create directories for each piece type
for color in ['white', 'black']:
for piece in self.pieces[color]:
(self.training_dir / f"{color}_{piece}").mkdir(parents=True, exist_ok=True)
def hex_to_rgb(self, hex_color):
"""Convert hex color to RGB tuple"""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def create_base_square(self, color, size=100):
"""Create a square with specified color"""
rgb_color = self.hex_to_rgb(color)
return Image.new('RGB', (size, size), rgb_color)
def add_borders(self, image, borders, border_color=(0, 0, 0)):
"""Add borders according to configuration"""
w, h = image.size
result = image.copy()
draw = ImageDraw.Draw(result)
border_size = 5
if borders.get('top'):
draw.line([(0, 0), (w-1, 0)], fill=border_color, width=border_size)
if borders.get('bottom'):
draw.line([(0, h-1), (w-1, h-1)], fill=border_color, width=border_size)
if borders.get('left'):
draw.line([(0, 0), (0, h-1)], fill=border_color, width=border_size)
return result
def download_piece(self, color, piece, target_size):
"""Download a specific chess piece"""
piece_letter = piece[0] if piece != 'knight' else 'n'
piece_url = f"{self.piece_base_url}/{color[0]}{piece_letter}.png"
response = requests.get(piece_url)
if response.status_code == 200:
piece_img = Image.open(BytesIO(response.content)).convert('RGBA')
return piece_img.resize((target_size, target_size), Image.Resampling.LANCZOS)
return None
def create_training_data(self):
"""Create comprehensive training dataset"""
square_size = 100
print("Downloading pieces and creating variations...")
for color in ['white', 'black']:
for piece in self.pieces[color]:
piece_dir = self.training_dir / f"{color}_{piece}"
print(f"\nProcessing {color} {piece}...")
# Download piece
piece_img = self.download_piece(color, piece, square_size)
if piece_img is None:
print(f"Failed to download {color} {piece}")
continue
# Create variations with different backgrounds
variation_count = 0
for bg_name, bg_color in self.square_colors.items():
# Create base square
base_square = self.create_base_square(bg_color, square_size)
# Add border variations
for border_config in self.border_configs:
# Add borders
bordered = self.add_borders(base_square, border_config)
# Convert to RGBA for composition
bordered_rgba = bordered.convert('RGBA')
# Combine with piece
combined = Image.alpha_composite(bordered_rgba, piece_img)
# Generate filename
border_desc = '_'.join(k for k,v in border_config.items() if v)
filename = f"{bg_name}_{border_desc}_{variation_count}.png" if border_desc else f"{bg_name}_{variation_count}.png"
# Save image
combined.save(piece_dir / filename)
variation_count += 1
print(f"Created {variation_count} variations for {color} {piece}")
def main():
processor = ChessAssetProcessor()
processor.create_training_data()
if __name__ == '__main__':
main()

View file

@ -1,240 +1,93 @@
# System Patterns
## Architecture Overview
### Core Components
1. Screenshot Capture Module
- Screen recording permission handling
- Multi-monitor support
- Keyboard shortcut system
- Firefox window detection
## Model Architecture
1. Core ML Integration
- Vision framework for image handling
- Direct category mapping
- No intermediate transformations
2. Board Position Analysis Engine
2. Classification Flow
```
Image → VNImageRequestHandler → VNCoreMLRequest → VNClassificationObservation → SquareClassification
```
3. Category System
- 12 piece categories:
* white_pawn to white_king
* black_pawn to black_king
- Empty square fallback
- No background variations in model
## Processing Pipeline
1. Board Detection
- VNDetectRectanglesRequest
- Aspect ratio validation
- Square extraction
2. Image Processing
- Contrast enhancement
- Edge sharpening
- Noise reduction
- Center crop
3. Classification
- Vision framework integration
- CoreML model inference
- FEN conversion logic
- Position validation system
3. Visual Overlay System
- Metal rendering pipeline
- Transparent window system
- Coordinate transformation
- Move visualization components
4. Stockfish Integration
- ARM64 binary integration
- Async engine wrapper
- Position analysis pipeline
- Evaluation caching
5. Machine Learning Pipeline
- Model training framework
- Theme detection system
- Piece recognition models
- Model versioning system
## Architectural Patterns
### Clean Architecture Layers
1. Presentation Layer (SwiftUI)
- User interface components
- View models
- State management
2. Domain Layer
- Core business logic
- Use cases
- Domain models
3. Data Layer
- Repositories
- Data sources
- Network services
### Design Patterns
1. MVVM for UI components
2. Dependency Injection for service composition
3. Observer pattern for state management
4. Factory pattern for object creation
5. Strategy pattern for analysis algorithms
## Detailed Component Specifications
### Metal Rendering Pipeline
1. Pipeline Stages:
- Vertex processing
- Fragment shading
- Composition
- Post-processing
2. Performance Optimization:
- Command buffer optimization
- Texture compression
- Shader LOD management
- Frame pacing
3. Visual Effects:
- Anti-aliasing
- Bloom effects
- Motion blur
- Depth effects
### Vision Framework Integration
1. Image Analysis Pipeline:
- Image preprocessing
- Feature detection
- Object recognition
- Position tracking
2. Performance Considerations:
- GPU acceleration
- Batch processing
- Memory optimization
- Confidence threshold
- Error handling
3. Integration Points:
- CoreML model integration
- Metal texture sharing
- SwiftUI view integration
- Async/await pattern
## Code Organization
1. Recognition Layer
```
PieceRecognizer
├── Model loading
├── Image preprocessing
└── Classification handling
```
### Async/Await Patterns
1. Concurrency Model:
- Task groups
- Async sequences
- Actor isolation
- Continuations
2. Model Layer
```
SquareClassification
├── Category mapping
├── Piece type/color
└── Empty square handling
```
2. Error Handling:
- Structured concurrency
- Task cancellation
- Error propagation
- Retry mechanisms
3. Core Components
```
BoardDetector
├── Rectangle detection
├── Square extraction
└── Position validation
```
3. Performance Optimization:
- Task prioritization
- Resource contention management
- Memory safety
- Thread management
## Data Flow
1. Capture
```
ScreenCapture → Raw Image → Board Rectangle
```
### CoreML Model Architecture
1. Model Specifications:
- Input/output formats
- Model quantization
- Neural engine optimization
- Model versioning
2. Processing
```
Board Rectangle → Individual Squares → Preprocessed Images
```
2. Training Pipeline:
- Data collection
- Model training
- Validation
- Deployment
3. Classification
```
Preprocessed Images → ML Model → Piece Categories → Chess Position
```
3. Performance Considerations:
- Batch processing
- Memory management
- Model compression
- Inference optimization
## Key Patterns
1. Direct Integration
- Vision framework throughout
- No intermediate conversions
- Consistent image handling
### Stockfish Integration
1. Engine Configuration:
- Thread management
- Hash size optimization
- Analysis depth
- Time controls
2. Error Handling
- Early validation
- Graceful fallbacks
- Detailed logging
2. Analysis Pipeline:
- Position evaluation
- Move generation
- Threat detection
- Position caching
3. Performance Optimization:
- Parallel analysis
- Cache management
- Engine tuning
- Resource allocation
## Data Flow Diagram
1. Input:
- Screenshot capture
- Keyboard input
- System events
2. Processing:
- Board detection
- Position analysis
- Move evaluation
- Visualization generation
3. Output:
- Visual overlay
- Move suggestions
- Position evaluation
- Threat analysis
## Error Handling Strategy
1. Input Validation:
- Screenshot quality checks
- Board position validation
- Move legality verification
2. Recovery Mechanisms:
- Automatic retry for failed operations
- Fallback analysis methods
- Graceful degradation
3. Error Reporting:
- User-friendly error messages
- Detailed error logging
- Crash reporting system
## Security Architecture
1. Data Protection:
- Secure storage for sensitive data
- Encrypted communication channels
- Data minimization principles
2. Access Control:
- Permission management system
- Role-based access control
- Activity monitoring
3. Privacy Features:
- Privacy manifests implementation
- Data collection transparency
- User consent management
## Performance Optimization
1. Rendering:
- Metal shader optimization
- Draw call batching
- Frame rate stabilization
2. Analysis:
- Position caching
- Parallel processing
- Engine optimization
3. Memory Management:
- Efficient resource allocation
- Memory leak prevention
- Garbage collection tuning
## Testing Strategy
1. Unit Testing:
- Core functionality
- Business logic
- Utility functions
2. Integration Testing:
- Module interactions
- Data flow verification
- System behavior
3. Performance Testing:
- Latency benchmarks
- Resource usage
- Stress testing
4. Security Testing:
- Vulnerability scanning
- Penetration testing
- Compliance verification
3. Performance
- Shared CIContext
- Efficient image processing
- Optimized model loading

View file

@ -1,247 +1,88 @@
# Tech Context
# Technical Context
## Technology Stack
### Core Technologies
1. Swift (5.9+)
2. SwiftUI (4.0+)
3. Metal (3.0+)
4. Vision (2.0+)
5. CoreML (5.0+)
6. Create ML (3.0+)
7. Stockfish (16+)
## Development Environment
- macOS Application
- Swift & SwiftUI
- Xcode 14+
- Target: macOS 12.3+
### Development Tools
1. Xcode (15.0+)
2. Swift Package Manager
3. Git (2.40+)
4. CoreML Tools (5.0+)
5. Create ML App (3.0+)
## Core Technologies
1. Vision Framework
- VNDetectRectanglesRequest for board detection
- VNCoreMLRequest for piece classification
- VNImageRequestHandler for image processing
### Testing Frameworks
1. XCTest (5.0+)
2. XCUITest (5.0+)
3. Performance Testing Tools
4. Security Testing Suite
2. Core ML Model
- Name: ChessPieceClassifier.mlmodel
- Input: RGB/RGBA images
- Output: Classification label
- Categories (exact names):
```swift
white_pawn, white_knight, white_bishop, white_rook, white_queen, white_king,
black_pawn, black_knight, black_bishop, black_rook, black_queen, black_king
```
## Development System Specifications
### Operating System
- System: Darwin
- Version: 15.2
- Architecture: arm64
3. ScreenCaptureKit
- Window capture at 30 FPS
- Configurable cursor visibility
- Chess.com window detection
### Hardware Specifications
- CPU: Apple M3 Max
- Memory: 64.00 GB
- GPU: Apple M3 Max (40 cores)
- Metal Support: Metal 3
## Image Processing
1. Preprocessing Pipeline
- Contrast enhancement (1.3x)
- Edge sharpening
- Noise reduction
- Color normalization
### Display Information
- Main Display: LG ULTRAGEAR+
- Resolution: 3840 x 1080
- Refresh Rate: 120Hz
- Features: Television support, rotation support
2. Square Extraction
- Aspect ratio validation
- Size normalization
- Center crop
### Development Tools
- Xcode: 16.2 (Build version 16C5032a)
- Swift: 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
- Target: arm64-apple-macosx15.0
## Model Integration
1. Loading
```swift
let config = MLModelConfiguration()
config.computeUnits = .all
let model = try MLModel(contentsOf: modelURL)
let vnModel = try VNCoreMLModel(for: model)
```
## Development Environment Requirements
### Minimum Requirements
- Apple Silicon (M1)
- 16GB RAM
- Metal 2 support
2. Classification
```swift
let request = VNCoreMLRequest(model: vnModel)
request.imageCropAndScaleOption = .centerCrop
```
### Recommended Requirements
- Apple M2/M3 series
- 32GB+ RAM
- Metal 3 support
- Apple Neural Engine
3. Result Handling
- Confidence threshold: 0.75
- Empty square fallback
- Direct category mapping
### Software Requirements
1. MacOS (Ventura 13.0+)
2. Xcode (15.0+)
3. Swift (5.9+)
4. Git (2.40+)
5. CoreML Tools (5.0+)
## Dependencies
- Foundation
- Vision
- CoreML
- CoreImage
- ScreenCaptureKit
- SwiftUI
## Configuration Details
### Swift Concurrency
1. Async/await pattern implementation
2. Task management system
3. Structured concurrency
4. Actor-based isolation
## Error Handling
- Invalid dimensions
- Model loading failures
- Recognition errors
- Low confidence results
### CoreML Integration
1. Model versioning system
2. Apple Neural Engine optimization
3. Model update mechanism
4. Performance monitoring
### Security Implementation
1. App Sandbox configuration
2. Privacy manifest requirements
3. Secure storage implementation
4. Data encryption standards
## Development Workflow
1. Version Control:
- Git branching strategy
- Code review process
- Commit message guidelines
2. CI/CD Pipeline:
- Automated testing
- Build verification
- Deployment automation
- Release management
3. Code Quality:
- Linting configuration
- Static analysis
- Code coverage requirements
- Documentation standards
## Monitoring & Logging
1. Performance Monitoring:
- Rendering performance
- Analysis latency
- Resource usage
2. Error Tracking:
- Crash reporting
- Error logging
- User feedback integration
3. Analytics:
- Usage tracking
- Feature adoption
- Performance metrics
## Technical Constraints
1. Real-time Requirements:
- <100ms analysis latency
- 120fps rendering
- 99.9% recognition accuracy
2. Compatibility:
- MacOS Silicon only
- Firefox browser integration
- Chess.com specific optimizations
3. Security:
- App Sandbox compliance
- Privacy manifest requirements
- Secure data handling
## Documentation Standards
1. Code Documentation:
- API documentation
- Architecture diagrams
- Technical specifications
2. User Documentation:
- Installation guide
- Usage instructions
- Troubleshooting guide
3. Developer Documentation:
- Setup instructions
- Contribution guidelines
- Code style guide
## Detailed Technical Specifications
### Metal Rendering Pipeline
1. Pipeline Stages:
- Vertex processing
- Fragment shading
- Composition
- Post-processing
2. Performance Optimization:
- Command buffer optimization
- Texture compression
- Shader LOD management
- Frame pacing
3. Visual Effects:
- Anti-aliasing
- Bloom effects
- Motion blur
- Depth effects
### Vision Framework Integration
1. Image Analysis Pipeline:
- Image preprocessing
- Feature detection
- Object recognition
- Position tracking
2. Performance Considerations:
- GPU acceleration
- Batch processing
- Memory optimization
- Error handling
3. Integration Points:
- CoreML model integration
- Metal texture sharing
- SwiftUI view integration
- Async/await pattern
### Async/Await Patterns
1. Concurrency Model:
- Task groups
- Async sequences
- Actor isolation
- Continuations
2. Error Handling:
- Structured concurrency
- Task cancellation
- Error propagation
- Retry mechanisms
3. Performance Optimization:
- Task prioritization
- Resource contention management
- Memory safety
- Thread management
### CoreML Model Architecture
1. Model Specifications:
- Input/output formats
- Model quantization
- Neural engine optimization
- Model versioning
2. Training Pipeline:
- Data collection
- Model training
- Validation
- Deployment
3. Performance Considerations:
- Batch processing
- Memory management
- Model compression
- Inference optimization
### Stockfish Integration
1. Engine Configuration:
- Thread management
- Hash size optimization
- Analysis depth
- Time controls
2. Analysis Pipeline:
- Position evaluation
- Move generation
- Threat detection
- Position caching
3. Performance Optimization:
- Parallel analysis
- Cache management
- Engine tuning
- Resource allocation
## File Organization
```
ChessPrism/
├── Models/
│ ├── SquareClassification.swift # Model output mapping
│ └── ChessPosition.swift # Board state
├── Recognition/
│ ├── PieceRecognizer.swift # ML integration
│ ├── FenGenerator.swift # Position encoding
│ └── MoveDetector.swift # Move analysis
└── Core/
├── BoardDetector.swift # Square extraction
└── ScreenCapture.swift # Window capture

View file

@ -1,90 +0,0 @@
import subprocess
import platform
import json
import os
def get_system_info():
info = {}
# Basic System Info
info['os'] = {
'system': platform.system(),
'version': platform.mac_ver()[0],
'architecture': platform.machine()
}
# CPU Info
cpu_command = "sysctl -n machdep.cpu.brand_string"
info['cpu'] = subprocess.getoutput(cpu_command)
# Memory Info
mem_command = "sysctl hw.memsize"
mem_bytes = int(subprocess.getoutput(mem_command).split()[1])
info['memory_gb'] = mem_bytes / (1024**3)
# GPU Info
gpu_command = "system_profiler SPDisplaysDataType"
gpu_info = subprocess.getoutput(gpu_command)
info['gpu'] = gpu_info
# Screen Info
displays_command = "system_profiler SPDisplaysDataType"
displays_info = subprocess.getoutput(displays_command)
info['displays'] = displays_info
# Xcode Version
xcode_command = "xcodebuild -version"
try:
info['xcode'] = subprocess.getoutput(xcode_command)
except:
info['xcode'] = "Xcode not installed"
# Swift Version
swift_command = "swift --version"
try:
info['swift'] = subprocess.getoutput(swift_command)
except:
info['swift'] = "Swift not installed"
# Metal Support
metal_command = "system_profiler SPDisplaysDataType | grep Metal"
info['metal_support'] = subprocess.getoutput(metal_command)
return info
def save_info():
info = get_system_info()
# Save as JSON
with open('tech_specs.json', 'w') as f:
json.dump(info, f, indent=2)
# Save as readable text
with open('tech_specs.txt', 'w') as f:
f.write("Chess Teaching Assistant - Development System Specifications\n")
f.write("=" * 60 + "\n\n")
f.write("Operating System:\n")
f.write(f"- System: {info['os']['system']}\n")
f.write(f"- Version: {info['os']['version']}\n")
f.write(f"- Architecture: {info['os']['architecture']}\n\n")
f.write("Hardware:\n")
f.write(f"- CPU: {info['cpu']}\n")
f.write(f"- Memory: {info['memory_gb']:.2f} GB\n\n")
f.write("Development Tools:\n")
f.write(f"- Xcode: {info['xcode']}\n")
f.write(f"- Swift: {info['swift']}\n\n")
f.write("Graphics:\n")
f.write(f"- Metal Support:\n{info['metal_support']}\n\n")
f.write("Display Information:\n")
f.write(f"{info['displays']}\n")
if __name__ == "__main__":
save_info()
print("System information has been collected and saved to:")
print("- tech_specs.json (machine-readable)")
print("- tech_specs.txt (human-readable)")

819
sample.py Normal file
View file

@ -0,0 +1,819 @@
def create_pgn_csv(video_list, pgn_list, output_csv_path):
# Prepare the data for the CSV
rows = [{"row_id": video, "output": pgn} for video, pgn in zip(video_list, pgn_list)]
# Write to the CSV
with open(output_csv_path, mode="w", newline="", encoding="utf-8") as file:
writer = csv.DictWriter(file, fieldnames=["row_id", "output"])
writer.writeheader()
writer.writerows(rows)
print(f"CSV file has been created at {output_csv_path}.")
import os
import cv2
import numpy as np
import glob
import matplotlib.pyplot as plt
import datetime
# DRAWING
def show_cv2_image(image, title='image'):
# plt.figure()
# plt.title(title)
# plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
cv2.imwrite(f'output/temp/{title}_{datetime.datetime.now().strftime("%Y%m%d%H%M%S")}.png', image)
# OCR
def ocr_image(input_image, verbose=False):
"""Detects text in the file."""
from google.cloud import vision
client = vision.ImageAnnotatorClient()
content = cv2.imencode('.jpg', input_image)[1].tobytes()
image = vision.Image(content=content)
response = client.text_detection(image=image)
texts = response.text_annotations
if verbose:
print("Texts:")
for text in texts:
print(f'\n"{text.description}"')
vertices = [
f"({vertex.x},{vertex.y})" for vertex in text.bounding_poly.vertices
]
print("bounds: {}".format(",".join(vertices)))
if response.error.message:
raise Exception(
"{}\nFor more info on error messages, check: "
"https://cloud.google.com/apis/design/errors".format(response.error.message)
)
return texts
def detection_to_dict(detection):
return {
'description': detection.description,
'vertices': [
(vertex.x, vertex.y) for vertex in detection.bounding_poly.vertices
]
}
def draw_box(image, a, b, c, d):
cv2.polylines(image, [np.array([a, b, c, d], np.int32)], True, (0, 255, 0), 2)
def show_image_with_ocr(image, title='ocr result'):
result = ocr_image(image)
if len(image.shape) == 2:
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
for text in result:
text_dict = detection_to_dict(text)
vertices = text_dict['vertices']
draw_box(image, vertices[0], vertices[1], vertices[2], vertices[3])
show_cv2_image(image, title)
def show_image_with_ocr_labelled(image, title='ocr result'):
result = ocr_image(image)
if len(image.shape) == 2:
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
for text in result:
text_dict = detection_to_dict(text)
vertices = text_dict['vertices']
draw_box(image, vertices[0], vertices[1], vertices[2], vertices[3])
cv2.putText(image, text_dict['description'], vertices[0], cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
show_cv2_image(image, title)
# PROCESS IMAGE
# Function to compute the intersection of two lines
def compute_intersection(line1, line2):
rho1, theta1 = line1
rho2, theta2 = line2
# Calculate the intersection of two lines
A = np.array([[np.cos(theta1), np.sin(theta1)], [np.cos(theta2), np.sin(theta2)]])
b = np.array([rho1, rho2])
# Solve the linear system to find the intersection point
intersection = np.linalg.solve(A, b)
return int(intersection[0]), int(intersection[1])
def draw_white_board_boundaries(image):
# Convert the image to HSV color space
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Define the range of green color in HSV
lower_green = np.array([40, 25, 40]) # Lower bound of green in HSV
upper_green = np.array([100, 200, 200]) # Upper bound of green in HSV
# Threshold the image to get only the green color
mask = cv2.inRange(hsv, lower_green, upper_green)
# Find contours
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Create a black canvas to draw contours
contour_image = np.zeros_like(mask)
# Draw the contours on the black canvas (255 for white contours)
cv2.drawContours(contour_image, contours, -1, (255), 1)
# Apply the Canny edge detector on the contour image
edges = cv2.Canny(contour_image, 50, 150, apertureSize=3)
# Apply Hough Line Transform to find lines in the edge-detected image
lines = cv2.HoughLines(edges, 1, np.pi / 180, threshold=100)
# Separate the lines into vertical and horizontal based on their angle (theta)
vertical_lines = []
horizontal_lines = []
# Find vertical and horizontal lines
if lines is not None:
for rho, theta in lines[:, 0]:
# Identify vertical lines (theta near 0 or 180 degrees)
if np.abs(theta) < np.pi / 180 * 10 or np.abs(theta - np.pi) < np.pi / 180 * 10:
vertical_lines.append((rho, theta))
# Identify horizontal lines (theta near 90 degrees)
elif np.abs(theta - np.pi / 2) < np.pi / 180 * 10:
horizontal_lines.append((rho, theta))
# Create an empty list to store intersection points
intersection_points = []
# Find intersection points between vertical and horizontal lines
for v_line in vertical_lines:
for h_line in horizontal_lines:
intersection = compute_intersection(v_line, h_line)
intersection_points.append(intersection)
# Create an empty image to draw the intersection points
intersection_image = np.zeros_like(image)
# Draw the intersection points on the image (red points)
for point in intersection_points:
cv2.circle(intersection_image, point, 10, (0, 0, 255), -1) # Red circle at intersection points
# Convert the intersection image to grayscale
grayscale_image = cv2.cvtColor(intersection_image, cv2.COLOR_BGR2GRAY)
# Find contours of the red intersection points (non-zero pixels)
contours, _ = cv2.findContours(grayscale_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Create an empty black image to draw the center points
center_image = np.copy(image)
# List to store the center points' coordinates
center_points = []
# Iterate over each contour and find the centroid (center point)
for contour in contours:
# Calculate the moments of the contour
moments = cv2.moments(contour)
# Calculate the centroid (center) of the contour
if moments['m00'] != 0:
cx = int(moments['m10'] / moments['m00'])
cy = int(moments['m01'] / moments['m00'])
# Draw the center point (blue) on the original image
cv2.circle(center_image, (cx, cy), 5, (255, 0, 0), -1) # Blue circle at center
# Store the center coordinates in the list
center_points.append((cx, cy))
center_points = sorted(center_points, key=lambda x: sum(x))
# Step 1: Find the convex hull of the center points
center_points_np = np.array(center_points, dtype=np.int32) # Convert to NumPy array
hull = cv2.convexHull(center_points_np) # Compute convex hull
# Step 2: Create a mask for the filled polygon
mask = np.ones_like(image, dtype=np.uint8) * 255 # Create a white mask with the same dimensions as the image
cv2.fillPoly(mask, [hull], (0, 0, 0)) # Draw the filled polygon in black on the mask
image[mask == 0] = 255
result_image = image
return result_image
def process_image(image):
processed_image = draw_white_board_boundaries(image)
result = processed_image
return result
# detect chess board orientation
def bounding_box(left, top, width, height):
return [(left, top), (left + width, top), (left + width, top + height), (left, top + height)]
def draw_bounding_box(image, box, color=(0, 255, 0)):
cv2.polylines(image, [np.array(box)], isClosed=True, color=color, thickness=2)
return image
def get_board_orientation(bound_8, bound_h):
# compute the center of the two bounding boxes
center_8 = np.mean(np.array(bound_8), axis=0)
center_h = np.mean(np.array(bound_h), axis=0)
# check the relative position of the two centers
if center_8[0] < center_h[0] and center_8[1] < center_h[1]:
return 'UPRIGHT'
elif center_8[0] > center_h[0] and center_8[1] > center_h[1]:
return 'UPSIDE_DOWN'
elif center_8[0] < center_h[0] and center_8[1] > center_h[1]:
return 'ROTATED_RIGHT'
elif center_8[0] > center_h[0] and center_8[1] < center_h[1]:
return 'ROTATED_LEFT'
def detect_chessboard_orientation(image):
data = ocr_image(image)
bounds_8 = [] # sample: [(x1, y1), (x2, y2), (x3, y3), (x4, y4)]
bounds_h = [] # sample: [(x1, y1), (x2, y2), (x3, y3), (x4, y4)]
for detection in data:
data_dict = detection_to_dict(detection)
if data_dict['description'] == '8':
# store the bounding box of the 8
bounds_8.append(data_dict['vertices'])
elif data_dict['description'] == 'h':
bounds_h.append(data_dict['vertices'])
# find the closest pair of 8 and h
closest = None
for bound_8 in bounds_8:
for bound_h in bounds_h:
distance = np.linalg.norm(np.array(bound_8) - np.array(bound_h))
if closest is None or distance < closest[0]:
closest = (distance, bound_8, bound_h)
print(f'Closest pair: {closest}')
if closest is None:
return False, closest
# compute the center of the two bounding boxes
center_8 = np.mean(np.array(closest[1]), axis=0)
center_h = np.mean(np.array(closest[2]), axis=0)
# draw the line between the two points
cv2.line(image, tuple(center_8.astype(int)), tuple(center_h.astype(int)), (255, 0, 0), 2)
# draw the bounding boxes
image = draw_bounding_box(image, bound_8, (0, 255, 0))
image = draw_bounding_box(image, bound_h, (0, 0, 255))
show_cv2_image(image, "detected image")
return get_board_orientation(closest[1], closest[2]), closest
def get_k(image):
images = [image]
for i in range(3):
images.append(cv2.rotate(images[-1], cv2.ROTATE_90_CLOCKWISE))
k = 0
for i in range(4):
result = detect_chessboard_orientation(process_image(images[i]))
if result[0] and result[1]:
bound_8 = result[1][1][0]
bound_h = result[1][1][1]
# bound_8 and bound_h are in the bottom-left window of the image
h, w = images[i].shape[:2]
if bound_8[0] < w / 2 and bound_8[1] > h / 2 and bound_h[0] < w / 2 and bound_h[1] > h / 2:
k = i
break
return k
import cv2
import numpy as np
from ultralytics import YOLO
from PIL import Image
import chess
import time
import chess.pgn
import mediapipe as mp
from statistics import mode
# Define a mapping of YOLO labels to chess piece names
def label_to_piece_name(label):
piece_map = {
1: "bB", # black-bishop
2: "bK", # black-king
3: "bN", # black-knight
4: "bP", # black-pawn
5: "bQ", # black-queen
6: "bR", # black-rook
7: "wB", # white-bishop
8: "wK", # white-king
9: "wN", # white-knight
10: "wP", # white-pawn
11: "wQ", # white-queen
12: "wR" # white-rook
}
return piece_map.get(label, "?")
# Function to find intersection points of the grid lines
def find_grid(image, k=0):
# image = cv2.imread(image_path)
# Convert the image to HSV color space
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
for i in range(k):
hsv = cv2.rotate(hsv, cv2.ROTATE_90_CLOCKWISE)
# Define the range of green color in HSV (assuming green grid lines)
lower_green = np.array([40, 25, 40]) # Lower bound of green in HSV
upper_green = np.array([100, 200, 200]) # Upper bound of green in HSV
# Threshold the image to get only the green color
mask = cv2.inRange(hsv, lower_green, upper_green)
# Find contours
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Create a black canvas to draw contours
contour_image = np.zeros_like(mask)
# Draw the contours on the black canvas (255 for white contours)
cv2.drawContours(contour_image, contours, -1, (255), 1)
# Apply Canny edge detector
edges = cv2.Canny(contour_image, 50, 150, apertureSize=3)
# Apply Hough Line Transform to find lines in the edge-detected image
lines = cv2.HoughLines(edges, 1, np.pi / 180, threshold=100)
# Separate the lines into vertical and horizontal based on their angle
vertical_lines = []
horizontal_lines = []
# Find vertical and horizontal lines
if lines is not None:
for rho, theta in lines[:, 0]:
# Identify vertical lines (theta near 0 or 180 degrees)
if np.abs(theta) < np.pi / 180 * 10 or np.abs(theta - np.pi) < np.pi / 180 * 10:
vertical_lines.append((rho, theta))
# Identify horizontal lines (theta near 90 degrees)
elif np.abs(theta - np.pi / 2) < np.pi / 180 * 10:
horizontal_lines.append((rho, theta))
# Create an empty list to store intersection points
intersection_points = []
# Function to compute the intersection of two lines
def compute_intersection(line1, line2):
rho1, theta1 = line1
rho2, theta2 = line2
A = np.array([[np.cos(theta1), np.sin(theta1)], [np.cos(theta2), np.sin(theta2)]])
b = np.array([rho1, rho2])
# Solve the linear system to find the intersection point
intersection = np.linalg.solve(A, b)
return int(intersection[0]), int(intersection[1])
# Find intersection points between vertical and horizontal lines
for v_line in vertical_lines:
for h_line in horizontal_lines:
intersection = compute_intersection(v_line, h_line)
intersection_points.append(intersection)
return intersection_points
# Function to map detected chess pieces to the board using intersection points
def map_yolo_results_to_chessboard(results, chessboard_corners):
"""
Maps YOLO detection results to a chessboard grid.
Args:
- results: YOLO detection results containing labels and bounding boxes.
- chessboard_corners: List of tuples [(x1, y1), (x2, y2), ..., (x4, y4)]
representing the corners of the chessboard
(top-left, top-right, bottom-left, bottom-right).
Returns:
- A formatted string representation of the chessboard with mapped pieces.
"""
# Extract the chessboard corners
top_left, top_right, bottom_left, bottom_right = chessboard_corners
# Calculate the width and height of each cell
cell_width = (top_right[0] - top_left[0]) / 8
cell_height = (bottom_left[1] - top_left[1]) / 8
# Initialize an empty 8x8 chessboard
board = [['' for _ in range(8)] for _ in range(8)]
# Process YOLO results
for r in results:
boxes = r.boxes.xywh.numpy() # Bounding boxes in (x_center, y_center, width, height)
labels = r.boxes.cls.numpy() # Class indices
for box, label in zip(boxes, labels):
x_center, y_center, _, _ = box
piece_name = label_to_piece_name(int(label))
# Determine the row and column based on the center point
col = int((x_center - top_left[0]) / cell_width)
row = int((y_center - top_left[1]) / cell_height)
# Ensure row and col are within bounds
if 0 <= row < 8 and 0 <= col < 8:
board[row][col] = piece_name
# Format the board for display
formatted_board = '\n'.join([' '.join([cell if cell else '--' for cell in row]) for row in board])
return formatted_board
def convert_to_valid_fen(board_string):
# Mapping of custom pieces to FEN standard pieces
piece_mapping = {
"wP": "P", "wR": "R", "wN": "N", "wB": "B", "wQ": "Q", "wK": "K",
"bP": "p", "bR": "r", "bN": "n", "bB": "b", "bQ": "q", "bK": "k",
"--": "1" # Empty squares
}
# Split the input string into rows
rows = board_string.strip().split("\n")
fen_rows = []
for row in rows:
squares = row.split() # Split the row into individual squares
fen_row = ""
for square in squares:
fen_row += piece_mapping.get(square, square) # Replace with mapped value
# Compress consecutive digits (empty spaces) into single numbers
compressed_row = ""
empty_count = 0
for char in fen_row:
if char.isdigit(): # Count empty squares
empty_count += int(char)
else:
if empty_count > 0:
compressed_row += str(empty_count)
empty_count = 0
compressed_row += char
if empty_count > 0:
compressed_row += str(empty_count) # Add remaining empty squares
fen_rows.append(compressed_row)
# Combine rows with "/" and add default metadata
fen_board = "/".join(fen_rows)
fen_metadata = " w - - 0 1" # White to move, no castling, no en passant
return fen_board + fen_metadata
def rotate_fen(fen):
# Split the FEN into board state and other details
board, *rest = fen.split(' ')
# Split the board into rows
rows = board.split('/')
# Rotate each row (reverse the pieces) and then reverse the row order
rotated_rows = [''.join(reversed(row)) for row in reversed(rows)]
# Recombine the rows into the rotated FEN
rotated_board = '/'.join(rotated_rows)
# Combine the rotated board with the rest of the FEN details
return ' '.join([rotated_board] + rest)
def convert_to_san(moves):
san_moves = []
move_number = 1
for i in range(0, len(moves), 2):
if i + 1 < len(moves):
# Pair moves for each turn
san_moves.append(f"{move_number}. {moves[i]} {moves[i+1]}")
else:
# If there's an odd move at the end, only record that
san_moves.append(f"{move_number}. {moves[i]}")
move_number += 1
return " ".join(san_moves)
def get_chessboard_corners(image, k=0):
coor = find_grid(image, k)
coor = sorted(coor, key=lambda x: sum(x))
min = coor[0]
max = coor[-1]
chessboard_corners = [(min[0], min[1]), (max[0], min[1]), (min[0], max[1]), (max[0], max[1])]
return chessboard_corners
def board_list_to_list(board_list):
output = [[[],[],[],[],[],[],[],[]],
[[],[],[],[],[],[],[],[]],
[[],[],[],[],[],[],[],[]],
[[],[],[],[],[],[],[],[]],
[[],[],[],[],[],[],[],[]],
[[],[],[],[],[],[],[],[]],
[[],[],[],[],[],[],[],[]],
[[],[],[],[],[],[],[],[]]]
for board in board_list:
board_splitR = board.split("\n")
for r,board_row in enumerate(board_splitR):
board_pos = board_row.split(" ")
for c,piece in enumerate(board_pos):
output[r][c].append(piece)
for r,row in enumerate(output):
for c,col in enumerate(row):
output[r][c] = mode(col)
return output
def board_to_pgn(prev_b, curr_b):
board_pos_coor = [['a8', 'b8', 'c8', 'd8', 'e8', 'f8','g8','h8'],
['a7', 'b7', 'c7', 'd7', 'e7', 'f7','g7','h7'],
['a6', 'b6', 'c6', 'd6', 'e6', 'f6','g6','h6'],
['a5', 'b5', 'c5', 'd5', 'e5', 'f5','g5','h5'],
['a4', 'b4', 'c4', 'd4', 'e4', 'f4','g4','h4'],
['a3', 'b3', 'c3', 'd3', 'e3', 'f3','g3','h3'],
['a2', 'b2', 'c2', 'd2', 'e2', 'f2','g2','h2'],
['a1', 'b1', 'c1', 'd1', 'e1', 'f1','g1','h1']]
# Find the coordinates of the changed piece
moved_from = None
moved_to = None
for row in range(8):
for col in range(8):
if prev_b[row][col] != curr_b[row][col]:
if curr_b[row][col] == '.':
# The piece moved from this square
moved_from = (row, col)
else:
# The piece moved to this square
moved_to = (row, col)
if moved_from is None or moved_to is None:
return "No valid move found", False # In case of invalid input
# Convert coordinates to chess notation
from_square = board_pos_coor[moved_from[0]][moved_from[1]]
to_square = board_pos_coor[moved_to[0]][moved_to[1]]
moved_piece = prev_b[moved_from[0]][moved_from[1]]
if moved_piece.islower(): # black move
if prev_b[moved_to[0]][moved_to[1]] != '.': # black capture
if moved_piece == 'p':
pgn = f"{board_pos_coor[moved_from[0]][moved_from[1]][0]}x{to_square}" # black pawn capture
else:
pgn = f"{moved_piece.upper()}x{to_square}" #black non pawn capture
else: # not capture
if moved_piece == 'p':
moved_piece = ""
else:
moved_piece = moved_piece.upper()
pgn = f"{moved_piece}{to_square}" # Regular pawn move
else: # white
if prev_b[moved_to[0]][moved_to[1]] != '.': # capture
if moved_piece == 'P':
pgn = f"{board_pos_coor[moved_from[0]][moved_from[1]][0]}x{to_square}" # capture
else:
pgn = f"{moved_piece}x{to_square}"
else: # not capture
if moved_piece == 'P':
pgn = f"{to_square}" # Regular piece move
else:
pgn = f"{moved_piece}{to_square}" # Regular pawn move
is_white = prev_b[moved_from[0]][moved_from[1]].isupper()
return pgn, is_white
def gen_pgn(vid_path, model):
mp_hands = mp.solutions.hands
hands = mp_hands.Hands(static_image_mode=False, max_num_hands=2, min_detection_confidence=0.5)
# mp_drawing = mp.solutions.drawing_utils
cap = cv2.VideoCapture(vid_path)
# Parameters for frame processing
frame_count = 0
frame_interval = int(float(cap.get(cv2.CAP_PROP_FPS)) * 0.5) # Process frame every 1 second
# previous_board = None # Track the previous board state
# previous_hand_present = False # Track if a hand was detected in the previous frame
# list of board list; appends the board_lists
board_list_list = []
# board list iterate every time hand is present
board_list = []
# get one frame
print('getting k from gen_pgn')
k = get_k(cap.read()[1])
while cap.isOpened():
ret, frame = cap.read()
if not ret:
if len(board_list) != 0:
board_list_list.append(board_list)
board_list = []
break # End of video
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Perform hand detection
hand_results = hands.process(rgb_frame)
not_hand_present = hand_results.multi_hand_landmarks == None
# Crop the frame
frame = frame[425:1495, :]
for i in range(k):
frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
# Process frame every frame_interval
if frame_count % frame_interval == 0:
if not_hand_present:
results = model(frame, conf=0.3)
chessboard_corners = get_chessboard_corners(frame, k)
formatted_board = map_yolo_results_to_chessboard(results, chessboard_corners)
fen = convert_to_valid_fen(formatted_board)
fen = rotate_fen(fen)
current_board = chess.Board(fen)
#print('board')
board_list.append(str(current_board)[1:-1])
elif not not_hand_present or not ret:
#print('hand')
if len(board_list) != 0:
board_list_list.append(board_list)
board_list = []
# Increment frame counter
frame_count += 1
#san_notation = convert_to_san(['..'] + san_move)
# print(san_notation)
# cap.release()
# cv2.destroyAllWindows()
# print(board_list_list)
# print(len(board_list_list))
# print(board_list_list[0])
# print(board_list_list[1])
# print(board_list_list[2])
for i,b_list in enumerate(board_list_list):
print(b_list)
new_board_list = board_list_to_list(b_list)
board_list_list[i] = new_board_list
print(str(new_board_list)+"\n\n")
pgn_index = 1
white_move = ".."
black_move = ".."
pgn_all = [""]
for i in range(len(board_list_list) - 1):
p_board = board_list_list[i]
c_board = board_list_list[i + 1]
pgn, is_white = board_to_pgn(prev_b=p_board, curr_b=c_board)
if is_white:
white_move = pgn
else:
black_move = pgn
pgn_row = f"{pgn_index}. {white_move} {black_move} "
if is_white or (not is_white and black_move != ".."):
if len(pgn_all) < pgn_index:
pgn_all.append(pgn_row)
else:
pgn_all[pgn_index - 1] = pgn_row
if black_move != "..":
white_move = ".."
black_move = ".."
pgn_index += 1
out_str = ""
for r in pgn_all:
out_str += r
if out_str == "":
return '1. '
return out_str
# # Initialize Mediapipe Hands
# mp_hands = mp.solutions.hands
# hands = mp_hands.Hands(static_image_mode=False, max_num_hands=2, min_detection_confidence=0.5)
# mp_drawing = mp.solutions.drawing_utils
# 1. .. move 2. move ..
# Load the YOLO model
model = YOLO("best2.pt") # Replace with the path to your trained YOLO model
# image_path = "detection/test2.jpg"
# image = cv2.imread(image_path)
video_path_2m = "kaggle/input/2_move_student.mp4" # Replace with the path to your video
video_path_4m = "kaggle/input/4_Move_studet.mp4" # Replace with the path to your video
video_path_6m = "kaggle/input/6_Move_student.mp4" # Replace with the path to your video
video_path_8m = "kaggle/input/8_Move_student.mp4" # Replace with the path to your video
video_path_2mr = "kaggle/input/2_Move_rotate_student.mp4"
video_path_list = [video_path_2mr, video_path_2m, video_path_4m, video_path_6m, video_path_8m]
output_path = "output/output_video.avi" # Optional: Specify a path to save the output video
# for vidp in vidp_list:
# for vidp in video_path_list:
# gen_pgn(vidp, model)
# print(gen_pgn(video_path_4m, model))
# Release resources
#R . B . Q . . R
#P P K . . P . P
#. . . . . N . .
#q . . P . . P .
#. . . p P . . n
#. . . . p . . .
#. p . . b p p p
#R n b k . . . .
#R . B . Q . . R
#P . K . . P . P
#. P . . . N . .
#q . . P . . P .
#. . . p P . . n
#. . . . p . . .
#. p . . b p p p
#R n b k . . . .
pgn = []
for path in video_path_list:
pgn.append(gen_pgn(path, model))
print(pgn)
import pandas as pd
vids = ['2_Move_rotate_student.mp4','2_move_student.mp4','4_Move_studet.mp4','6_Move_student.mp4','8_Move_student.mp4', '(Bonus)Long_video_student.mp4']
print(len(pgn), len(vids))
if len(pgn) < len(vids):
pgn.append('1.')
# pgn.append('1.')
df = pd.DataFrame({
"row_id": vids,
"output": pgn
})
# Save to CSV
df.to_csv("kaggle/working/submission.csv", index=False, encoding="utf-8")

View file

@ -1,162 +0,0 @@
import os
import subprocess
def create_directory_structure():
# Main project directories
directories = [
'src',
'src/core',
'src/features',
'src/utils',
'src/services',
'tests',
'tests/unit',
'tests/integration',
'tests/performance',
'docs',
'docs/api',
'docs/architecture',
'docs/development',
'assets',
'assets/ml-models',
'assets/shaders',
'scripts',
'config'
]
# Create directories
for directory in directories:
os.makedirs(directory, exist_ok=True)
def create_base_files():
# Git-related files
files = {
'.gitignore': '''
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Xcode
xcuserdata/
*.xcscmblueprint
*.xccheckout
build/
DerivedData/
*.moved-aside
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
# Swift Package Manager
.build/
Packages/
Package.pins
Package.resolved
# ML Models
*.mlmodel
# Environment
.env
.env.*
# IDE
.idea/
.vscode/
# Testing
*.xcresult
# Fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
''',
'README.md': '''
# Chess Teaching Assistant
An intelligent chess teaching assistant for macOS that provides real-time analysis and visualization.
## Features
- Real-time board position analysis
- Visual move suggestions
- Attack pattern visualization
- Defensive planning indicators
## Development
See [Development Guide](docs/development/getting-started.md) for setup instructions.
## Architecture
See [Architecture Overview](docs/architecture/overview.md) for system design details.
## License
[MIT License](LICENSE)
''',
'LICENSE': '''MIT License
Copyright (c) 2025 Chess Teaching Assistant
Permission is hereby granted, free of charge...''',
'docs/development/getting-started.md': '''# Getting Started
## Prerequisites
- Xcode 15.0+
- macOS 14.0+
- Swift 6.0+
## Setup Instructions
1. Clone the repository
2. Install dependencies
3. Build and run
''',
'docs/architecture/overview.md': '''# Architecture Overview
## System Components
- Screenshot Capture Module
- Board Position Analysis Engine
- Visual Overlay System
- Stockfish Integration
''',
'src/core/__init__.py': '',
'src/features/__init__.py': '',
'src/utils/__init__.py': '',
'src/services/__init__.py': '',
'tests/__init__.py': '',
'config/default.json': '''{
"analysis": {
"depth": 20,
"threads": 4
},
"visualization": {
"arrowColor": "#FF0000",
"highlightColor": "#00FF00"
}
}'''
}
for file_path, content in files.items():
with open(file_path, 'w') as f:
f.write(content.strip())
def initialize_git():
commands = [
['git', 'init'],
['git', 'add', '.'],
['git', 'commit', '-m', 'Initial project structure'],
['git', 'branch', '-M', 'main'],
]
for command in commands:
subprocess.run(command)
def main():
create_directory_structure()
create_base_files()
initialize_git()
print("Project structure created successfully!")
if __name__ == "__main__":
main()