An interactive group game that guides players through nature areas using their phones. Players complete bingo challenges by taking photos and answer multiple-choice questions about the location, competing for the highest score.
- Features
- Technology Stack
- Entity Relationship Diagram (ERD)
- Installation
- Deployment
- Configuration
- Edge Cases & Special Handling
- Project Structure
- Testing
β¨ Features
- Game Hosting: Create games with unique PIN codes for players to join
- Bingo Mode: Players capture photos of nature items to complete a 3x3 bingo card
- Question Mode: Sequential multiple-choice questions about the location
- Real-time Leaderboards: Track player scores and progress in real-time
- Photo Management: Host approves/rejects player photos with feedback
- Admin Panel: Full CRUD interface for managing locations, bingo items, and questions
- Game Modes: Configurable game modes per location (Bingo, Questions, or both)
- Timer Support: Optional countdown timer for games
- Player Feedback: Post-game feedback collection (rating and age)
π Technology Stack
| Component | Technology | Version |
|---|---|---|
| Backend Framework | Laravel | ^12.0 |
| Frontend Framework | Livewire | ^3.7 |
| PHP Version | PHP | ^8.2 |
| CSS Framework | Tailwind CSS | ^3.4 |
| JavaScript Framework | Alpine.js | ^3.4 |
| Charts | Chart.js | ^4.4.1 |
| Icons | Blade Icons (Heroicons, Lucide, Solar, Bootstrap) | Various |
| Fonts | Lexend, Figtree | Google/Bunny Fonts |
| Build Tool | Vite | ^7.0 |
| Storage | AWS S3 / Cloudflare R2 | Optional |
| Testing Framework | Pest PHP | ^4.1 |
π Entity Relationship Diagram (ERD)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TEMPLATE LAYER β
β (Admin-managed location templates) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββ
β locations β
βββββββββββββββββββββββββββββββββββ€
β PK id β
β name β
β description β
β image_path β
β province β
β distance β
β url β
β game_modes (JSON) β
β bingo_three_in_row_points β
β bingo_full_card_points β
β created_at β
β updated_at β
βββββββββββββββββββββββββββββββββββ
β
β 1:N
ββββββββββββββββββββββββββββββββββββββββββββ
β β
βΌ βΌ
βββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ
β location_bingo_items β β location_route_stops β
βββββββββββββββββββββββββββββββββββ€ βββββββββββββββββββββββββββββββββββ€
β PK id β β PK id β
β FK location_id β β FK location_id β
β label β β name β
β points β β question_text β
β icon β β option_a/b/c/d β
β fact β β correct_option (ENUM) β
β created_at β β points β
β updated_at β β sequence β
βββββββββββββββββββββββββββββββββββ β image_path β
β created_at β
β updated_at β
βββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β INSTANCE LAYER β
β (Runtime game instances & data) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββ
β games β
βββββββββββββββββββββββββββββββββββ€
β PK id β
locations.id βββββββββββββ FK location_id β
β pin (UNIQUE) β
β status (ENUM) β
β host_token β
β timer_enabled β
β timer_duration_minutes β
β timer_ends_at β
β started_at / finished_at β
β created_at / updated_at β
βββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββ
β 1:N β 1:N β 1:N
βΌ βΌ βΌ
βββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ
β game_players β β bingo_items β β route_stops β
βββββββββββββββββββββββββββββββββββ€ βββββββββββββββββββββββββββββββββββ€ βββββββββββββββββββββββββββββββββββ€
β PK id β β PK id β β PK id β
β FK game_id β β FK game_id β β FK game_id β
β name β β label β β name β
β token (UNIQUE) β β points β β question_text β
β score β β position β β option_a/b/c/d β
β feedback_rating β β icon_path β β correct_option (ENUM) β
β feedback_age β β created_at β β points β
β created_at β β updated_at β β sequence β
β updated_at β βββββββββββββββββββββββββββββββββββ β image_path β
βββββββββββββββββββββββββββββββββββ β β created_at β
β β β updated_at β
β 1:N β 1:N βββββββββββββββββββββββββββββββββββ
β β β
βΌ βΌ β 1:N
βββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
β route_stop_answers β β photos β β
βββββββββββββββββββββββββββββββββββ€ βββββββββββββββββββββββββββββββββββ€ β
β PK id β β PK id β β
β FK game_player_id β β FK game_id β β
β FK route_stop_id ββββββββββββββββββββββ FK game_player_id β β
β chosen_option (ENUM) β β FK bingo_item_id ββββββββββββββββββββββββ
β is_correct β β path β
β score_awarded β β status (ENUM) β
β answered_at β β taken_at β
β created_at β β created_at β
β updated_at β β updated_at β
β β βββββββββββββββββββββββββββββββββββ
β UNIQUE(game_player_id, β
β route_stop_id) β
βββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SYSTEM LAYER (Admin users) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββ
β users β
βββββββββββββββββββββββββββββββββββ€
β PK id β
β name β
β email (UNIQUE) β
β password β
β is_admin β
β admin_per_page β
β created_at β
β updated_at β
βββββββββββββββββββββββββββββββββββ
| Parent Table | Child Table | Relationship | Foreign Key |
|---|---|---|---|
locations |
location_bingo_items |
1:N | location_id |
locations |
location_route_stops |
1:N | location_id |
locations |
games |
1:N | location_id |
games |
game_players |
1:N | game_id |
games |
bingo_items |
1:N | game_id |
games |
route_stops |
1:N | game_id |
game_players |
photos |
1:N | game_player_id |
game_players |
route_stop_answers |
1:N | game_player_id |
bingo_items |
photos |
1:N | bingo_item_id |
route_stops |
route_stop_answers |
1:N | route_stop_id |
| Table | Column | Values |
|---|---|---|
games |
status |
lobby, started, finished |
photos |
status |
pending, approved, rejected |
location_route_stops / route_stops |
correct_option |
A, B, C, D |
route_stop_answers |
chosen_option |
A, B, C, D |
π Installation
- PHP 8.2 or higher
- Composer
- Node.js 18+ and npm
- SQLite (for development) or MySQL/PostgreSQL (for production)
git clone <repository-url>
cd NatuurMomentcomposer install
npm installcp .env.example .env
php artisan key:generatephp artisan migrate
php artisan db:seed # Optional: seed with sample datanpm run build # Production build
# OR
npm run dev # Development with hot reloadphp artisan serve
# OR use the dev script (includes queue worker and Vite)
composer run devThe application should now be running at http://localhost:8000
php artisan db:seed --class=DatabaseSeeder
# Default admin: admin@example.com / password
β οΈ Security: Change the default admin password immediately after first login!
π Deployment
This project was developed and deployed using Laravel Cloud. Laravel Cloud provides a seamless deployment experience for Laravel applications.
-
Create a Laravel Cloud account at cloud.laravel.com
-
Connect your repository - Link your GitHub repository to Laravel Cloud
-
Create a new application - Select your repository and branch
-
Configure environment variables - Add the following in the Laravel Cloud dashboard:
APP_KEY(generate withphp artisan key:generate --show)DB_CONNECTION,DB_HOST,DB_DATABASE,DB_USERNAME,DB_PASSWORDAWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_BUCKET(for photo storage)
-
Configure build settings:
- Build command:
npm install && npm run build - Laravel Cloud automatically runs
composer installandphp artisan migrate
- Build command:
-
Deploy - Push to your branch or trigger a manual deployment
Laravel Cloud handles SSL certificates, queue workers, and automatic deployments on push.
- PHP 8.2+ with extensions (openssl, pdo, mbstring, tokenizer, xml, ctype, json, fileinfo)
- MySQL 5.7+ or PostgreSQL 10+
- Web server (Nginx or Apache) with mod_rewrite
- SSL certificate (HTTPS required for photo uploads)
# Install dependencies
composer install --optimize-autoloader --no-dev
npm install && npm run build
# Configure environment
cp .env.example .env
php artisan key:generate
# Run migrations
php artisan migrate --force
# Optimize for production
php artisan config:cache
php artisan route:cache
php artisan view:cacheβοΈ Configuration
| Variable | Description | Example |
|---|---|---|
APP_ENV |
Environment | production |
APP_DEBUG |
Debug mode (false in production!) | false |
APP_URL |
Application URL | https://natuurmoment.example.com |
DB_CONNECTION |
Database driver | mysql |
AWS_ACCESS_KEY_ID |
AWS/R2 access key (optional) | your_access_key |
AWS_SECRET_ACCESS_KEY |
AWS/R2 secret key (optional) | your_secret_key |
AWS_BUCKET |
S3/R2 bucket name (optional) | natuurmoment-photos |
- Local Storage: Photos stored in
storage/app/public/photosby default - Cloud Storage: Configure AWS credentials in
.envto use S3/R2 - The
Photomodel automatically falls back to local storage if cloud is unavailable
β οΈ Edge Cases & Special Handling
Players must answer questions in sequence. Question N+1 is only unlocked after question N is answered.
- Solution:
RouteStop::isUnlockedFor()checks if all previous questions are answered - Direct URL access: Prevented by validation in
PlayerRouteQuestioncomponent - Browser refresh: State persisted in database, players continue from current question
Players cannot answer the same question twice.
- Solution: Database unique constraint on
[game_player_id, route_stop_id] - Race condition: Database constraint catches duplicate submissions
- UI protection: Submit button disabled after first answer
Host must approve photos before they count toward bingo completion.
- Solution: Photos have
statusfield:pending,approved,rejected - Completion check: Only
approvedphotos count toward 9-photo requirement
Locations must have sufficient content for enabled game modes.
- Bingo Mode: Requires at least 9 bingo items
- Question Mode: Requires at least 1 question
- Validation: Locations without valid game modes hidden from home page
Game PINs must be unique.
- Solution:
Game::generatePin()uses do-while loop to ensure uniqueness - Database constraint: Unique index on
pincolumn
Cloud storage may be unavailable.
- Solution:
Photo::getUrlAttribute()automatically falls back to local storage - Error handling: Exceptions during cloud storage checks are caught and logged
π Project Structure
NatuurMoment/
βββ app/
β βββ Constants/ # Game mode constants
β βββ Http/
β β βββ Controllers/ # REST controllers
β β βββ Middleware/ # Custom middleware (IsAdmin)
β β βββ Requests/ # Form request validation
β βββ Livewire/ # Livewire components
β β βββ CreateGame.php
β β βββ HostGame.php
β β βββ HostLobby.php
β β βββ JoinGame.php
β β βββ PlayerPhotoCapture.php
β β βββ PlayerRouteQuestion.php
β β βββ ...
β βββ Models/ # Eloquent models
β βββ Rules/ # Custom validation rules
βββ config/ # Configuration files
βββ database/
β βββ migrations/ # Database migrations
β βββ seeders/ # Database seeders
β βββ factories/ # Model factories
βββ public/ # Public assets
βββ resources/
β βββ css/ # Tailwind CSS
β βββ js/ # JavaScript/Alpine.js
β βββ views/ # Blade templates
β βββ admin/ # Admin panel views
β βββ livewire/ # Livewire component views
βββ routes/ # Route definitions
βββ storage/ # File storage
βββ tests/ # Pest PHP tests
π§ͺ Testing
The project uses Pest PHP for testing.
# Run all tests
php artisan test
# Run specific test file
php artisan test tests/Feature/Admin/LocationTest.php
# Run with coverage
php artisan test --coverage- Feature Tests: Admin panel CRUD operations, authentication, game flow
- Unit Tests: Model relationships, helper methods, validation rules
- Livewire Tests: Component interactions, form submissions, real-time updates
π TODO
- Add functionality for host to play the game with the players
- Let hosts create accounts so they can create their own routes/locations
This project is licensed under the MIT License.
Made with β€οΈ for nature enthusiasts