Created
December 4, 2025 11:26
-
-
Save callmephil/91bdbddb8551c3c8f5257f85f17eb9d0 to your computer and use it in GitHub Desktop.
Infinite Tic - Tac - Toe (Deep Seek)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:math'; | |
| import 'package:flutter/material.dart'; | |
| // ============= CONSTANTS ============= | |
| class _AppColors { | |
| static const Color primary = Color(0xFFFFD700); | |
| static const Color scaffoldBackground = Color(0xFF1A1A1A); | |
| static const Color deviceColor = Color(0xFFFFCC00); | |
| static const Color boardBackground = Color(0xFF000000); | |
| static const Color cellBorder = Color(0xFF2A2A2A); | |
| static const Color xColor = Colors.redAccent; | |
| static const Color oColor = Colors.blueAccent; | |
| static const Color statusBorderActiveX = Colors.redAccent; | |
| static const Color statusBorderActiveO = Colors.blueAccent; | |
| static const Color statusBorderWin = Colors.green; | |
| static const Color statusBackground = Colors.black54; | |
| static const Color restartButtonBg = Color(0xFFFFCC00); | |
| static const Color restartButtonText = Colors.black; | |
| static const Color aiThinking = Colors.orangeAccent; | |
| } | |
| class _AppSizes { | |
| static const double boardSize = 320.0; | |
| static const double boardPadding = 16.0; | |
| static const double boardBorderRadius = 40.0; | |
| static const double innerBoardBorderRadius = 24.0; | |
| static const double cellIconSizeX = 60.0; | |
| static const double cellIconSizeO = 50.0; | |
| static const double statusFontSize = 24.0; | |
| static const double statusBorderWidth = 2.0; | |
| static const double statusHorizontalPadding = 24.0; | |
| static const double statusVerticalPadding = 12.0; | |
| static const double statusBorderRadius = 12.0; | |
| static const double buttonHorizontalPadding = 32.0; | |
| static const double buttonVerticalPadding = 16.0; | |
| static const Duration winAnimationDuration = Duration(milliseconds: 500); | |
| static const Duration aiMoveDelay = Duration(milliseconds: 600); | |
| static const double boardShadowBlur = 20.0; | |
| static const double boardShadowSpread = 5.0; | |
| } | |
| // ============= GAME LOGIC ============= | |
| enum _Player { x, o } | |
| enum _GameMode { twoPlayer, vsAI } | |
| class _GameState { | |
| final List<int> xMoves; | |
| final List<int> oMoves; | |
| final bool isXTurn; | |
| final _Player? winner; | |
| final List<int> winningLine; | |
| final _GameMode gameMode; | |
| final bool isAiThinking; | |
| const _GameState({ | |
| this.xMoves = const [], | |
| this.oMoves = const [], | |
| this.isXTurn = true, | |
| this.winner, | |
| this.winningLine = const [], | |
| this.gameMode = _GameMode.twoPlayer, | |
| this.isAiThinking = false, | |
| }); | |
| _GameState copyWith({ | |
| List<int>? xMoves, | |
| List<int>? oMoves, | |
| bool? isXTurn, | |
| _Player? winner, | |
| List<int>? winningLine, | |
| _GameMode? gameMode, | |
| bool? isAiThinking, | |
| }) { | |
| return _GameState( | |
| xMoves: xMoves ?? this.xMoves, | |
| oMoves: oMoves ?? this.oMoves, | |
| isXTurn: isXTurn ?? this.isXTurn, | |
| winner: winner ?? this.winner, | |
| winningLine: winningLine ?? this.winningLine, | |
| gameMode: gameMode ?? this.gameMode, | |
| isAiThinking: isAiThinking ?? this.isAiThinking, | |
| ); | |
| } | |
| bool get isGameOver => winner != null; | |
| bool get isAiTurn => gameMode == _GameMode.vsAI && !isXTurn && !isGameOver; | |
| } | |
| class _GameLogic { | |
| static const List<List<int>> 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 | |
| ]; | |
| _GameState makeMove(_GameState currentState, int index, {bool isAiMove = false}) { | |
| if (currentState.isGameOver || _isCellOccupied(currentState, index)) { | |
| return currentState; | |
| } | |
| final newXMoves = List<int>.from(currentState.xMoves); | |
| final newOMoves = List<int>.from(currentState.oMoves); | |
| if (currentState.isXTurn) { | |
| _addMoveWithFIFO(newXMoves, index); | |
| } else { | |
| _addMoveWithFIFO(newOMoves, index); | |
| } | |
| final winner = _checkWinner(newXMoves, newOMoves); | |
| final winningLine = winner != null | |
| ? _getWinningLine(currentState.isXTurn ? newXMoves : newOMoves) | |
| : <int>[]; // Fix: Explicitly type the empty list to List<int> | |
| return currentState.copyWith( | |
| xMoves: newXMoves, | |
| oMoves: newOMoves, | |
| isXTurn: winner == null ? !currentState.isXTurn : currentState.isXTurn, | |
| winner: winner, | |
| winningLine: winningLine, | |
| isAiThinking: false, | |
| ); | |
| } | |
| int? getAiMove(_GameState state) { | |
| final emptyCells = List.generate(9, (index) => index) | |
| .where((index) => !_isCellOccupied(state, index)) | |
| .toList(); | |
| if (emptyCells.isEmpty) return null; | |
| // Try to block opponent's win | |
| final blockingMove = _findBlockingMove(state); | |
| if (blockingMove != null) return blockingMove; | |
| // Try to win if possible | |
| final winningMove = _findWinningMove(state); | |
| if (winningMove != null) return winningMove; | |
| // Prefer center and corners | |
| final centerAndCorners = [4, 0, 2, 6, 8]; | |
| for (final move in centerAndCorners) { | |
| if (emptyCells.contains(move)) return move; | |
| } | |
| // Otherwise random | |
| return emptyCells[Random().nextInt(emptyCells.length)]; | |
| } | |
| int? _findBlockingMove(_GameState state) { | |
| final opponentMoves = state.isXTurn ? state.oMoves : state.xMoves; | |
| for (final pattern in winPatterns) { | |
| final opponentInPattern = pattern.where((cell) => opponentMoves.contains(cell)).toList(); | |
| if (opponentInPattern.length == 2) { | |
| final emptyCell = pattern.firstWhere( | |
| (cell) => !_isCellOccupied(state, cell), | |
| orElse: () => -1, | |
| ); | |
| if (emptyCell != -1) return emptyCell; | |
| } | |
| } | |
| return null; | |
| } | |
| int? _findWinningMove(_GameState state) { | |
| final currentMoves = state.isXTurn ? state.xMoves : state.oMoves; | |
| for (final pattern in winPatterns) { | |
| final myInPattern = pattern.where((cell) => currentMoves.contains(cell)).toList(); | |
| if (myInPattern.length == 2) { | |
| final emptyCell = pattern.firstWhere( | |
| (cell) => !_isCellOccupied(state, cell), | |
| orElse: () => -1, | |
| ); | |
| if (emptyCell != -1) return emptyCell; | |
| } | |
| } | |
| return null; | |
| } | |
| bool _isCellOccupied(_GameState state, int index) { | |
| return state.xMoves.contains(index) || state.oMoves.contains(index); | |
| } | |
| void _addMoveWithFIFO(List<int> moves, int index) { | |
| moves.add(index); | |
| if (moves.length > 3) { | |
| moves.removeAt(0); | |
| } | |
| } | |
| _Player? _checkWinner(List<int> xMoves, List<int> oMoves) { | |
| if (_hasWinningPattern(xMoves)) return _Player.x; | |
| if (_hasWinningPattern(oMoves)) return _Player.o; | |
| return null; | |
| } | |
| bool _hasWinningPattern(List<int> moves) { | |
| if (moves.length < 3) return false; | |
| return winPatterns.any((pattern) => | |
| pattern.every((cell) => moves.contains(cell)) | |
| ); | |
| } | |
| List<int> _getWinningLine(List<int> moves) { | |
| return winPatterns.firstWhere( | |
| (pattern) => pattern.every((cell) => moves.contains(cell)), | |
| orElse: () => [], | |
| ); | |
| } | |
| _GameState reset(_GameState currentState) { | |
| return _GameState( | |
| gameMode: currentState.gameMode, | |
| isXTurn: true, | |
| ); | |
| } | |
| } | |
| // ============= UI COMPONENTS ============= | |
| class _BoardCell extends StatelessWidget { | |
| final bool isX; | |
| final bool isO; | |
| final bool isWinningCell; | |
| final bool isOldestPiece; | |
| final bool isGameActive; | |
| final Animation<double>? pulseAnimation; | |
| final VoidCallback onTap; | |
| final bool isAiThinking; | |
| final int position; | |
| const _BoardCell({ | |
| required this.isX, | |
| required this.isO, | |
| required this.isWinningCell, | |
| required this.isOldestPiece, | |
| required this.isGameActive, | |
| this.pulseAnimation, | |
| required this.onTap, | |
| required this.isAiThinking, | |
| required this.position, | |
| }); | |
| @override | |
| Widget build(BuildContext context) { | |
| return GestureDetector( | |
| onTap: onTap, | |
| excludeFromSemantics: false, | |
| child: Semantics( | |
| button: true, | |
| label: _getSemanticLabel(), | |
| child: Container( | |
| decoration: BoxDecoration( | |
| border: Border.all(color: _AppColors.cellBorder, width: 1), | |
| color: isAiThinking ? _AppColors.aiThinking.withOpacity(0.1) : null, | |
| ), | |
| child: Center( | |
| child: _buildContent(), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _buildContent() { | |
| if (isX || isO) { | |
| return _AnimatedGamePiece( | |
| isX: isX, | |
| isWinningCell: isWinningCell, | |
| isOldestPiece: isOldestPiece, | |
| isGameActive: isGameActive, | |
| pulseAnimation: pulseAnimation, | |
| ); | |
| } | |
| return const SizedBox(); | |
| } | |
| String _getSemanticLabel() { | |
| if (isX) return 'X at ${_getPositionText()}'; | |
| if (isO) return 'O at ${_getPositionText()}'; | |
| return 'Empty cell at ${_getPositionText()}'; | |
| } | |
| String _getPositionText() { | |
| final row = (position ~/ 3) + 1; | |
| final col = (position % 3) + 1; | |
| return 'row $row, column $col'; | |
| } | |
| } | |
| class _AnimatedGamePiece extends StatelessWidget { | |
| final bool isX; | |
| final bool isWinningCell; | |
| final bool isOldestPiece; | |
| final bool isGameActive; | |
| final Animation<double>? pulseAnimation; | |
| const _AnimatedGamePiece({ | |
| required this.isX, | |
| required this.isWinningCell, | |
| required this.isOldestPiece, | |
| required this.isGameActive, | |
| this.pulseAnimation, | |
| }); | |
| @override | |
| Widget build(BuildContext context) { | |
| return AnimatedBuilder( | |
| animation: pulseAnimation ?? AlwaysStoppedAnimation(1.0), | |
| builder: (context, child) { | |
| double opacity = 1.0; | |
| if (isWinningCell && pulseAnimation != null) { | |
| opacity = 0.2 + (pulseAnimation!.value * 0.8); | |
| } else if (isOldestPiece && isGameActive) { | |
| opacity = 0.3; | |
| } | |
| return Opacity( | |
| opacity: opacity, | |
| child: child, | |
| ); | |
| }, | |
| child: isX | |
| ? const Icon( | |
| Icons.close, | |
| size: _AppSizes.cellIconSizeX, | |
| color: _AppColors.xColor, | |
| ) | |
| : const Icon( | |
| Icons.circle_outlined, | |
| size: _AppSizes.cellIconSizeO, | |
| color: _AppColors.oColor, | |
| ), | |
| ); | |
| } | |
| } | |
| class _GameBoard extends StatelessWidget { | |
| final _GameState state; | |
| final Animation<double>? pulseAnimation; | |
| final ValueChanged<int> onCellTap; | |
| final List<int>? aiThinkingCells; | |
| const _GameBoard({ | |
| required this.state, | |
| this.pulseAnimation, | |
| required this.onCellTap, | |
| this.aiThinkingCells, | |
| }); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Container( | |
| width: _AppSizes.boardSize, | |
| height: _AppSizes.boardSize, | |
| padding: const EdgeInsets.all(_AppSizes.boardPadding), | |
| decoration: BoxDecoration( | |
| color: _AppColors.deviceColor, | |
| borderRadius: BorderRadius.circular(_AppSizes.boardBorderRadius), | |
| boxShadow: [ | |
| BoxShadow( | |
| color: _AppColors.deviceColor.withOpacity(0.3), | |
| blurRadius: _AppSizes.boardShadowBlur, | |
| spreadRadius: _AppSizes.boardShadowSpread, | |
| ), | |
| ], | |
| ), | |
| child: Container( | |
| decoration: BoxDecoration( | |
| color: _AppColors.boardBackground, | |
| borderRadius: BorderRadius.circular(_AppSizes.innerBoardBorderRadius), | |
| ), | |
| child: ClipRRect( | |
| borderRadius: BorderRadius.circular(_AppSizes.innerBoardBorderRadius), | |
| child: GridView.builder( | |
| physics: const NeverScrollableScrollPhysics(), | |
| gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( | |
| crossAxisCount: 3, | |
| childAspectRatio: 1.0, | |
| ), | |
| itemCount: 9, | |
| itemBuilder: (context, index) => _buildCell(index), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _buildCell(int index) { | |
| final isOldestX = state.xMoves.length == 3 && state.xMoves.first == index; | |
| final isOldestO = state.oMoves.length == 3 && state.oMoves.first == index; | |
| return _BoardCell( | |
| isX: state.xMoves.contains(index), | |
| isO: state.oMoves.contains(index), | |
| isWinningCell: state.winningLine.contains(index), | |
| isOldestPiece: isOldestX || isOldestO, | |
| isGameActive: !state.isGameOver, | |
| pulseAnimation: pulseAnimation, | |
| onTap: () => onCellTap(index), | |
| isAiThinking: aiThinkingCells?.contains(index) ?? false, | |
| position: index, | |
| ); | |
| } | |
| } | |
| // ============= MAIN APP ============= | |
| void main() { | |
| runApp(const InfinityTicTacToeApp()); | |
| } | |
| class InfinityTicTacToeApp extends StatelessWidget { | |
| const InfinityTicTacToeApp({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| title: 'Infinity Tic-Tac-Toe', | |
| debugShowCheckedModeBanner: false, | |
| theme: ThemeData( | |
| brightness: Brightness.dark, | |
| scaffoldBackgroundColor: _AppColors.scaffoldBackground, | |
| primaryColor: _AppColors.primary, | |
| useMaterial3: true, | |
| colorScheme: ColorScheme.fromSeed( | |
| seedColor: _AppColors.primary, | |
| brightness: Brightness.dark, | |
| ), | |
| ), | |
| home: const GameScreen(), | |
| ); | |
| } | |
| } | |
| class GameScreen extends StatefulWidget { | |
| const GameScreen({super.key}); | |
| @override | |
| State<GameScreen> createState() => _GameScreenState(); | |
| } | |
| class _GameScreenState extends State<GameScreen> with SingleTickerProviderStateMixin { | |
| late AnimationController _flashController; | |
| late Animation<double> _pulseAnimation; | |
| final _GameLogic _gameLogic = _GameLogic(); | |
| _GameState _state = const _GameState(); | |
| List<int> _aiThinkingCells = []; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _flashController = AnimationController( | |
| vsync: this, | |
| duration: _AppSizes.winAnimationDuration, | |
| ); | |
| _pulseAnimation = Tween<double>(begin: 0.2, end: 1.0).animate( | |
| CurvedAnimation(parent: _flashController, curve: Curves.easeInOut), | |
| ); | |
| } | |
| @override | |
| void dispose() { | |
| _flashController.dispose(); | |
| super.dispose(); | |
| } | |
| void _handleCellTap(int index) { | |
| if (_state.isGameOver || _state.isAiThinking) return; | |
| final newState = _gameLogic.makeMove(_state, index); | |
| _updateState(newState); | |
| if (newState.isAiTurn && !newState.isGameOver) { | |
| _makeAiMove(); | |
| } | |
| } | |
| Future<void> _makeAiMove() async { | |
| setState(() { | |
| _state = _state.copyWith(isAiThinking: true); | |
| _aiThinkingCells = List.generate(9, (i) => i) | |
| .where((i) => !_state.xMoves.contains(i) && !_state.oMoves.contains(i)) | |
| .toList(); | |
| }); | |
| await Future.delayed(_AppSizes.aiMoveDelay); | |
| final aiMove = _gameLogic.getAiMove(_state); | |
| if (aiMove != null) { | |
| final newState = _gameLogic.makeMove(_state, aiMove, isAiMove: true); | |
| _updateState(newState); | |
| } | |
| setState(() { | |
| _aiThinkingCells = []; | |
| }); | |
| } | |
| void _updateState(_GameState newState) { | |
| setState(() { | |
| _state = newState; | |
| }); | |
| if (newState.isGameOver && newState.winner != null) { | |
| _flashController.repeat(reverse: true); | |
| } else { | |
| _flashController.stop(); | |
| _flashController.reset(); | |
| } | |
| } | |
| void _resetGame() { | |
| final newState = _gameLogic.reset(_state); | |
| _updateState(newState); | |
| } | |
| void _toggleGameMode() { | |
| final newMode = _state.gameMode == _GameMode.twoPlayer | |
| ? _GameMode.vsAI | |
| : _GameMode.twoPlayer; | |
| _updateState(_state.copyWith( | |
| gameMode: newMode, | |
| isAiThinking: false, | |
| )); | |
| if (newMode == _GameMode.vsAI && !_state.isXTurn && !_state.isGameOver) { | |
| _makeAiMove(); | |
| } | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| backgroundColor: Colors.grey[900], | |
| appBar: AppBar( | |
| title: const Text("Infinity Tic-Tac-Toe"), | |
| backgroundColor: Colors.transparent, | |
| elevation: 0, | |
| centerTitle: true, | |
| actions: [ | |
| IconButton( | |
| icon: Icon( | |
| _state.gameMode == _GameMode.vsAI | |
| ? Icons.smart_toy | |
| : Icons.people, | |
| color: _AppColors.primary, | |
| ), | |
| onPressed: _toggleGameMode, | |
| tooltip: _state.gameMode == _GameMode.vsAI | |
| ? 'Switch to Two Player' | |
| : 'Play vs AI', | |
| ), | |
| ], | |
| ), | |
| body: Center( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| // Status Display | |
| Container( | |
| padding: const EdgeInsets.symmetric( | |
| horizontal: _AppSizes.statusHorizontalPadding, | |
| vertical: _AppSizes.statusVerticalPadding, | |
| ), | |
| decoration: BoxDecoration( | |
| color: _AppColors.statusBackground, | |
| borderRadius: BorderRadius.circular(_AppSizes.statusBorderRadius), | |
| border: Border.all( | |
| color: _state.winner != null | |
| ? _AppColors.statusBorderWin | |
| : (_state.isXTurn ? _AppColors.statusBorderActiveX : _AppColors.statusBorderActiveO), | |
| width: _AppSizes.statusBorderWidth, | |
| ), | |
| ), | |
| child: _buildStatusText(), | |
| ), | |
| const SizedBox(height: 30), | |
| // Game Mode Indicator | |
| if (_state.gameMode == _GameMode.vsAI) ...[ | |
| Container( | |
| padding: const EdgeInsets.all(8), | |
| decoration: BoxDecoration( | |
| color: Colors.black38, | |
| borderRadius: BorderRadius.circular(8), | |
| ), | |
| child: Row( | |
| mainAxisSize: MainAxisSize.min, | |
| children: [ | |
| Icon( | |
| Icons.smart_toy, | |
| color: _AppColors.aiThinking, | |
| size: 16, | |
| ), | |
| const SizedBox(width: 8), | |
| Text( | |
| 'VS AI', | |
| style: TextStyle( | |
| color: _AppColors.aiThinking, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| const SizedBox(height: 10), | |
| ], | |
| // Game Board | |
| _GameBoard( | |
| state: _state, | |
| pulseAnimation: _pulseAnimation, | |
| onCellTap: _handleCellTap, | |
| aiThinkingCells: _aiThinkingCells, | |
| ), | |
| const SizedBox(height: 40), | |
| // Reset Button | |
| ElevatedButton.icon( | |
| onPressed: _resetGame, | |
| style: ElevatedButton.styleFrom( | |
| backgroundColor: _AppColors.restartButtonBg, | |
| foregroundColor: _AppColors.restartButtonText, | |
| padding: const EdgeInsets.symmetric( | |
| horizontal: _AppSizes.buttonHorizontalPadding, | |
| vertical: _AppSizes.buttonVerticalPadding, | |
| ), | |
| ), | |
| icon: const Icon(Icons.refresh), | |
| label: const Text("RESTART GAME", style: TextStyle(fontWeight: FontWeight.bold)), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _buildStatusText() { | |
| if (_state.isAiThinking) { | |
| return const Row( | |
| mainAxisSize: MainAxisSize.min, | |
| children: [ | |
| SizedBox( | |
| width: 20, | |
| height: 20, | |
| child: CircularProgressIndicator( | |
| strokeWidth: 2, | |
| color: _AppColors.aiThinking, | |
| ), | |
| ), | |
| SizedBox(width: 12), | |
| Text( | |
| "AI Thinking...", | |
| style: TextStyle( | |
| fontSize: _AppSizes.statusFontSize, | |
| fontWeight: FontWeight.bold, | |
| color: _AppColors.aiThinking, | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| final text = _state.winner != null | |
| ? "Winner: ${_state.winner == _Player.x ? 'X' : 'O'}!" | |
| : "Turn: ${_state.isXTurn ? 'X (Red)' : 'O (Blue)'}"; | |
| return Text( | |
| text, | |
| style: const TextStyle( | |
| fontSize: _AppSizes.statusFontSize, | |
| fontWeight: FontWeight.bold, | |
| color: Colors.white, | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment