Skip to content

Instantly share code, notes, and snippets.

@1998code
Created November 17, 2025 15:03
Show Gist options
  • Select an option

  • Save 1998code/90768001939492d84408f7b65708aa3d to your computer and use it in GitHub Desktop.

Select an option

Save 1998code/90768001939492d84408f7b65708aa3d to your computer and use it in GitHub Desktop.
reTicTacCHA
//
// reTicTacCHA.swift
// Tic-Tac-Toe reCAPTCHA
//
// A fun reCAPTCHA-styled Tic-Tac-Toe game where users must beat an AI
// to prove they're human. Features glass morphism UI inspired by iOS 18+
// and a minimax AI opponent with configurable difficulty.
//
// Created by Ming on 17/11/2025.
//
import SwiftUI
import Combine
// MARK: - Game Logic
/// Represents the two players in the game
enum Player {
case human // User playing as X
case ai // AI opponent playing as O
/// Returns the symbol (X or O) for each player
var symbol: String {
switch self {
case .human: return "X"
case .ai: return "O"
}
}
}
/// Tracks the current state of the game
enum GameState {
case playing // Game is in progress
case humanWon // Human won - verification successful
case aiWon // AI won - verification failed
case draw // Board full with no winner - verification failed
}
/// Main game controller managing game state and logic
class TicTacToeGame: ObservableObject {
// MARK: - Published Properties
/// 3x3 board represented as array of 9 positions (nil = empty, "X" = human, "O" = AI)
@Published var board: [String?] = Array(repeating: nil, count: 9)
/// Current game state (playing, won, lost, or draw)
@Published var gameState: GameState = .playing
/// Whether the initial checkbox has been clicked
@Published var isChecked: Bool = false
/// Whether to show loading spinner in checkbox
@Published var showProgress: Bool = false
/// Whether to show the game dialog (vs initial CAPTCHA screen)
@Published var showGame: Bool = false
/// Message displayed at top of game dialog
@Published var message: String = "Select all squares to defeat the AI"
// MARK: - Game Logic
/// Handles human player's move at the specified board index
/// - Parameter index: Position on the board (0-8)
func makeMove(at index: Int) {
// Only allow moves on empty squares during active gameplay
guard board[index] == nil && gameState == .playing else { return }
// Place human's X
board[index] = Player.human.symbol
// Check if human won with this move
if checkWinner(for: Player.human) {
gameState = .humanWon
message = "Verification successful! You're human!"
return
}
// Check if board is full (draw)
if isBoardFull() {
gameState = .draw
message = "Verification failed. Please try again."
return
}
// AI responds immediately (synchronous to avoid race conditions)
let moveIndex: Int
// 40% chance to make a random move (gives human a better chance to win)
if Double.random(in: 0...1) < 0.4 {
let availableMoves = board.indices.filter { board[$0] == nil }
moveIndex = availableMoves.randomElement() ?? 0
} else {
// Use minimax algorithm for optimal play
moveIndex = findBestMove()
}
board[moveIndex] = Player.ai.symbol
// Check game end conditions
// Always prioritize checking if human won (handles edge cases)
if checkWinner(for: Player.human) {
gameState = .humanWon
message = "Verification successful! You're human!"
} else if checkWinner(for: Player.ai) {
gameState = .aiWon
message = "Verification failed. Unusual activity detected."
} else if isBoardFull() {
gameState = .draw
message = "Verification failed. Please try again."
}
}
// MARK: - AI Algorithm
/// Finds the best move using minimax algorithm
/// - Returns: Index of the optimal move (0-8)
func findBestMove() -> Int {
var bestScore = Int.min
var bestMove = 0
// Try each available position and evaluate using minimax
for i in 0..<9 {
if board[i] == nil {
// Simulate AI move
board[i] = Player.ai.symbol
let score = minimax(depth: 0, isMaximizing: false)
// Undo move
board[i] = nil
// Track the move with the highest score
if score > bestScore {
bestScore = score
bestMove = i
}
}
}
return bestMove
}
/// Minimax algorithm for optimal move selection
/// - Parameters:
/// - depth: Current depth in the game tree
/// - isMaximizing: True if maximizing player (AI), false if minimizing player (human)
/// - Returns: Score for the current board state
func minimax(depth: Int, isMaximizing: Bool) -> Int {
// Base cases: check terminal states
if checkWinner(for: Player.ai) { return 10 - depth } // AI wins (favor faster wins)
if checkWinner(for: Player.human) { return depth - 10 } // Human wins
if isBoardFull() { return 0 } // Draw
if isMaximizing {
// AI's turn: maximize score
var bestScore = Int.min
for i in 0..<9 {
if board[i] == nil {
board[i] = Player.ai.symbol
let score = minimax(depth: depth + 1, isMaximizing: false)
board[i] = nil
bestScore = max(score, bestScore)
}
}
return bestScore
} else {
// Human's turn: minimize score
var bestScore = Int.max
for i in 0..<9 {
if board[i] == nil {
board[i] = Player.human.symbol
let score = minimax(depth: depth + 1, isMaximizing: true)
board[i] = nil
bestScore = min(score, bestScore)
}
}
return bestScore
}
}
// MARK: - Helper Functions
/// Checks if the specified player has won the game
/// - Parameter player: The player to check (human or AI)
/// - Returns: True if the player has three in a row
func checkWinner(for player: Player) -> Bool {
let winPatterns = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows
[0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns
[0, 4, 8], [2, 4, 6] // Diagonals
]
// Check all possible win patterns (rows, columns, diagonals)
return winPatterns.contains { pattern in
pattern.allSatisfy { board[$0] == player.symbol }
}
}
/// Checks if the board is completely filled
/// - Returns: True if no empty squares remain
func isBoardFull() -> Bool {
board.allSatisfy { $0 != nil }
}
/// Resets the entire game to initial state
func reset() {
board = Array(repeating: nil, count: 9)
gameState = .playing
isChecked = false
showProgress = false
showGame = false
message = "Select all squares to defeat the AI"
}
/// Starts a new game (called after clicking checkbox)
func startGame() {
// Clear previous game state
board = Array(repeating: nil, count: 9)
gameState = .playing
message = "Select all squares to defeat the AI"
// Show game dialog and hide progress spinner
withAnimation {
showGame = true
showProgress = false
}
}
}
// MARK: - Views
/// Main content view with gradient background
struct ContentView: View {
@StateObject private var game = TicTacToeGame()
var body: some View {
ZStack {
// Gradient background
LinearGradient(
gradient: Gradient(colors: [
Color(red: 0.85, green: 0.90, blue: 1.0),
Color(red: 0.95, green: 0.95, blue: 0.98),
Color(red: 0.90, green: 0.85, blue: 1.0)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
// Switch between initial CAPTCHA and game views
VStack(spacing: 0) {
if !game.showGame {
InitialCaptchaView(game: game)
.transition(.opacity)
} else {
GameCaptchaView(game: game)
.transition(.opacity)
}
}
}
}
}
/// Initial CAPTCHA screen with "I'm not a robot" checkbox
struct InitialCaptchaView: View {
@ObservedObject var game: TicTacToeGame
var body: some View {
VStack(spacing: 20) {
Spacer()
VStack(spacing: 15) {
HStack(spacing: 12) {
// Checkbox showing different states: unchecked, loading, success, or failure
CheckboxView(
isChecked: $game.isChecked,
showX: game.gameState == .aiWon || game.gameState == .draw,
showCheck: game.gameState == .humanWon,
showProgress: game.showProgress
)
.onTapGesture {
// Only respond to taps when not already in progress
if !game.showProgress && !game.showGame {
// Show loading spinner
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
game.showProgress = true
}
// Start game after brief delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
game.startGame()
}
}
}
// Dynamic text based on game outcome
Text(
game.gameState == .humanWon ? "Confirmed you're not a robot 🎉" :
(game.gameState == .aiWon || game.gameState == .draw) ? "You're a robot, try again!" :
"I'm not a robot"
)
.font(.system(size: 16))
.foregroundColor(.primary)
Spacer()
}
.padding()
.glassEffect(.clear.interactive(), in: .rect(cornerRadius: 12)) // iOS 18+ glass morphism effect
.shadow(color: Color.black.opacity(0.15), radius: 10, x: 0, y: 5)
// Footer with branding and reset button
HStack {
Image(systemName: "shield.checkered")
.font(.system(size: 10))
.foregroundColor(.gray)
Text("reTicTacCHA")
.font(.system(size: 10))
.foregroundColor(.gray)
Spacer()
Text("Privacy - Terms")
.font(.system(size: 9))
.foregroundColor(.blue)
Button(action: {
withAnimation {
game.reset()
}
}) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 9))
.foregroundColor(.gray)
}
}
.padding(.horizontal, 5)
}
.frame(width: 280)
Spacer()
}
}
}
/// Main game dialog showing the Tic-Tac-Toe board
struct GameCaptchaView: View {
@ObservedObject var game: TicTacToeGame
var body: some View {
VStack(spacing: 0) {
Spacer()
VStack(spacing: 0) {
// Header
HStack {
Image(systemName: "shield.checkered")
.foregroundColor(.white)
Text(game.message)
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
Spacer()
Button(action: {
withAnimation {
game.showGame = false
}
}) {
Image(systemName: "xmark")
.foregroundColor(.white.opacity(0.8))
}
}
.padding()
.background(
ZStack {
Color(red: 0.24, green: 0.52, blue: 0.98)
Color.white.opacity(0.1)
}
)
.clipShape(UnevenRoundedRectangle(topLeadingRadius: 16, topTrailingRadius: 16))
.foregroundColor(.white)
// Game Board - 3x3 grid with dynamic sizing
GeometryReader { geometry in
// Calculate square size to fit perfectly in available width
let squareSize = (geometry.size.width - 2) / 3 // Account for 2 dividers
VStack(spacing: 0) {
ForEach(0..<3) { row in
HStack(spacing: 0) {
ForEach(0..<3) { col in
let index = row * 3 + col
GameSquare(
symbol: game.board[index],
isWinning: false
)
.frame(width: squareSize, height: squareSize)
.onTapGesture {
game.makeMove(at: index)
}
if col < 2 {
Divider()
.frame(width: 1)
.background(Color.gray.opacity(0.3))
}
}
}
.frame(height: squareSize)
if row < 2 {
Divider()
.frame(height: 1)
.background(Color.gray.opacity(0.3))
}
}
}
}
.aspectRatio(1, contentMode: .fit)
.background(.thinMaterial)
// Footer
HStack {
Button(action: {
withAnimation {
game.reset()
}
}) {
HStack {
Image(systemName: "arrow.clockwise")
Text("New Challenge")
}
.font(.system(size: 12))
.foregroundColor(.blue)
}
Spacer()
if game.gameState != .playing {
Button(action: {
withAnimation {
game.showGame = false
}
}) {
Text("VERIFY")
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 8)
.background(
game.gameState == .humanWon ?
Color.green : Color(red: 0.24, green: 0.52, blue: 0.98)
)
.cornerRadius(4)
}
}
}
.padding()
.glassEffect(.clear.interactive(), in: .rect(cornerRadius: 0))
HStack {
Image(systemName: "shield.checkered")
.font(.system(size: 10))
.foregroundColor(.gray)
Text("reTicTacCHA")
.font(.system(size: 10))
.foregroundColor(.gray)
Spacer()
Text("Privacy - Terms")
.font(.system(size: 9))
.foregroundColor(.blue)
Button(action: {
withAnimation {
game.reset()
}
}) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 9))
.foregroundColor(.gray)
}
}
.padding(.horizontal)
.padding(.vertical, 8)
.glassEffect(.clear.interactive(), in: .rect(cornerRadius: 0))
}
.frame(width: 360)
.clipShape(RoundedRectangle(cornerRadius: 16))
.glassEffect(.clear.interactive(), in: .rect(cornerRadius: 16))
.shadow(color: Color.black.opacity(0.25), radius: 20, x: 0, y: 10)
Spacer()
}
}
}
/// Checkbox component with multiple states: empty, loading, success (checkmark), failure (X)
struct CheckboxView: View {
@Binding var isChecked: Bool
var showX: Bool = false // Show red X for failure
var showCheck: Bool = false // Show green checkmark for success
var showProgress: Bool = false // Show loading spinner
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 3)
.stroke(Color.gray.opacity(0.5), lineWidth: 2)
.frame(width: 28, height: 28)
.background(
RoundedRectangle(cornerRadius: 3)
.fill(Color.white)
)
if showProgress {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
.scaleEffect(0.7)
} else if showX {
Image(systemName: "xmark")
.foregroundColor(.red)
.font(.system(size: 18, weight: .bold))
} else if showCheck {
Image(systemName: "checkmark")
.foregroundColor(.green)
.font(.system(size: 18, weight: .bold))
} else if isChecked {
Image(systemName: "checkmark")
.foregroundColor(.green)
.font(.system(size: 18, weight: .bold))
}
}
}
}
/// Individual square in the 3x3 Tic-Tac-Toe grid
struct GameSquare: View {
let symbol: String? // "X", "O", or nil for empty
let isWinning: Bool // Currently unused - could highlight winning squares
var body: some View {
ZStack {
Color.clear
if let symbol = symbol {
if symbol == "X" {
Image(systemName: "xmark")
.font(.system(size: 50, weight: .bold))
.foregroundColor(.blue)
} else {
Image(systemName: "circle")
.font(.system(size: 50, weight: .medium))
.foregroundColor(.red)
}
} else {
Rectangle()
.stroke(Color.white.opacity(0.3), lineWidth: 1)
}
}
.background(Color.white.opacity(0.01))
.contentShape(Rectangle())
}
}
#Preview {
ContentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment