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("

๐ŸŽ‰ Challenge beendet!

%s gewinnt gegen %s

Endstand: %d : %d

", 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 := "

๐Ÿ Aktueller Stand

    " 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("
  1. %s - %d Punkte
  2. ", name, score) } htmlMsg += "
" 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) }