- Introduction
- Features
- Game Rules
- Technology Stack
- Architecture
- Database Structure
- Installation
- Usage
- Code Structure
- Implementation Details
- Game Logic & Flow
- State Management
- Firebase Integration
- UI/UX Design
- Performance Optimizations
- Testing
- Deployment
- Future Enhancements
- Contributing
- License
- Contact
- Acknowledgements
Mafia Party Game is a digital implementation of the classic social deduction party game "Mafia" (also known as "Werewolf"). It's designed for mobile platforms using Flutter and Firebase, allowing friends to play together remotely. The game features real-time multiplayer functionality, role assignment, day/night cycles, voting mechanisms, and chat communication.
This project demonstrates advanced Flutter development techniques, Firebase integration, real-time database usage, state management with providers and BLoC, and creating an engaging multiplayer experience.
- Real-time multiplayer gameplay - Play with friends from anywhere
- Room-based system - Create and join game rooms with unique codes
- Role assignment - Random distribution of special roles (Mafia, Detective, Doctor)
- Day/Night cycles - Alternating game phases for discussion and secret actions
- Voting system - Democratic elimination of suspected Mafia members
- In-game chat - Communicate with other players during discussion phases
- Role-specific actions - Special abilities for Mafia, Detective, and Doctor roles
- Customizable game settings - Adjust number of roles and phase durations
- Authentication system - User accounts and profiles
- Responsive UI - Works on various device sizes
- Dark theme - Sleek, modern interface with dark aesthetics
Mafia is a social deduction game where players are secretly assigned roles: Villagers, Mafia, Detective, and Doctor. The game alternates between day and night phases. During the night, the Mafia chooses someone to eliminate, while the Doctor can save someone, and the Detective can investigate a player's identity. During the day, all players discuss and vote to eliminate a suspected Mafia member.
- Villagers: Regular citizens trying to identify and eliminate the Mafia
- Mafia: Know each other's identities and secretly eliminate one player each night
- Detective: Can investigate one player each night to learn their role
- Doctor: Can protect one player each night from being eliminated
- Waiting Phase: Players join the room before the game starts
- Night Phase: Special roles perform their actions secretly
- Day Phase: All players discuss to identify the Mafia
- Voting Phase: Players vote to eliminate a suspected Mafia member
- Game Over: When either all Mafia are eliminated (Villagers win) or Mafia outnumber Villagers (Mafia win)
- Villagers win when all Mafia members are eliminated
- Mafia wins when they equal or outnumber the Villagers
- Flutter (≥3.0.0) - Google's UI toolkit for building natively compiled applications
- Firebase
- Cloud Firestore - For real-time game state synchronization
- Firebase Authentication - For user management
- Firebase Core - Base Firebase functionality
- Provider - For dependency injection and lightweight state management
- Flutter BLoC - For more complex state management scenarios
- firebase_core (v2.24.2) - Essential Firebase functionality
- firebase_auth (v4.15.3) - User authentication
- cloud_firestore (v4.13.6) - Real-time database
- provider (v6.1.1) - State management and dependency injection
- flutter_bloc (v8.1.3) - Advanced state management
- cupertino_icons (v1.0.6) - iOS-style icons
- js (v0.7.1) - JavaScript interoperability (used for web version)
- flutter_test - Testing framework
- flutter_lints (v3.0.1) - Code quality and style enforcement
The Mafia Game follows a clean architecture approach with separation of concerns between UI, business logic, and data layers:
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Presentation │ │ Business │ │ Data │
│ Layer │◄────►│ Layer │◄────►│ Layer │
└───────────────┘ └───────────────┘ └───────────────┘
UI Game Logic & Firebase &
Components State Management Local Storage
┌─────────────────────────────────────────────────────────────┐
│ Flutter App │
│ │
│ ┌─────────────────┐ ┌────────────────┐ ┌────────────────┐│
│ │ Routes │ │ Theme │ │ Widgets ││
│ └─────────────────┘ └────────────────┘ └────────────────┘│
│ │
│ ┌─────────────────┐ ┌────────────────┐ ┌────────────────┐│
│ │ Screens │ │ Models │ │ Services ││
│ │ │ │ │ │ ││
│ │ - Auth Screens │ │ - Game Room │ │ - Auth Service ││
│ │ - Home Screen │ │ - Player │ │ - Game Service ││
│ │ - Game Screens │ │ - Game Phase │ │ ││
│ └─────────────────┘ └────────────────┘ └────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ State Management (Provider/BLoC) ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Firebase Integration ││
│ │ ││
│ │ ┌─────────────┐ ┌──────────────┐ ┌───────────┐ ││
│ │ │ Firestore │ │ Auth │ │ Firebase │ ││
│ │ │ Database │ │ Service │ │ Core │ ││
│ │ └─────────────┘ └──────────────┘ └───────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
┌───────────┐ ┌───────────┐ ┌────────────┐ ┌─────────────┐
│ Routes │────►│ Screens │────►│ Services │────►│ Firebase │
└───────────┘ └───────────┘ └────────────┘ └─────────────┘
│ ▲ ▲
▼ │ │
┌───────────┐ ┌────────────┐ │
│ Widgets │────►│ Models │───────────┘
└───────────┘ └────────────┘
The game uses Firebase Firestore to maintain game state across multiple clients. Here's the detailed database schema:
Each document represents a game room:
{
"name": "Room Name",
"hostId": "userId-12345",
"mafiaCount": 2,
"detectiveCount": 1,
"doctorCount": 1,
"dayTimeSeconds": 120,
"nightTimeSeconds": 30,
"voteTimeSeconds": 30,
"phase": "waiting",
"phaseEndTime": 1621234567890,
"mafiaTarget": "userId-abc",
"doctorTarget": "userId-def",
"detectiveTarget": "userId-ghi",
"votes": {
"userId-123": "userId-456",
"userId-789": "userId-456"
},
"players": [
{
"id": "userId-123",
"name": "Player1",
"role": "mafia",
"status": "alive",
"isHost": true
},
{
"id": "userId-456",
"name": "Player2",
"role": "villager",
"status": "alive",
"isHost": false
}
]
}
Chat messages for in-game communication:
{
"roomCode": "123456",
"senderId": "userId-123",
"senderName": "Player1",
"message": "I think Player2 is suspicious",
"timestamp": "2023-05-01T12:34:56Z"
}
ROOMS COLLECTION
├── document (roomCode)
│ ├── name (string)
│ ├── hostId (string)
│ ├── mafiaCount (number)
│ ├── detectiveCount (number)
│ ├── doctorCount (number)
│ ├── dayTimeSeconds (number)
│ ├── nightTimeSeconds (number)
│ ├── voteTimeSeconds (number)
│ ├── phase (string)
│ ├── phaseEndTime (timestamp)
│ ├── mafiaTarget (string/null)
│ ├── doctorTarget (string/null)
│ ├── detectiveTarget (string/null)
│ ├── votes (map)
│ │ ├── userId1 -> targetId
│ │ └── userId2 -> targetId
│ └── players (array)
│ ├── player1 (map)
│ │ ├── id (string)
│ │ ├── name (string)
│ │ ├── role (string)
│ │ ├── status (string)
│ │ └── isHost (boolean)
│ └── player2 (map)
│ └── ...
│
MESSAGES COLLECTION
├── document1
│ ├── roomCode (string)
│ ├── senderId (string)
│ ├── senderName (string)
│ ├── message (string)
│ └── timestamp (timestamp)
└── document2
└── ...
The application uses Firestore's real-time listeners to stay synchronized across devices:
┌────────────┐ ┌───────────────┐ ┌────────────┐
│ Client 1 │◄──►│ Firestore │◄──►│ Client 2 │
│ (Player 1) │ │ Database │ │ (Player 2) │
└────────────┘ └───────────────┘ └────────────┘
▲ ▲ ▲
│ │ │
▼ ▼ ▼
┌────────────┐ ┌───────────────┐ ┌────────────┐
│ Client 3 │◄──►│ Firebase │◄──►│ Client 4 │
│ (Player 3) │ │ Auth │ │ (Player 4) │
└────────────┘ └───────────────┘ └────────────┘
- Flutter SDK (≥3.0.0)
- Dart SDK
- Android Studio / Xcode for emulators
- Firebase project (for backend)
- Git
-
Clone the repository
git clone https://github.com/officiallyutso/Mafia-game-flutter.git cd Mafia-game-flutter
-
Install dependencies
flutter pub get
-
Configure Firebase
- Create a new Firebase project at https://console.firebase.google.com/
- Add Android/iOS apps to your Firebase project
- Download and add the configuration files:
google-services.json
(for Android)GoogleService-Info.plist
(for iOS)
- Enable Authentication (Email/Password) and Firestore Database
-
Create Firebase Options
flutterfire configure
-
Run the app
flutter run
- Launch the app
- Register with email and password
- Login to your account
- Go to the "Create Room" screen
- Set game parameters (number of Mafia, Detectives, Doctors)
- Set time parameters for day, night, and voting phases
- Create the room and share the room code with friends
- Select "Join Room" option
- Enter the 6-digit room code
- Wait for the host to start the game
- When the game starts, you'll be assigned a role
- During night phases, perform your role's actions if applicable
- During day phases, discuss with other players via the chat
- Vote during the voting phase
- Continue until one team wins
The project follows a structured organization to maintain clarity and separation of concerns:
mafia/
├── android/
├── ios/
├── web/
├── lib/
│ ├── main.dart # Entry point
│ ├── firebase_options.dart # Firebase configuration
│ ├── src/
│ │ ├── app.dart # Root app widget
│ │ ├── routes/
│ │ │ └── routes.dart # Navigation routes
│ │ ├── models/
│ │ │ └── game_room.dart # Data models
│ │ ├── screens/
│ │ │ ├── auth/
│ │ │ │ ├── login_screen.dart
│ │ │ │ └── register_screen.dart
│ │ │ ├── home/
│ │ │ │ └── home_screen.dart
│ │ │ └── game/
│ │ │ ├── create_room_screen.dart
│ │ │ ├── join_room_screen.dart
│ │ │ ├── lobby_screen.dart
│ │ │ ├── game_screen.dart
│ │ │ └── widgets/
│ │ │ ├── role_reveal_widget.dart
│ │ │ ├── night_action_widget.dart
│ │ │ ├── day_discussion_widget.dart
│ │ │ ├── voting_widget.dart
│ │ │ ├── game_over_widget.dart
│ │ │ ├── player_list_widget.dart
│ │ │ ├── chat_widget.dart
│ │ │ └── timer_widget.dart
│ │ └── services/
│ │ ├── auth_service.dart # Authentication
│ │ └── game_service.dart # Game logic
├── test/
├── pubspec.yaml
└── README.md
File | Purpose |
---|---|
main.dart |
Entry point that initializes Firebase and sets up providers |
app.dart |
Root widget with theme, routes, and auth state management |
routes.dart |
Navigation route definitions for the entire app |
game_room.dart |
Data models for game state and player information |
auth_service.dart |
Firebase authentication wrapper |
game_service.dart |
Game logic and Firestore operations |
create_room_screen.dart |
UI for creating new game rooms |
join_room_screen.dart |
UI for joining existing game rooms |
lobby_screen.dart |
Pre-game waiting room |
game_screen.dart |
Main game screen that changes based on game phase |
The authentication system uses Firebase Authentication with email/password sign-in. The implementation is encapsulated in the AuthService
class, which provides methods for:
// From auth_service.dart
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
// Auth state changes stream
Stream<User?> get authStateChanges => _auth.authStateChanges();
// Current user
User? get currentUser => _auth.currentUser;
// Sign in with email and password
Future<UserCredential> signInWithEmailAndPassword(String email, String password) async {
// Implementation...
}
// Register with email and password
Future<UserCredential> createUserWithEmailAndPassword(String email, String password) async {
// Implementation...
}
// Sign out
Future<void> signOut() async {
// Implementation...
}
}
The app uses a StreamBuilder
in app.dart
to listen to authentication state changes and redirect users accordingly:
// From app.dart
StreamBuilder(
stream: context.read<AuthService>().authStateChanges,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
final user = snapshot.data;
if (user != null) {
return const HomeScreen();
}
return const LoginScreen();
}
// Show loading indicator while checking auth state
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
},
)
The core game logic is implemented in the GameService
class, which handles all interactions with the Firestore database:
// From game_service.dart
class GameService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
// Collection references
CollectionReference get _roomsCollection => _firestore.collection('rooms');
CollectionReference get _messagesCollection => _firestore.collection('messages');
// Game room management methods
Future<String> createRoom(GameRoom room) async { /* ... */ }
Future<GameRoom> joinRoom(String roomCode, Player player) async { /* ... */ }
Future<void> leaveRoom(String roomCode, String playerId) async { /* ... */ }
Future<void> startGame(String roomCode) async { /* ... */ }
// Game action methods
Future<void> submitNightAction(String roomCode, String playerId,
PlayerRole role, String targetId) async { /* ... */ }
Future<void> submitVote(String roomCode, String voterId,
String targetId) async { /* ... */ }
// Phase processing methods
Future<void> processPhaseEnd(String roomCode) async { /* ... */ }
Future<void> _processNightEnd(String roomCode, GameRoom room) async { /* ... */ }
Future<void> _processDayEnd(String roomCode, GameRoom room) async { /* ... */ }
Future<void> _processVotingEnd(String roomCode, GameRoom room) async { /* ... */ }
Future<void> _checkGameOver(String roomCode, List<Player> players) async { /* ... */ }
// Chat methods
Future<void> sendMessage(String roomCode, String senderId,
String senderName, String message) async { /* ... */ }
// Streams for real-time updates
Stream<GameRoom> roomStream(String roomCode) { /* ... */ }
Stream<QuerySnapshot> messagesStream(String roomCode) { /* ... */ }
}
The game relies on several key data models:
// From game_room.dart
enum GamePhase {
waiting,
night,
day,
voting,
gameOver,
}
enum PlayerRole {
villager,
mafia,
detective,
doctor,
}
enum PlayerStatus {
alive,
dead,
}
class Player {
final String id;
final String name;
PlayerRole? role;
PlayerStatus status;
bool isHost;
// Implementation...
}
class GameRoom {
final String? id;
final String name;
final String hostId;
final int mafiaCount;
final int detectiveCount;
final int doctorCount;
final int dayTimeSeconds;
final int nightTimeSeconds;
final int voteTimeSeconds;
final List<Player> players;
GamePhase phase;
int? phaseEndTime;
String? mafiaTarget;
String? doctorTarget;
String? detectiveTarget;
Map<String, String> votes;
// Implementation...
}
The game includes several specialized UI components for different gameplay phases:
- RoleRevealWidget: Shows the player's assigned role at the start of the game
- NightActionWidget: Interface for night-time role actions (Mafia, Detective, Doctor)
- DayDiscussionWidget: Discussion interface with chat during the day phase
- VotingWidget: Voting interface during the voting phase
- GameOverWidget: Shows the game results and winner
- PlayerListWidget: Displays all players and their status
- ChatWidget: Real-time chat functionality
- TimerWidget: Countdown timer for each phase
The game follows a predictable lifecycle with state transitions triggered by player actions and timers:
┌────────────────┐
│ Waiting Room │
│ (Lobby) │
└───────┬────────┘
│
▼
┌────────────────┐
│ Role Reveal │
└───────┬────────┘
│
▼
┌────────────────┐ ┌────────────────┐
│ Night Phase │◄────────►│ Day Phase │
└───────┬────────┘ └───────┬────────┘
│ │
│ │
│ ▼
│ ┌────────────────┐
│ │ Voting Phase │
│ └───────┬────────┘
│ │
│ ┌────────────────────┘
│ │
▼ ▼
┌────────────────┐
│ Game Over │
└────────────────┘
The game automatically transitions between phases using a timer system:
// From game_screen.dart
void _startPhaseTimer() {
_phaseTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_checkPhaseEnd();
});
}
Future<void> _checkPhaseEnd() async {
final roomSnapshot = await FirebaseFirestore.instance
.collection('rooms')
.doc(_roomCode)
.get();
if (!roomSnapshot.exists) {
return;
}
final room = GameRoom.fromDocument(roomSnapshot);
if (room.phaseEndTime == null) {
return;
}
final now = DateTime.now().millisecondsSinceEpoch;
if (now >= room.phaseEndTime!) {
await _gameService.processPhaseEnd(_roomCode);
}
}
When a phase ends, the processPhaseEnd
method in GameService
handles the appropriate game logic:
// From game_service.dart
Future<void> processPhaseEnd(String roomCode) async {
final roomDoc = await _roomsCollection.doc(roomCode).get();
if (!roomDoc.exists) {
return;
}
final room = GameRoom.fromDocument(roomDoc);
switch (room.phase) {
case GamePhase.night:
await _processNightEnd(roomCode, room);
break;
case GamePhase.day:
await _processDayEnd(roomCode, room);
break;
case GamePhase.voting:
await _processVotingEnd(roomCode, room);
break;
default:
break;
}
}
Special roles have unique actions they can perform during the night phase:
- Mafia: Select a player to eliminate
- Detective: Investigate a player's role
- Doctor: Protect a player from elimination
// From game_service.dart
Future<void> submitNightAction(
String roomCode,
String playerId,
PlayerRole role,
String targetId,
) async {
final field = role == PlayerRole.mafia
? 'mafiaTarget'
: role == PlayerRole.doctor
? 'doctorTarget'
: 'detectiveTarget';
await _roomsCollection.doc(roomCode).update({
field: targetId,
});
}
During the voting phase, players can vote to eliminate a suspected Mafia member or skip voting:
// From game_service.dart
Future<void> submitVote(
String roomCode,
String voterId,
String targetId,
) async {
await _roomsCollection.doc(roomCode).update({
'votes.$voterId': targetId,
});
}
At the end of the voting phase, votes are tallied and the player with the most votes is eliminated:
// From game_service.dart
Future<void> _processVotingEnd(String roomCode, GameRoom room) async {
final players = List<Player>.from(room.players);
// Count votes
final voteCounts = <String, int>{};
room.votes.forEach((_, targetId) {
voteCounts[targetId] = (voteCounts[targetId] ?? 0) + 1;
});
// Check for skip votes
final skipVotes = voteCounts[skipVoteId] ?? 0;
// Count total votes
int totalVotes = room.votes.length;
// Find the player with the most votes
String? eliminatedId;
int maxVotes = 0;
// If half or more of the votes are to skip, then skip elimination
if (skipVotes >= totalVotes / 2) {
eliminatedId = null; // Skip elimination
} else {
// Process normal votes (excluding skip votes)
voteCounts.forEach((playerId, count) {
if (playerId != skipVoteId && count > maxVotes) {
maxVotes = count;
eliminatedId = playerId;
} else if (playerId != skipVoteId && count == maxVotes) {
// Tie - no elimination
eliminatedId = null;
}
});
}
// Eliminate the player if there was a clear majority
if (eliminatedId != null) {
final targetIndex = players.indexWhere((p) => p.id == eliminatedId);
if (targetIndex != -1) {
players[targetIndex].status = PlayerStatus.dead;
}
}
// Move to night phase
final now = DateTime.now().millisecondsSinceEpoch;
final phaseEndTime = now + (room.nightTimeSeconds * 1000);
await _roomsCollection.doc(roomCode).update({
'players': players.map((p) => p.toMap()).toList(),
'phase': GamePhase.night.toString().split('.').last,
'phaseEndTime': phaseEndTime,
'votes': {},
});
// Check if game is over
await _checkGameOver(roomCode, players);
}
After each elimination (via Mafia at night or voting during the day), the game checks if either side has won:
// From game_service.dart
Future<void> _checkGameOver(String roomCode, List<Player> players) async {
final alivePlayers = players.where((p) => p.status == PlayerStatus.alive).toList();
final aliveMafia = alivePlayers.where((p) => p.role == PlayerRole.mafia).length;
final aliveVillagers = alivePlayers.length - aliveMafia;
// Game over conditions
bool isGameOver = false;
String? winningTeam;
// Mafia wins if they equal or outnumber the villagers
if (aliveMafia >= aliveVillagers) {
isGameOver = true;
winningTeam = 'mafia';
}
// Villagers win if all mafia are eliminated
if (aliveMafia == 0) {
isGameOver = true;
winningTeam = 'villagers';
}
if (isGameOver) {
await _roomsCollection.doc(roomCode).update({
'phase': GamePhase.gameOver.toString().split('.').last,
'winningTeam': winningTeam,
});
}
}
The app uses a combination of Provider and BLoC patterns for state management:
Provider is used for dependency injection and simpler state management needs:
// From main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(
MultiProvider(
providers: [
Provider<AuthService>(
create: (_) => AuthService(),
),
Provider<GameService>(
create: (_) => GameService(),
),
],
child: const MafiaApp(),
),
);
}
For more complex states like game room management, the BLoC pattern is used:
// From game_room_bloc.dart
class GameRoomBloc extends Bloc<GameRoomEvent, GameRoomState> {
final GameService _gameService;
StreamSubscription? _roomSubscription;
StreamSubscription? _messagesSubscription;
GameRoomBloc(this._gameService) : super(GameRoomInitial()) {
on<JoinRoom>(_onJoinRoom);
on<CreateRoom>(_onCreateRoom);
on<LeaveRoom>(_onLeaveRoom);
on<StartGame>(_onStartGame);
on<SubmitNightAction>(_onSubmitNightAction);
on<SubmitVote>(_onSubmitVote);
on<SendMessage>(_onSendMessage);
on<RoomUpdated>(_onRoomUpdated);
on<MessagesUpdated>(_onMessagesUpdated);
}
void _onJoinRoom(JoinRoom event, Emitter<GameRoomState> emit) async {
emit(GameRoomLoading());
try {
final room = await _gameService.joinRoom(event.roomCode, event.player);
_subscribeToRoom(event.roomCode);
_subscribeToMessages(event.roomCode);
emit(GameRoomLoaded(room: room, messages: []));
} catch (e) {
emit(GameRoomError(message: e.toString()));
}
}
// Additional event handlers...
void _subscribeToRoom(String roomCode) {
_roomSubscription?.cancel();
_roomSubscription = _gameService.roomStream(roomCode).listen(
(room) {
add(RoomUpdated(room: room));
},
onError: (error) {
add(GameRoomErrorEvent(message: error.toString()));
},
);
}
// Additional methods...
@override
Future<void> close() {
_roomSubscription?.cancel();
_messagesSubscription?.cancel();
return super.close();
}
}
The app leverages Firebase for all backend functionality:
// From main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// Rest of initialization...
}
The app uses Firestore's real-time listeners to sync game state across devices:
// From game_service.dart
Stream<GameRoom> roomStream(String roomCode) {
return _roomsCollection.doc(roomCode).snapshots().map(
(snapshot) => GameRoom.fromDocument(snapshot),
);
}
Stream<QuerySnapshot> messagesStream(String roomCode) {
return _messagesCollection
.where('roomCode', isEqualTo: roomCode)
.orderBy('timestamp', descending: false)
.snapshots();
}
User authentication is managed through Firebase Auth:
// From auth_service.dart
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
Stream<User?> get authStateChanges => _auth.authStateChanges();
// Authentication methods...
}
The app uses a dark theme with red accents to create a mysterious and suspenseful atmosphere fitting for the Mafia game:
// From app.dart
ThemeData(
primarySwatch: Colors.red,
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF121212),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF1E1E1E),
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
// Additional theme configuration...
)
The UI is designed to work across different device sizes:
// From responsive_builder.dart
class ResponsiveBuilder extends StatelessWidget {
final Widget Function(BuildContext, ScreenSize) builder;
const ResponsiveBuilder({
Key? key,
required this.builder,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
ScreenSize screenSize;
if (constraints.maxWidth < 600) {
screenSize = ScreenSize.small;
} else if (constraints.maxWidth < 900) {
screenSize = ScreenSize.medium;
} else {
screenSize = ScreenSize.large;
}
return builder(context, screenSize);
},
);
}
}
The app includes animations for phase changes and role reveals:
// From role_reveal_widget.dart
class RoleRevealWidget extends StatefulWidget {
final PlayerRole role;
const RoleRevealWidget({
Key? key,
required this.role,
}) : super(key: key);
@override
_RoleRevealWidgetState createState() => _RoleRevealWidgetState();
}
class _RoleRevealWidgetState extends State<RoleRevealWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animation,
child: ScaleTransition(
scale: _animation,
child: _buildRoleCard(),
),
);
}
Widget _buildRoleCard() {
// Build role-specific UI...
}
}
To minimize database reads, the app implements caching strategies:
// From game_repository.dart
class GameRepository {
final GameService _gameService;
final _roomCache = <String, GameRoom>{};
GameRepository(this._gameService);
Future<GameRoom> getRoom(String roomCode) async {
// Check cache first
if (_roomCache.containsKey(roomCode)) {
return _roomCache[roomCode]!;
}
// Fetch from Firestore if not in cache
final roomDoc = await FirebaseFirestore.instance
.collection('rooms')
.doc(roomCode)
.get();
if (!roomDoc.exists) {
throw Exception('Room not found');
}
final room = GameRoom.fromDocument(roomDoc);
_roomCache[roomCode] = room;
return room;
}
// Additional methods...
}
To ensure smooth UI rendering, the app employs several optimization techniques:
- Lazy Loading: Lists of players and messages are loaded using
ListView.builder
for efficient rendering - Const Constructors: Widgets use const constructors when possible to optimize rebuilds
- Minimal Rebuilds: BLoC and Provider are used to minimize unnecessary widget rebuilds
// From player_list_widget.dart
class PlayerListWidget extends StatelessWidget {
final List<Player> players;
final String currentPlayerId;
const PlayerListWidget({
Key? key,
required this.players,
required this.currentPlayerId,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: players.length,
itemBuilder: (context, index) {
final player = players[index];
return PlayerCard(
player: player,
isCurrentPlayer: player.id == currentPlayerId,
);
},
);
}
}
The project includes unit, widget, and integration tests:
// From game_service_test.dart
void main() {
group('GameService', () {
late FirebaseFirestore firestore;
late GameService gameService;
setUp(() {
firestore = FakeFirebaseFirestore();
gameService = GameService();
});
test('create room generates unique code', () async {
final room = GameRoom(
name: 'Test Room',
hostId: 'user123',
mafiaCount: 1,
detectiveCount: 1,
doctorCount: 1,
dayTimeSeconds: 120,
nightTimeSeconds: 30,
voteTimeSeconds: 30,
players: [
Player(
id: 'user123',
name: 'Host',
status: PlayerStatus.alive,
isHost: true,
),
],
phase: GamePhase.waiting,
votes: {},
);
final roomCode = await gameService.createRoom(room);
expect(roomCode.length, 6);
expect(int.tryParse(roomCode), isNotNull);
});
// Additional tests...
});
}
// From game_screen_test.dart
void main() {
testWidgets('GameScreen displays correct UI for night phase', (WidgetTester tester) async {
// Create a mock GameRoom
final room = GameRoom(
id: '123456',
name: 'Test Room',
hostId: 'user123',
mafiaCount: 1,
detectiveCount: 1,
doctorCount: 1,
dayTimeSeconds: 120,
nightTimeSeconds: 30,
voteTimeSeconds: 30,
players: [
Player(
id: 'user123',
name: 'Player 1',
role: PlayerRole.mafia,
status: PlayerStatus.alive,
isHost: true,
),
Player(
id: 'user456',
name: 'Player 2',
role: PlayerRole.villager,
status: PlayerStatus.alive,
isHost: false,
),
],
phase: GamePhase.night,
phaseEndTime: DateTime.now().millisecondsSinceEpoch + 30000,
votes: {},
);
// Build the GameScreen with the mock room
await tester.pumpWidget(
MaterialApp(
home: MockGameScreen(room: room, currentPlayerId: 'user123'),
),
);
// Verify that the night phase UI is displayed
expect(find.text('Night Phase'), findsOneWidget);
expect(find.text('Choose a player to eliminate'), findsOneWidget);
expect(find.byType(TimerWidget), findsOneWidget);
});
// Additional test cases...
}
// From app_test.dart
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('End-to-end test', () {
testWidgets('Complete game flow test', (WidgetTester tester) async {
// Launch the app
app.main();
await tester.pumpAndSettle();
// Login
await tester.enterText(find.byType(TextFormField).at(0), '[email protected]');
await tester.enterText(find.byType(TextFormField).at(1), 'password123');
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
// Create a room
await tester.tap(find.text('Create Room'));
await tester.pumpAndSettle();
// Configure room settings
await tester.tap(find.text('Create'));
await tester.pumpAndSettle();
// Wait for room creation and verify
expect(find.text('Waiting for players...'), findsOneWidget);
// Additional steps to simulate full game flow...
});
});
}
To build and deploy the app for production, follow these steps:
flutter build apk --release
The APK will be created at build/app/outputs/apk/release/app-release.apk
.
flutter build ios --release
Then use Xcode to archive and upload to the App Store.
flutter build web --release
The web build will be created in the build/web
directory.
The project uses GitHub Actions for CI/CD:
# .github/workflows/ci.yml
name: CI/CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.10.0'
channel: 'stable'
- name: Install dependencies
run: flutter pub get
- name: Run tests
run: flutter test
- name: Build APK
run: flutter build apk --release
- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: app-release
path: build/app/outputs/apk/release/app-release.apk
Here are some planned improvements for future versions:
- Additional Roles: Add new special roles like Jester, Sheriff, and Bodyguard
- Custom Role Creation: Allow hosts to create custom roles with unique abilities
- Game History: Record and display statistics for players across multiple games
- Achievements System: Unlock achievements for completing specific game milestones
- Offline Mode: Support for playing locally on a single device
- Push Notifications: Alert players when it's their turn or when a game is about to start
- Voice Chat: Optional voice communication during day phases
- Advanced Analytics: Track player behavior and game balance
- Cross-platform Sync: Seamless gameplay across different devices
- Customizable Themes: Allow players to personalize the app's appearance
- Animated Transitions: Enhance transitions between game phases
- Custom Avatars: Let players choose or upload profile pictures
- Sound Effects: Add immersive audio cues for different game events
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
Please follow the Flutter style guide and run the following before submitting PRs:
flutter format .
flutter analyze
This project is licensed under the MIT License - see the LICENSE file for details.
Utso Sarkar - @officiallyutso - [email protected]
Project Link: https://github.com/officiallyutso/Mafia-game-flutter
- Flutter
- Firebase
- Provider
- Flutter BLoC
- Dart
- All the open-source libraries used in this project
- The Mafia/Werewolf game community for inspiration