A location-based guessing game where users try to identify where pictures were taken on a map. Players can upload their own pictures, which become part of the game's content. The game includes multiple modes and features both online and offline functionality.
- Daily challenges with 5 random pictures
- Unlimited mode for continuous play
- Picture upload with location data
- Google Maps integration
- Global and friends leaderboards
- User profiles with biography and picture gallery
- Offline play capability
- Score synchronization
Before you begin, ensure you have the following installed:
- Flutter (latest stable version)
- Dart (latest stable version)
- Git
- Supabase CLI
- Docker Desktop (for local Supabase)
- Node.js (LTS version)
git clone https://gitlab.socs.uoguelph.ca/w25-4030-section-01/group_17/uoguesser.git
cd uoguesserCreate a .env file in the project root:
touch .envAdd the following variables to your .env file (get these values from the team lead):
SUPABASE_URL=your_production_supabase_url
SUPABASE_ANON_KEY=your_production_supabase_anon_keyImportant: We are currently working directly with the production database. The local Supabase setup described below is optional and only needed if you want to experiment with database changes without affecting the production environment.
flutter pub getNote: This section is for developers who need to test database changes locally. For regular development, you can skip this section as we are working with the production database.
If you need to test database changes without affecting the production environment, you can set up a local Supabase instance:
-
Start Docker Desktop
-
Initialize Supabase project:
supabase init- Start local Supabase:
supabase startThis will create a local Supabase instance with the following credentials:
API URL: http://127.0.0.1:54321
GraphQL URL: http://127.0.0.1:54321/graphql/v1
S3 Storage URL: http://127.0.0.1:54321/storage/v1/s3
DB URL: postgresql://postgres:[email protected]:54322/postgres
Studio URL: http://127.0.0.1:54323
Inbucket URL: http://127.0.0.1:54324
JWT secret: <secret>
anon key: <key>
service_role key: <key>
S3 Access Key: <key>
S3 Secret Key: <key>
S3 Region: local
- To use the local instance instead of production, update your
.envfile with the local credentials provided bysupabase start.
flutter runImportant: We are currently working with the production database. Please coordinate with everyone before making any database changes.
All database migrations are stored in supabase/migrations/. To manage migrations:
- Create a new migration:
supabase migration new your_migration_name- Apply migrations:
supabase migration up- Revert last migration:
supabase migration down- Reset database:
supabase db reset- Start local Supabase:
supabase start- Stop local Supabase:
supabase stop- View logs:
supabase logslib/
├── main.dart # Application entry point
├── providers/ # State management
│ ├── player.provider.dart # Player state management
│ └── ...
├── server/ # Backend integration
│ ├── services/ # Business logic layer
│ │ ├── player.service.dart
│ │ ├── game.service.dart
│ │ └── ...
│ ├── models/ # Data models
│ │ ├── player.dart
│ │ ├── game.dart
│ │ └── ...
│ └── data/ # Data access layer
│ ├── player.repository.dart
│ ├── game.repository.dart
│ └── ...
├── screens/ # UI screens
The application uses the Provider pattern for state management. Each major feature has its own provider that manages its state and business logic. For example, the PlayerProvider:
class PlayerProvider extends ChangeNotifier {
final _playerService = GetIt.instance<PlayerService>();
Player? _currentPlayer;
List<Picture> _pictures = [];
// Expose state
Player? get currentPlayer => _currentPlayer;
List<Picture> get pictures => _pictures;
// Methods to modify state
Future<void> updateProfile({required String name, String? biography}) async {
// Update player profile
// Notify listeners of changes
}
}The server-side code follows a clean architecture pattern with three main layers:
- Pure Dart classes representing database entities
- Handles JSON serialization/deserialization
- No business logic or data access
- Example:
Player,Game,Picturemodels
// Example model structure (player.dart)
class Player {
final String id;
final String username;
Player.fromJson(Map<String, dynamic> json) {
// Conversion from JSON to Dart object
}
Map<String, dynamic> toJson() {
// Conversion from Dart object to JSON
}
}- Repository classes for direct Supabase interaction
- Handles raw database queries and mutations
- No business logic
- One repository per entity type
- Example:
PlayerRepository,GameRepository,PictureRepository
// Example repository structure (player.repository.dart)
class PlayerRepository {
final SupabaseClient _supabase;
Future<Player> getPlayer(String id) async {
// Raw database query
final response = await _supabase
.from('players')
.select()
.eq('player_id', id)
.single();
return Player.fromJson(response);
}
}- Business logic implementation
- Orchestrates data operations
- Error handling and validation
- One service per major feature
- Example:
PlayerService,GameService,PictureService
// Example service structure (player.service.dart)
class PlayerService {
final PlayerRepository _repository;
Future<Player> getPlayerProfile(String id) async {
try {
// Business logic, validation, error handling
return await _repository.getPlayer(id);
} catch (e) {
throw PlayerServiceException('Failed to get player profile: $e');
}
}
}The application uses the get_it package for service location and dependency injection:
- Service Registration (
service_locator.dart):- Registers all repositories and services as singletons
- Manages dependencies between services
- Initializes Supabase client
// Example service registration
final getIt = GetIt.instance;
setupServices() {
// Register repositories
getIt.registerLazySingleton(() => PlayerRepository(supabase));
// Register services with their dependencies
getIt.registerLazySingleton(() => PlayerService(getIt<PlayerRepository>()));
}- Service Usage in App:
- Services are accessed through
getIt - No need to manually manage dependencies
- Consistent singleton instances throughout the app
- Services are accessed through
// Example service usage in widgets
final playerService = getIt<PlayerService>();- Install Supabase CLI (if not already installed):
brew install supabase/tap/supabase # macOS- Login to Supabase:
supabase login- List your Supabase projects:
supabase projects listThis will show all your projects and their reference IDs in the format:
Name | Reference ID | Database Status | URL
+-------------------------------------------------------+--------------+-----------------+-----------+
your-project-name | abcdefghijkl | Active | app.supabase.com
- Link your local project to Supabase (using the Reference ID from step 3):
supabase link --project-ref your-project-refNote: The project reference is the unique identifier shown in the 'Reference ID' column from the projects list
- Push database changes:
supabase db pushEdge functions are serverless functions that run on Supabase's edge network. They're located in the supabase/functions directory.
- Create a new function:
supabase functions new your-function-nameThis creates a new TypeScript function in supabase/functions/your-function-name/index.ts
- Example edge function structure:
// supabase/functions/calculate-score/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
interface ScoreParams {
guessLocation: [number, number]
actualLocation: [number, number]
}
serve(async (req) => {
try {
const { guessLocation, actualLocation } = await req.json() as ScoreParams
// Your function logic here
const score = calculateScore(guessLocation, actualLocation)
return new Response(
JSON.stringify({ score }),
{ headers: { 'Content-Type': 'application/json' } }
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
)
}
})- Start the edge functions development server:
supabase functions serve- Test with curl:
curl -L -X POST 'http://localhost:54321/functions/v1/your-function-name' \
-H 'Authorization: Bearer your-anon-key' \
-d '{"key": "value"}'- Deploy a single function:
supabase functions deploy your-function-name- Deploy all functions:
supabase functions deploysupabase functions deploy --no-verify-jwt-
Supabase Connection Issues
- Ensure Docker is running
- Check if Supabase is started (
supabase status) - Verify environment variables are correct
-
Flutter Build Issues
- Run
flutter clean - Delete
pubspec.lockand runflutter pub get - Ensure all dependencies are compatible
- Run
-
Database Migration Issues
- Reset the database (
supabase db reset) - Check migration file syntax
- Verify migration order
- Reset the database (
- University of Guelph
- CIS*4030 Course Team
- All contributors to this project