The SnakeModel
represents the game’s data, including the grid dimensions, snake position, food position, and movement dire
SnakeModel
The SnakeModel
represents the game’s data, including the grid dimensions, snake position, food position, and movement direction.
import 'dart:math';
import 'dart:ui';
class SnakeModel {
final int rows; // Number of rows in the grid
final int columns; // Number of columns in the grid
SnakeModel({required this.rows, required this.columns});
List<Offset> snake = [Offset(10, 10)]; // Snake's initial position
Offset food = Offset(5, 5); // Initial position of the food
String direction = 'up'; // Snake's initial movement direction
/// Generates a new random position for the food that doesn't overlap with the snake.
Offset generateFood() {
Random random = Random();
Offset newFood;
do {
newFood = Offset(
random.nextInt(columns).toDouble(),
random.nextInt(rows).toDouble(),
);
} while (snake.contains(newFood)); // Ensure food doesn't spawn on the snake
return newFood;
}
}
SnakeViewModel
The SnakeViewModel
handles all game logic. It updates the snake’s position, checks for collisions, and triggers UI updates.
import 'dart:async';
import 'dart:ui';
import 'model.dart';
class SnakeViewModel {
final SnakeModel model; // Reference to the model holding game data
final void Function() onGameUpdate; // Callback to notify the UI of changes
bool isGameOver = false; // Tracks if the game is over
Timer? timer; // Timer for periodic updates (game loop)
SnakeViewModel(this.model, this.onGameUpdate);
/// Starts the game by resetting the state and beginning the game loop
void startGame() {
isGameOver = false; // Reset game-over state
model.snake = [Offset(10, 10)]; // Reset snake's position
model.food = model.generateFood(); // Generate new food position
model.direction = 'up'; // Reset direction to 'up'
timer?.cancel(); // Cancel any existing timer
timer =
Timer.periodic(const Duration(milliseconds: 500), (_) => updateGame()); // Start game loop
}
/// Handles the game loop, updating the snake's position and checking for collisions
void updateGame() {
if (isGameOver) return;
Offset newHead = getNextPosition(); // Calculate the snake's next head position
if (checkCollision(newHead)) {
// Check for collisions
isGameOver = true;
timer?.cancel(); // Stop the game loop
} else {
model.snake.insert(0, newHead); // Add the new head to the snake
if (newHead == model.food) {
// Check if the snake eats the food
model.food = model.generateFood(); // Generate a new food position
} else {
model.snake.removeLast(); // Remove the tail to maintain the same length
}
}
onGameUpdate(); // Notify the UI to update
}
/// Calculates the snake's next head position based on the current direction
Offset getNextPosition() {
Offset head = model.snake.first;
switch (model.direction) {
case 'up':
return Offset(head.dx, head.dy - 1);
case 'down':
return Offset(head.dx, head.dy + 1);
case 'left':
return Offset(head.dx - 1, head.dy);
case 'right':
return Offset(head.dx + 1, head.dy);
default:
return head;
}
}
/// Checks if the snake collides with itself or the walls
bool checkCollision(Offset position) {
// Check if the position is out of bounds
if (position.dx < 0 ||
position.dx >= model.columns ||
position.dy < 0 ||
position.dy >= model.rows) {
return true;
}
// Check if the position overlaps with the snake
if (model.snake.contains(position)) {
return true;
}
return false;
}
/// Changes the snake's direction, avoiding reversing into itself
void changeDirection(String newDirection) {
// Prevent reversing direction
if ((newDirection == 'up' && model.direction != 'down') ||
(newDirection == 'down' && model.direction != 'up') ||
(newDirection == 'left' && model.direction != 'right') ||
(newDirection == 'right' && model.direction != 'left')) {
model.direction = newDirection;
}
}
}
SnakeView
The SnakeView
is the user interface. It displays the game board, the snake, and the food, and handles user input through buttons.
import 'package:flutter/material.dart';
import 'model.dart';
import 'modelview.dart';
class SnakeView extends StatefulWidget {
@override
_SnakeViewState createState() => _SnakeViewState();
}
class _SnakeViewState extends State<SnakeView> {
late SnakeModel _model; // Model holding game data
late SnakeViewModel _viewModel; // ViewModel managing game logic
@override
void initState() {
super.initState();
_model = SnakeModel(rows: 20, columns: 20); // Create a 20x20 grid
_viewModel = SnakeViewModel(_model, () => setState(() {})); // Initialize ViewModel with UI callback
_viewModel.startGame(); // Start the game
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black, // Set background color to black
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Game Board
Expanded(
child: GridView.builder(
physics: NeverScrollableScrollPhysics(), // Disable scrolling
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _model.columns, // Define number of columns
),
itemCount: _model.rows * _model.columns, // Total grid cells
itemBuilder: (context, index) {
int x = index % _model.columns; // Calculate x-coordinate
int y = index ~/ _model.columns; // Calculate y-coordinate
Offset position = Offset(x.toDouble(), y.toDouble());
bool isSnake = _model.snake.contains(position); // Check if cell is part of the snake
bool isFood = position == _model.food; // Check if cell contains food
return Container(
margin: EdgeInsets.all(1), // Add spacing between cells
decoration: BoxDecoration(
color: isSnake
? Colors.green // Snake color
: isFood
? Colors.red // Food color
: Colors.grey[800], // Default cell color
shape: BoxShape.rectangle, // Use rectangular cells
),
);
},
),
),
// Control Buttons
Column(
children: [
// Game Over Message
if (_viewModel.isGameOver)
Text(
'Game Over!', // Display message when game is over
style: TextStyle(color: Colors.red, fontSize: 24),
),
// Directional Controls
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => _viewModel.changeDirection('up'), // Move up
child: Icon(Icons.arrow_upward),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => _viewModel.changeDirection('left'), // Move left
child: Icon(Icons.arrow_back),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () => _viewModel.changeDirection('right'), // Move right
child: Icon(Icons.arrow_forward),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => _viewModel.changeDirection('down'), // Move down
child: Icon(Icons.arrow_downward),
),
],
),
],
),
),
// Restart Button
ElevatedButton(
onPressed: _viewModel.startGame, // Restart the game
child: Text('Restart'),
),
],
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'view.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: SnakeView());
}
}