Skip to content

Instantly share code, notes, and snippets.

@callmephil
Created December 4, 2025 11:26
Show Gist options
  • Select an option

  • Save callmephil/91bdbddb8551c3c8f5257f85f17eb9d0 to your computer and use it in GitHub Desktop.

Select an option

Save callmephil/91bdbddb8551c3c8f5257f85f17eb9d0 to your computer and use it in GitHub Desktop.
Infinite Tic - Tac - Toe (Deep Seek)
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