365 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			365 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package handlers
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"gralias/models"
 | |
| 	"html/template"
 | |
| 	"net/http"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| func HandleCreateRoom(w http.ResponseWriter, r *http.Request) {
 | |
| 	turnTimeStr := r.PostFormValue("game_time")
 | |
| 	ttU64, err := strconv.ParseUint(turnTimeStr, 10, 64)
 | |
| 	if err != nil {
 | |
| 		log.Warn("failed to parse turn time", "game_time", turnTimeStr)
 | |
| 	}
 | |
| 	// parse payload
 | |
| 	payload := &models.RoomReq{
 | |
| 		RoomPass:  r.PostFormValue("room_pass"),
 | |
| 		Language:  r.PostFormValue("language"),
 | |
| 		RoundTime: uint32(ttU64),
 | |
| 	}
 | |
| 	// create a room
 | |
| 	room, err := createRoom(r.Context(), payload)
 | |
| 	if err != nil {
 | |
| 		msg := "failed to create a room"
 | |
| 		log.Error(msg, "error", err)
 | |
| 		abortWithError(w, msg)
 | |
| 		return
 | |
| 	}
 | |
| 	fi, err := getFullInfoByCtx(r.Context())
 | |
| 	if err != nil {
 | |
| 		msg := "failed to get full info from ctx"
 | |
| 		log.Error(msg, "error", err)
 | |
| 		abortWithError(w, msg)
 | |
| 		return
 | |
| 	}
 | |
| 	fi.State.RoomID = &room.ID
 | |
| 	fi.Room = room
 | |
| 	if err := repo.PlayerSetRoomID(r.Context(), room.ID, fi.State.Username); err != nil {
 | |
| 		log.Error("failed to set room id", "error", err)
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	notify(models.NotifyRoomListUpdate, "")
 | |
| 	tmpl, err := template.ParseGlob("components/*.html")
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	if err := tmpl.ExecuteTemplate(w, "base", fi); err != nil {
 | |
| 		log.Error("failed to execute base template", "error", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func HandleJoinTeam(w http.ResponseWriter, r *http.Request) {
 | |
| 	if err := r.ParseForm(); err != nil {
 | |
| 		log.Error("failed to parse form", "error", err)
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	team := r.PostFormValue("team")
 | |
| 	role := r.PostFormValue("role")
 | |
| 	if team == "" || role == "" {
 | |
| 		msg := "missing team or role"
 | |
| 		log.Error(msg)
 | |
| 		abortWithError(w, msg)
 | |
| 		return
 | |
| 	}
 | |
| 	// get username
 | |
| 	fi, err := getFullInfoByCtx(r.Context())
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	if fi.Room.IsRunning && role == "mime" {
 | |
| 		err = errors.New("cannot join as mime when game is running")
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	fi, err = joinTeam(r.Context(), role, team)
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	// reveal all cards
 | |
| 	if fi.State.Role == "mime" {
 | |
| 		fi.Room.MimeView() // there must be a better way
 | |
| 	} else {
 | |
| 		fi.Room.GuesserView()
 | |
| 	}
 | |
| 	// return html
 | |
| 	tmpl, err := template.ParseGlob("components/*.html")
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	notify(models.NotifyRoomUpdatePrefix+fi.Room.ID, "")
 | |
| 	if err := tmpl.ExecuteTemplate(w, "base", fi); err != nil {
 | |
| 		log.Error("failed to execute base template", "error", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func HandleEndTurn(w http.ResponseWriter, r *http.Request) {
 | |
| 	// get username
 | |
| 	fi, err := getFullInfoByCtx(r.Context())
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	// check if one who pressed it is from the team who has the turn
 | |
| 	if fi.Room.TeamTurn != fi.State.Team {
 | |
| 		msg := fmt.Sprintln("unexpected team turn:" + fi.Room.TeamTurn)
 | |
| 		abortWithError(w, msg)
 | |
| 		return
 | |
| 	}
 | |
| 	fi.Room.ChangeTurn()
 | |
| 	fi.Room.MimeDone = false
 | |
| 	StopTurnTimer(fi.Room.ID)
 | |
| 	if err := saveFullInfo(r.Context(), fi); err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	// return html
 | |
| 	tmpl, err := template.ParseGlob("components/*.html")
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	notifyBotIfNeeded(fi.Room)
 | |
| 	notify(models.NotifyRoomUpdatePrefix+fi.Room.ID, "")
 | |
| 	if err := tmpl.ExecuteTemplate(w, "base", fi); err != nil {
 | |
| 		log.Error("failed to execute base template", "error", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func HandleStartGame(w http.ResponseWriter, r *http.Request) {
 | |
| 	fi, err := getFullInfoByCtx(r.Context())
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	// check if enough players
 | |
| 	if err := fi.Room.CanStart(); err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	// Initialize transaction
 | |
| 	ctx, tx, err := repo.InitTx(r.Context())
 | |
| 	if err != nil {
 | |
| 		log.Error("failed to init transaction", "error", err)
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	defer func() {
 | |
| 		if r := recover(); r != nil {
 | |
| 			if err := tx.Rollback(); err != nil {
 | |
| 				log.Error("failed to rollback transaction", "error", err)
 | |
| 			}
 | |
| 			panic(r)
 | |
| 		}
 | |
| 	}()
 | |
| 	fi.Room.MimeDone = false
 | |
| 	fi.Room.IsRunning = true
 | |
| 	fi.Room.IsOver = false
 | |
| 	fi.Room.TeamTurn = "blue"
 | |
| 	fi.Room.OpenedThisTurn = 0
 | |
| 	fi.Room.ThisTurnLimit = 0
 | |
| 	loadCards(fi.Room)
 | |
| 	fi.Room.UpdateCounter()
 | |
| 	fi.Room.TeamWon = ""
 | |
| 	action := models.Action{
 | |
| 		RoomID:     fi.Room.ID,
 | |
| 		CreatedAt:  time.Now(),
 | |
| 		Actor:      fi.State.Username,
 | |
| 		ActorColor: string(fi.State.Team),
 | |
| 		WordColor:  string(fi.State.Team),
 | |
| 		Action:     models.ActionTypeGameStarted,
 | |
| 	}
 | |
| 	fi.Room.ActionHistory = append(fi.Room.ActionHistory, action)
 | |
| 	// Use the new context with transaction
 | |
| 	// if err := saveFullInfo(ctx, fi); err != nil {
 | |
| 	// 	if err := tx.Rollback(); err != nil {
 | |
| 	// 		log.Error("failed to rollback transaction", "error", err)
 | |
| 	// 	}
 | |
| 	// 	abortWithError(w, err.Error())
 | |
| 	// 	return
 | |
| 	// }
 | |
| 	// Save action history
 | |
| 	if err := repo.ActionCreate(ctx, &action); err != nil {
 | |
| 		if err := tx.Rollback(); err != nil {
 | |
| 			log.Error("failed to rollback transaction", "error", err)
 | |
| 		}
 | |
| 		log.Error("failed to save action", "error", err)
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	// Save word cards
 | |
| 	for _, card := range fi.Room.Cards {
 | |
| 		card.RoomID = fi.Room.ID // Ensure RoomID is set for each card
 | |
| 		if err := repo.WordCardsCreate(ctx, &card); err != nil {
 | |
| 			if err := tx.Rollback(); err != nil {
 | |
| 				log.Error("failed to rollback transaction", "error", err)
 | |
| 			}
 | |
| 			log.Error("failed to save word card", "error", err)
 | |
| 			abortWithError(w, err.Error())
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 	if err := repo.RoomUpdate(ctx, fi.Room); err != nil {
 | |
| 		log.Error("failed to update room", "error", err)
 | |
| 		// nolint: errcheck
 | |
| 		tx.Rollback()
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	// Commit the transaction
 | |
| 	if err := tx.Commit(); err != nil {
 | |
| 		log.Error("failed to commit transaction", "error", err)
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	// reveal all cards
 | |
| 	if fi.State.Role == "mime" {
 | |
| 		fi.Room.MimeView()
 | |
| 	} else {
 | |
| 		fi.Room.GuesserView()
 | |
| 	}
 | |
| 	// return html
 | |
| 	tmpl, err := template.ParseGlob("components/*.html")
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	notifyBotIfNeeded(fi.Room)
 | |
| 	// to update only the room that should be updated
 | |
| 	notify(models.NotifyRoomUpdatePrefix+fi.Room.ID, "")
 | |
| 	// notify(models.NotifyBacklogPrefix+fi.Room.ID, "game started")
 | |
| 	if err := tmpl.ExecuteTemplate(w, "room", fi); err != nil {
 | |
| 		log.Error("failed to execute room template", "error", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func HandleJoinRoom(w http.ResponseWriter, r *http.Request) {
 | |
| 	roomID := r.URL.Query().Get("id")
 | |
| 	room, err := repo.RoomGetExtended(r.Context(), roomID)
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	tmpl, err := template.ParseGlob("components/*.html")
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	fi, err := getFullInfoByCtx(r.Context())
 | |
| 	if err != nil {
 | |
| 		// INFO: if non-loggined user join: prompt to login
 | |
| 		fi = &models.FullInfo{}
 | |
| 		fi.LinkLogin = roomID
 | |
| 		if err := tmpl.ExecuteTemplate(w, "base", fi); err != nil {
 | |
| 			log.Error("failed to execute base template", "error", err)
 | |
| 		}
 | |
| 		// abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	// room.PlayerList = append(room.PlayerList, fi.State.Username)
 | |
| 	fi.State.RoomID = &room.ID
 | |
| 	fi.Room = room
 | |
| 	fi.List = nil
 | |
| 	if err := repo.PlayerSetRoomID(r.Context(), room.ID, fi.State.Username); err != nil {
 | |
| 		log.Error("failed to set room_id for player", "error", err, "username", fi.State.Username, "room_id", room.ID)
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	if err := tmpl.ExecuteTemplate(w, "room", fi); err != nil {
 | |
| 		log.Error("failed to execute room template", "error", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func HandleGiveClue(w http.ResponseWriter, r *http.Request) {
 | |
| 	if err := r.ParseForm(); err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	clue := r.PostFormValue("clue")
 | |
| 	num := r.PostFormValue("number")
 | |
| 	fi, err := getFullInfoByCtx(r.Context())
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	guessLimitU64, err := strconv.ParseUint(num, 10, 8)
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	// validations ===
 | |
| 	if fi.State.Team != models.UserTeam(fi.Room.TeamTurn) {
 | |
| 		err = errors.New("not your team's move")
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	if fi.State.Role != "mime" {
 | |
| 		err = errors.New("need to be mime to open the card")
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	if fi.Room.MimeDone {
 | |
| 		// team already have a clue
 | |
| 		abortWithError(w, "your team already has a clue")
 | |
| 		return
 | |
| 	}
 | |
| 	// check if the clue is the same as one of the existing words
 | |
| 	for _, card := range fi.Room.Cards {
 | |
| 		if strings.EqualFold(card.Word, clue) {
 | |
| 			msg := fmt.Sprintf("cannot use existing word (%s) as a clue", clue)
 | |
| 			abortWithError(w, msg)
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 	// ===
 | |
| 	action := models.Action{
 | |
| 		RoomID:     fi.Room.ID,
 | |
| 		Actor:      fi.State.Username,
 | |
| 		ActorColor: string(fi.State.Team),
 | |
| 		WordColor:  string(fi.State.Team),
 | |
| 		Action:     models.ActionTypeClue,
 | |
| 		Word:       clue,
 | |
| 		Number:     num,
 | |
| 	}
 | |
| 	fi.Room.ActionHistory = append(fi.Room.ActionHistory, action)
 | |
| 	if err := repo.ActionCreate(r.Context(), &action); err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	fi.Room.MimeDone = true
 | |
| 	fi.Room.ThisTurnLimit = uint8(guessLimitU64) + 1
 | |
| 	if guessLimitU64 == 0 {
 | |
| 		fi.Room.ThisTurnLimit = 9
 | |
| 	}
 | |
| 	fi.Room.OpenedThisTurn = 0
 | |
| 	StartTurnTimer(fi.Room.ID, fi.Room.Settings.RoundTime)
 | |
| 	log.Debug("given clue", "clue", clue, "limit", fi.Room.ThisTurnLimit)
 | |
| 	// notify(models.NotifyBacklogPrefix+fi.Room.ID, clue+num)
 | |
| 	notify(models.NotifyRoomUpdatePrefix+fi.Room.ID, clue+num)
 | |
| 	if err := saveFullInfo(r.Context(), fi); err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	notifyBotIfNeeded(fi.Room)
 | |
| }
 | |
| 
 | |
| func HandleRenotifyBot(w http.ResponseWriter, r *http.Request) {
 | |
| 	fi, err := getFullInfoByCtx(r.Context())
 | |
| 	if err != nil {
 | |
| 		abortWithError(w, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	notifyBotIfNeeded(fi.Room)
 | |
| }
 | 
