Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions backend/cmd/web/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,67 +6,71 @@ import (
"os"

"letsgo/internal/auth"
"letsgo/internal/config"
"letsgo/internal/db"
"letsgo/internal/external"
"letsgo/internal/game"
"letsgo/internal/live"
"letsgo/internal/mdw"
"letsgo/internal/repo"
"letsgo/internal/token"
"letsgo/pkg/jwt/v2"

"letsgo/internal/config"

"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)

func main() {
appCfg, err := config.Load()
if err != nil {
panic(err)
}
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
slog.SetDefault(logger)

cfg, err := config.Load()
if err != nil {
panic(err)
}
postgres, redis, err := db.Open(cfg.DB)
postgres, redis, err := db.Open(appCfg.DB)
if err != nil {
panic(err)
}
defer postgres.Close()

store := repo.NewStore(postgres, redis)

accessManager, err := jwt.NewManager(cfg.Auth.AccSecret,
cfg.Auth.AccTTL, cfg.Auth.Issuer, cfg.Auth.Audience, token.UserPayload{})
accessManager, err := jwt.NewManager(appCfg.Auth.AccSecret,
appCfg.Auth.AccTTL, appCfg.Auth.Issuer, appCfg.Auth.Audience, token.UserPayload{})
if err != nil {
panic(err)
}
authModule := auth.NewModule(store.User, store.KVStore, accessManager, cfg.Auth)
authModule := auth.NewModule(store.User, store.KVStore, accessManager, appCfg.Auth)

const userCtxKey mdw.ContextKey = "userPayload"
userAccMdw := mdw.AccessMdw(accessManager, cfg.Auth.AccCookieName, cfg.Auth.AccTTL, userCtxKey)
userAccMdw := mdw.AccessMdw(accessManager, appCfg.Auth.AccCookieName, appCfg.Auth.AccTTL, userCtxKey)

r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)

games := game.NewRegistry()
games.Register("connect4", game.NewConnect4())
games.Register("tictactoe", game.NewTicTacToe())

r.Route("/api", func(api chi.Router) {
api.Use(middleware.Logger)
api.Mount("/auth", authModule.Router())
api.Mount("/live", live.Router(userAccMdw, userCtxKey, cfg.WS))
api.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
})

api.Mount("/auth", authModule.Router())
api.Mount("/live", live.Router(userAccMdw, userCtxKey, games, appCfg.WS))
})

// static pages
r.Get("/stat/*", external.StaticPageHandler(cfg.StaticPages))
r.Get("/stat/*", external.StaticPageHandler(appCfg.StaticPages))

//frontend
// frontend
// r.Mount("/", external.FrontendRevProxy(cfg.FrontendUrl))

println("---Server start---")
Expand Down
111 changes: 111 additions & 0 deletions backend/internal/game/connect4.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package game

import (
"encoding/json"
"fmt"
)

type Connect4 struct {
baseGame
board [6][7]int
}

func NewConnect4() Factory {
return func(creator string, payload json.RawMessage) (Game, error) {
game := &Connect4{
baseGame: newBase(2, "connect4"),
board: [6][7]int{},
}
game.Players = []string{creator}
game.Turn = 0
return game, nil
}
}

func (c *Connect4) Move(sender string, payload json.RawMessage) (*GameState, error) {
mv, idx, err := c.validateMove(sender, payload)
if err != nil {
return nil, err
}

if mv.To.Col < 0 || mv.To.Col > 6 {
return nil, fmt.Errorf("invalid move")
}

// Find the first empty row in the column
var droppedRow int = -1
for row := 5; row >= 0; row-- {
if c.board[row][mv.To.Col] == 0 {
c.board[row][mv.To.Col] = idx + 1
droppedRow = row
break
}
}

if droppedRow == -1 {
return nil, fmt.Errorf("invalid move")
}

if win := c.checkWinner(droppedRow, mv.To.Col); win != 0 {
c.Status = StatusWin
c.Winner = c.Players[win-1]
} else if c.checkDraw() {
c.Status = StatusDraw
}

c.Turn = 1 - c.Turn
return c.State(), nil
}

func (c *Connect4) State() *GameState {
return c.state(c.board)
}

func (c *Connect4) checkWinner(startRow, startCol int) int {
player := c.board[startRow][startCol]
if player == 0 {
return 0
}

directions := [][2]int{{0, 1}, {1, 0}, {1, 1}, {1, -1}}

for _, dir := range directions {
count := 1
for i := 1; i < 4; i++ {
row, col := startRow+dir[0]*i, startCol+dir[1]*i
if row >= 0 && row < 6 && col >= 0 && col < 7 && c.board[row][col] == player {
count++
} else {
break
}
}
for i := 1; i < 4; i++ {
row, col := startRow-dir[0]*i, startCol-dir[1]*i
if row >= 0 && row < 6 && col >= 0 && col < 7 && c.board[row][col] == player {
count++
} else {
break
}
}
if count >= 4 {
return player
}
}
return 0
}

func (c *Connect4) checkDraw() bool {
for col := range 7 {
if c.board[0][col] == 0 {
return false
}
}
return true
}

func (c *Connect4) Tick() (*GameState, bool) {
if c.handleTimeout() {
return c.State(), true
}
return nil, false
}
35 changes: 35 additions & 0 deletions backend/internal/game/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package game

import (
"encoding/json"
"fmt"
)

type Factory func(creator string, payload json.RawMessage) (Game, error)

// GameInfo just in case we need payload for creation
type GameInfo struct {
Factory Factory
}

type Registry struct {
games map[string]GameInfo
}

func NewRegistry() *Registry {
return &Registry{games: make(map[string]GameInfo)}
}

func (r *Registry) Register(name string, factory Factory) {
r.games[name] = GameInfo{
Factory: factory,
}
}

func (r *Registry) Create(name, creator string, payload json.RawMessage) (Game, error) {
info, ok := r.games[name]
if !ok {
return nil, fmt.Errorf("game type not supported: %s", name)
}
return info.Factory(creator, payload)
}
Loading