import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Super Tic-Tac-Toe', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Local Tic Tac Toe'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { TTCState turn = TTCState.x; late TTCGame game; String get turnText => switch (turn) { TTCState.empty => "", TTCState.x => "X", TTCState.o => "O", }; void onTurnEnd() { setState(() { turn = switch (turn) { TTCState.x => TTCState.o, TTCState.o => TTCState.x, _ => TTCState.x }; }); } @override void initState() { super.initState(); game = TTCGame( turn: turn, onClick: onTurnEnd, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), backgroundColor: Theme.of(context).colorScheme.inversePrimary, ), body: Padding( padding: const EdgeInsets.all(10), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Spacer(flex: 5), Center( child: Text( "$turnText's turn", style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold), )), const Spacer(flex: 1), TTCGame( turn: turn, onClick: onTurnEnd, ), const Spacer(flex: 5), ], )), ); } } enum TTCState { empty, x, o } /// Board of a single game of tic tac toe class TTCGame extends StatefulWidget { const TTCGame({ super.key, required this.turn, this.data, this.onClick, }); final TTCState turn; final List? data; /// hook into acction of tapping on square final Function? onClick; @override State createState() => _TTCGameState(); } class _TTCGameState extends State { late TTCState turn; late List data; void setCellState(int index) { switch (data[index]) { case TTCState.empty: setState(() { data[index] = turn; turn = switch (turn) { TTCState.x => TTCState.o, TTCState.o => TTCState.x, TTCState.empty => TTCState.empty, }; }); widget.onClick?.call(); notifyWin(); break; default: ScaffoldMessenger.of(context) ..clearSnackBars() ..showSnackBar(const SnackBar( content: Text("Invalid Choice: cell already has value"))); } } Iterable getRow(int index) => data.getRange(index, index + 3); Iterable getCol(int index) => [ data[index], data[index + 3], data[index + 3 * 2], ]; Iterable getCross(int index) { if (index == 0) { return [data[0], data[4], data[8]]; } return [data[3], data[4], data[6]]; } TTCState _checkRow(Iterable row) => row.reduce((value, element) => (element == TTCState.empty || value == TTCState.empty || element != value) ? TTCState.empty : value); TTCState _checkBoard(Iterable rowValues) => rowValues.reduce((value, element) { if (value != TTCState.empty) { return value; } if (element != TTCState.empty) { return element; } return TTCState.empty; }); TTCState _checkWin() { Iterable rows = Iterable.generate(3, (i) => _checkRow(getRow(i))); Iterable cols = Iterable.generate(3, (i) => _checkRow(getCol(i))); Iterable crosses = Iterable.generate(2, (i) => _checkRow(getCross(i))); return _checkBoard([...rows, ...cols, ...crosses]); } void notifyWin() { TTCState state = _checkWin(); if (state != TTCState.empty) { ScaffoldMessenger.of(context) ..clearSnackBars() ..showSnackBar( SnackBar(content: Text("${state.name.toUpperCase()} wins"))); } } Border _genCellBorder( int index, { BorderSide borderSide = const BorderSide(), }) { return Border( top: index < 3 ? BorderSide.none : borderSide, bottom: index > 5 ? BorderSide.none : borderSide, left: [0, 3, 6].contains(index) ? BorderSide.none : borderSide, right: [2, 5, 8].contains(index) ? BorderSide.none : borderSide, ); } Widget _genCell(int index, TTCState state) => Container( decoration: BoxDecoration(border: _genCellBorder(index)), child: TTCCell( state: state, stateSetCallback: () { setCellState(index); }, )); @override void initState() { super.initState(); turn = widget.turn; data = widget.data ?? [ TTCState.empty, TTCState.empty, TTCState.empty, TTCState.empty, TTCState.empty, TTCState.empty, TTCState.empty, TTCState.empty, TTCState.empty, ]; } @override Widget build(BuildContext context) { List cells = []; for (final (index, state) in data.indexed) { cells.add(_genCell(index, state)); } return GridView.count( crossAxisCount: 3, shrinkWrap: true, children: cells, ); } } class TTCCell extends StatelessWidget { const TTCCell({ super.key, required this.state, this.stateSetCallback, this.textStyle, }); final TTCState state; final VoidCallback? stateSetCallback; final TextStyle? textStyle; String get text => switch (state) { TTCState.empty => "", TTCState.x => "X", TTCState.o => "O", }; @override Widget build(BuildContext context) => InkWell( onTap: stateSetCallback, child: Center( child: Text(text, style: textStyle), )); }