Last active
January 29, 2025 20:01
-
-
Save csells/422a542ebcd0157f2b95d174c7ca707b to your computer and use it in GitHub Desktop.
Flutter snake game with keyboard bindings
This file contains 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:async'; | |
import 'dart:math'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/services.dart'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Snake Game', | |
theme: ThemeData( | |
primarySwatch: Colors.green, | |
), | |
home: const SnakeGame(), | |
); | |
} | |
} | |
class SnakeGame extends StatefulWidget { | |
const SnakeGame({super.key}); | |
@override | |
SnakeGameState createState() => SnakeGameState(); | |
} | |
class SnakeGameState extends State<SnakeGame> { | |
static const int gridSize = 20; | |
static const int gameSpeed = 200; | |
late List<List<bool>> gameBoard; | |
late List<Point<int>> snake; | |
late Point<int> food; | |
Direction direction = Direction.right; | |
bool isGameOver = false; | |
int score = 0; | |
Timer? gameTimer; | |
final FocusNode _focusNode = FocusNode(); | |
@override | |
void initState() { | |
super.initState(); | |
_initializeGame(); | |
_startTimer(); | |
_focusNode.requestFocus(); | |
} | |
void _initializeGame() { | |
gameBoard = List.generate( | |
gridSize, (_) => List.generate(gridSize, (_) => false)); | |
snake = [Point(gridSize ~/ 2, gridSize ~/ 2)]; | |
_placeFood(); | |
isGameOver = false; | |
score = 0; | |
direction = Direction.right; | |
} | |
void _startTimer() { | |
gameTimer = Timer.periodic(const Duration(milliseconds: gameSpeed), (timer) { | |
if (!isGameOver) { | |
_updateGame(); | |
} else { | |
timer.cancel(); | |
} | |
}); | |
} | |
@override | |
void dispose() { | |
gameTimer?.cancel(); | |
_focusNode.dispose(); | |
super.dispose(); | |
} | |
void _placeFood() { | |
Random random = Random(); | |
while (true) { | |
int x = random.nextInt(gridSize); | |
int y = random.nextInt(gridSize); | |
Point<int> potentialFood = Point(x, y); | |
if (!snake.contains(potentialFood)) { | |
food = potentialFood; | |
break; | |
} | |
} | |
} | |
void _updateGame() { | |
setState(() { | |
Point<int> head = snake.first; | |
Point<int> newHead; | |
switch (direction) { | |
case Direction.up: | |
newHead = Point(head.x, (head.y - 1 + gridSize) % gridSize); | |
break; | |
case Direction.down: | |
newHead = Point(head.x, (head.y + 1) % gridSize); | |
break; | |
case Direction.left: | |
newHead = Point((head.x - 1 + gridSize) % gridSize, head.y); | |
break; | |
case Direction.right: | |
newHead = Point((head.x + 1) % gridSize, head.y); | |
break; | |
} | |
if (snake.contains(newHead)) { | |
isGameOver = true; | |
gameTimer?.cancel(); | |
return; | |
} | |
snake.insert(0, newHead); | |
if (newHead == food) { | |
score++; | |
_placeFood(); | |
} else { | |
snake.removeLast(); | |
} | |
}); | |
} | |
void _resetGame() { | |
setState(() { | |
_initializeGame(); | |
_startTimer(); | |
_focusNode.requestFocus(); | |
}); | |
} | |
void _changeDirection(Direction newDirection) { | |
setState(() { | |
if (direction == Direction.up && newDirection == Direction.down) return; | |
if (direction == Direction.down && newDirection == Direction.up) return; | |
if (direction == Direction.left && newDirection == Direction.right) return; | |
if (direction == Direction.right && newDirection == Direction.left) return; | |
direction = newDirection; | |
}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('Snake Game'), | |
centerTitle: true, | |
), | |
body: Focus( | |
focusNode: _focusNode, | |
onKeyEvent: (FocusNode node, KeyEvent event) { | |
if (event is KeyDownEvent) { | |
if (event.logicalKey == LogicalKeyboardKey.arrowUp) { | |
if(direction != Direction.down) { | |
direction = Direction.up; | |
} | |
return KeyEventResult.handled; | |
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { | |
if(direction != Direction.up){ | |
direction = Direction.down; | |
} | |
return KeyEventResult.handled; | |
} else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { | |
if(direction != Direction.right){ | |
direction = Direction.left; | |
} | |
return KeyEventResult.handled; | |
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { | |
if(direction != Direction.left){ | |
direction = Direction.right; | |
} | |
return KeyEventResult.handled; | |
} | |
} | |
return KeyEventResult.ignored; | |
}, | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Expanded( | |
child: AspectRatio( | |
aspectRatio: 1.0, | |
child: Container( | |
padding: const EdgeInsets.all(10.0), | |
decoration: BoxDecoration( | |
border: Border.all(color: Colors.black, width: 2.0) | |
), | |
child: _buildGameBoard(), | |
) | |
) | |
), | |
Padding( | |
padding: const EdgeInsets.only(bottom: 20.0), | |
child: Text("Score: $score", style: const TextStyle(fontSize: 24),) | |
), | |
if (isGameOver) | |
ElevatedButton( | |
onPressed: _resetGame, | |
child: const Text('Play Again'), | |
), | |
Padding( | |
padding: const EdgeInsets.only(top: 20.0), | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Column( | |
children: [ | |
IconButton( | |
icon: const Icon(Icons.arrow_upward), | |
onPressed: () => _changeDirection(Direction.up), | |
), | |
Row( | |
children: [ | |
IconButton( | |
icon: const Icon(Icons.arrow_back), | |
onPressed: () => _changeDirection(Direction.left), | |
), | |
IconButton( | |
icon: const Icon(Icons.arrow_downward), | |
onPressed: () => _changeDirection(Direction.down), | |
), | |
IconButton( | |
icon: const Icon(Icons.arrow_forward), | |
onPressed: () => _changeDirection(Direction.right), | |
), | |
] | |
) | |
], | |
), | |
], | |
), | |
), | |
], | |
), | |
), | |
); | |
} | |
Widget _buildGameBoard() { | |
return GridView.builder( | |
physics: const NeverScrollableScrollPhysics(), | |
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( | |
crossAxisCount: gridSize, | |
), | |
itemCount: gridSize * gridSize, | |
itemBuilder: (context, index) { | |
int row = index ~/ gridSize; | |
int col = index % gridSize; | |
bool isSnakeCell = snake.contains(Point(col, row)); | |
bool isFoodCell = (food.x == col && food.y == row); | |
return Container( | |
margin: const EdgeInsets.all(1), | |
decoration: BoxDecoration( | |
color: isSnakeCell | |
? Colors.green | |
: isFoodCell | |
? Colors.red | |
: Colors.grey[200], | |
shape: BoxShape.rectangle, | |
), | |
); | |
}, | |
); | |
} | |
} | |
enum Direction { up, down, left, right } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment