This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/consent-service/internal/services/matrix/game_rooms.go
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

369 lines
12 KiB
Go

package matrix
import (
"context"
"fmt"
"time"
)
// ========================================
// Breakpilot Drive Game Room Types
// ========================================
// GameMode represents different multiplayer game modes
type GameMode string
const (
GameModeSolo GameMode = "solo"
GameModeCoop GameMode = "coop" // 2 players, same track
GameModeChallenge GameMode = "challenge" // 1v1 competition
GameModeClassRace GameMode = "class_race" // Whole class competition
)
// GameRoomConfig holds configuration for game rooms
type GameRoomConfig struct {
GameMode GameMode `json:"game_mode"`
SessionID string `json:"session_id"`
HostUserID string `json:"host_user_id"`
HostName string `json:"host_name"`
ClassName string `json:"class_name,omitempty"`
MaxPlayers int `json:"max_players,omitempty"`
TeacherIDs []string `json:"teacher_ids,omitempty"`
EnableVoice bool `json:"enable_voice,omitempty"`
}
// GameRoom represents an active game room
type GameRoom struct {
RoomID string `json:"room_id"`
SessionID string `json:"session_id"`
GameMode GameMode `json:"game_mode"`
HostUserID string `json:"host_user_id"`
Players []string `json:"players"`
CreatedAt time.Time `json:"created_at"`
IsActive bool `json:"is_active"`
}
// GameEvent represents game events to broadcast
type GameEvent struct {
Type string `json:"type"`
SessionID string `json:"session_id"`
PlayerID string `json:"player_id"`
Data interface{} `json:"data"`
Timestamp time.Time `json:"timestamp"`
}
// GameEventType constants
const (
GameEventPlayerJoined = "player_joined"
GameEventPlayerLeft = "player_left"
GameEventGameStarted = "game_started"
GameEventQuizAnswered = "quiz_answered"
GameEventScoreUpdate = "score_update"
GameEventAchievement = "achievement"
GameEventChallengeWon = "challenge_won"
GameEventRaceFinished = "race_finished"
)
// ========================================
// Game Room Management
// ========================================
// CreateGameTeamRoom creates a private room for 2-4 players (Co-Op mode)
func (s *MatrixService) CreateGameTeamRoom(ctx context.Context, config GameRoomConfig) (*CreateRoomResponse, error) {
roomName := fmt.Sprintf("Breakpilot Drive - Team %s", config.SessionID[:8])
topic := "Co-Op Spielsession - Arbeitet zusammen!"
// All players can write
users := make(map[string]int)
users[s.GenerateUserID(config.HostUserID)] = 50
req := CreateRoomRequest{
Name: roomName,
Topic: topic,
Visibility: "private",
Preset: "private_chat",
InitialState: []StateEvent{
{
Type: "m.room.encryption",
StateKey: "",
Content: map[string]string{
"algorithm": "m.megolm.v1.aes-sha2",
},
},
// Custom game state
{
Type: "breakpilot.game.session",
StateKey: "",
Content: map[string]interface{}{
"session_id": config.SessionID,
"game_mode": string(config.GameMode),
"host_id": config.HostUserID,
"created_at": time.Now().UTC().Format(time.RFC3339),
},
},
},
PowerLevelContentOverride: &PowerLevels{
EventsDefault: 0, // All players can send messages
UsersDefault: 50,
Users: users,
Events: map[string]int{
"breakpilot.game.event": 0, // Anyone can send game events
},
},
}
return s.CreateRoom(ctx, req)
}
// CreateGameChallengeRoom creates a 1v1 challenge room
func (s *MatrixService) CreateGameChallengeRoom(ctx context.Context, config GameRoomConfig, challengerID string, opponentID string) (*CreateRoomResponse, error) {
roomName := fmt.Sprintf("Challenge: %s", config.SessionID[:8])
topic := "1v1 Wettbewerb - Möge der Bessere gewinnen!"
allPlayers := []string{
s.GenerateUserID(challengerID),
s.GenerateUserID(opponentID),
}
users := make(map[string]int)
for _, id := range allPlayers {
users[id] = 50
}
req := CreateRoomRequest{
Name: roomName,
Topic: topic,
Visibility: "private",
Preset: "private_chat",
Invite: allPlayers,
InitialState: []StateEvent{
{
Type: "breakpilot.game.session",
StateKey: "",
Content: map[string]interface{}{
"session_id": config.SessionID,
"game_mode": string(GameModeChallenge),
"challenger_id": challengerID,
"opponent_id": opponentID,
"created_at": time.Now().UTC().Format(time.RFC3339),
},
},
},
PowerLevelContentOverride: &PowerLevels{
EventsDefault: 0,
UsersDefault: 50,
Users: users,
},
}
return s.CreateRoom(ctx, req)
}
// CreateGameClassRaceRoom creates a room for class-wide competition
func (s *MatrixService) CreateGameClassRaceRoom(ctx context.Context, config GameRoomConfig) (*CreateRoomResponse, error) {
roomName := fmt.Sprintf("Klassenrennen: %s", config.ClassName)
topic := fmt.Sprintf("Klassenrennen der %s - Alle gegen alle!", config.ClassName)
// Teachers get moderator power level
users := make(map[string]int)
for _, teacherID := range config.TeacherIDs {
users[s.GenerateUserID(teacherID)] = 100
}
req := CreateRoomRequest{
Name: roomName,
Topic: topic,
Visibility: "private",
Preset: "private_chat",
InitialState: []StateEvent{
{
Type: "breakpilot.game.session",
StateKey: "",
Content: map[string]interface{}{
"session_id": config.SessionID,
"game_mode": string(GameModeClassRace),
"class_name": config.ClassName,
"teacher_ids": config.TeacherIDs,
"created_at": time.Now().UTC().Format(time.RFC3339),
},
},
},
PowerLevelContentOverride: &PowerLevels{
EventsDefault: 0, // Students can send messages
UsersDefault: 10, // Default student level
Users: users,
Invite: 100, // Only teachers can invite
Kick: 100, // Only teachers can kick
Events: map[string]int{
"breakpilot.game.event": 0, // Anyone can send game events
"breakpilot.game.leaderboard": 100, // Only teachers update leaderboard
},
},
}
return s.CreateRoom(ctx, req)
}
// ========================================
// Game Event Broadcasting
// ========================================
// SendGameEvent sends a game event to a room
func (s *MatrixService) SendGameEvent(ctx context.Context, roomID string, event GameEvent) error {
event.Timestamp = time.Now().UTC()
return s.sendEvent(ctx, roomID, "breakpilot.game.event", event)
}
// SendPlayerJoinedEvent notifies room that a player joined
func (s *MatrixService) SendPlayerJoinedEvent(ctx context.Context, roomID string, sessionID string, playerID string, playerName string) error {
event := GameEvent{
Type: GameEventPlayerJoined,
SessionID: sessionID,
PlayerID: playerID,
Data: map[string]string{
"player_name": playerName,
},
}
// Also send a visible message
msg := fmt.Sprintf("🎮 %s ist dem Spiel beigetreten!", playerName)
if err := s.SendMessage(ctx, roomID, msg); err != nil {
// Log but don't fail
fmt.Printf("Warning: failed to send join message: %v\n", err)
}
return s.SendGameEvent(ctx, roomID, event)
}
// SendScoreUpdateEvent broadcasts score updates
func (s *MatrixService) SendScoreUpdateEvent(ctx context.Context, roomID string, sessionID string, playerID string, score int, accuracy float64) error {
event := GameEvent{
Type: GameEventScoreUpdate,
SessionID: sessionID,
PlayerID: playerID,
Data: map[string]interface{}{
"score": score,
"accuracy": accuracy,
},
}
return s.SendGameEvent(ctx, roomID, event)
}
// SendQuizAnsweredEvent broadcasts when a player answers a quiz
func (s *MatrixService) SendQuizAnsweredEvent(ctx context.Context, roomID string, sessionID string, playerID string, correct bool, subject string) error {
event := GameEvent{
Type: GameEventQuizAnswered,
SessionID: sessionID,
PlayerID: playerID,
Data: map[string]interface{}{
"correct": correct,
"subject": subject,
},
}
return s.SendGameEvent(ctx, roomID, event)
}
// SendAchievementEvent broadcasts when a player earns an achievement
func (s *MatrixService) SendAchievementEvent(ctx context.Context, roomID string, sessionID string, playerID string, achievementID string, achievementName string) error {
event := GameEvent{
Type: GameEventAchievement,
SessionID: sessionID,
PlayerID: playerID,
Data: map[string]interface{}{
"achievement_id": achievementID,
"achievement_name": achievementName,
},
}
// Also send a visible celebration message
msg := fmt.Sprintf("🏆 Erfolg freigeschaltet: %s!", achievementName)
if err := s.SendMessage(ctx, roomID, msg); err != nil {
fmt.Printf("Warning: failed to send achievement message: %v\n", err)
}
return s.SendGameEvent(ctx, roomID, event)
}
// SendChallengeWonEvent broadcasts challenge result
func (s *MatrixService) SendChallengeWonEvent(ctx context.Context, roomID string, sessionID string, winnerID string, winnerName string, loserName string, winnerScore int, loserScore int) error {
event := GameEvent{
Type: GameEventChallengeWon,
SessionID: sessionID,
PlayerID: winnerID,
Data: map[string]interface{}{
"winner_name": winnerName,
"loser_name": loserName,
"winner_score": winnerScore,
"loser_score": loserScore,
},
}
// Send celebration message
msg := fmt.Sprintf("🎉 %s gewinnt gegen %s mit %d zu %d Punkten!", winnerName, loserName, winnerScore, loserScore)
if err := s.SendHTMLMessage(ctx, roomID, msg, fmt.Sprintf("<h3>🎉 Challenge beendet!</h3><p><strong>%s</strong> gewinnt gegen %s</p><p>Endstand: %d : %d</p>", winnerName, loserName, winnerScore, loserScore)); err != nil {
fmt.Printf("Warning: failed to send challenge result message: %v\n", err)
}
return s.SendGameEvent(ctx, roomID, event)
}
// SendClassRaceLeaderboard broadcasts current leaderboard in class race
func (s *MatrixService) SendClassRaceLeaderboard(ctx context.Context, roomID string, sessionID string, leaderboard []map[string]interface{}) error {
// Build leaderboard message
msg := "🏁 Aktueller Stand:\n"
htmlMsg := "<h3>🏁 Aktueller Stand</h3><ol>"
for i, entry := range leaderboard {
if i >= 10 { // Top 10 only
break
}
name := entry["name"].(string)
score := entry["score"].(int)
msg += fmt.Sprintf("%d. %s - %d Punkte\n", i+1, name, score)
htmlMsg += fmt.Sprintf("<li><strong>%s</strong> - %d Punkte</li>", name, score)
}
htmlMsg += "</ol>"
return s.SendHTMLMessage(ctx, roomID, msg, htmlMsg)
}
// ========================================
// Game Room Utilities
// ========================================
// AddPlayerToGameRoom invites and sets up a player in a game room
func (s *MatrixService) AddPlayerToGameRoom(ctx context.Context, roomID string, playerMatrixID string, playerName string) error {
// Invite the player
if err := s.InviteUser(ctx, roomID, playerMatrixID); err != nil {
return fmt.Errorf("failed to invite player: %w", err)
}
// Set display name if not already set
if err := s.SetDisplayName(ctx, playerMatrixID, playerName); err != nil {
// Log but don't fail - display name might already be set
fmt.Printf("Warning: failed to set display name: %v\n", err)
}
return nil
}
// CloseGameRoom sends end message and archives the room
func (s *MatrixService) CloseGameRoom(ctx context.Context, roomID string, sessionID string) error {
// Send closing message
msg := "🏁 Spiel beendet! Danke fürs Mitspielen. Dieser Raum wird archiviert."
if err := s.SendMessage(ctx, roomID, msg); err != nil {
return fmt.Errorf("failed to send closing message: %w", err)
}
// Update room state to mark as closed
closeEvent := map[string]interface{}{
"closed": true,
"closed_at": time.Now().UTC().Format(time.RFC3339),
}
return s.sendEvent(ctx, roomID, "breakpilot.game.closed", closeEvent)
}