You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

307 lines
6.7 KiB

package main
import (
"log"
"math/rand"
"net/http"
"os"
"sync"
"sync/atomic"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/notnil/chess"
)
const (
ColorWhite = "white"
ColorBlack = "black"
)
type ChessHub struct {
ChanNewUser chan string
ChanWaitingRoom map[string]chan bool // key is UserID
ChanRoomsUserJoined chan *ChessRoom
ChanUserJoined map[string]chan bool // key is RoomID
CountUserJoined map[string]*atomic.Int32 // key is RoomID
Rooms map[string]*ChessRoom // key is RoomID
Clients map[string]*ChessClient // key is UserID
}
func NewChessHub() *ChessHub {
return &ChessHub{
ChanNewUser: make(chan string),
ChanWaitingRoom: make(map[string]chan bool),
ChanRoomsUserJoined: make(chan *ChessRoom),
ChanUserJoined: make(map[string]chan bool),
CountUserJoined: make(map[string]*atomic.Int32),
Rooms: make(map[string]*ChessRoom),
Clients: make(map[string]*ChessClient),
}
}
func (c *ChessHub) RunWorkerNewUser() {
queue := make([]string, 0)
for userID := range c.ChanNewUser {
queue = append(queue, userID)
if len(queue) >= 2 {
var (
userID1 = queue[0]
userID2 = queue[1]
roomID = uuid.NewString()
user1Color = rand.Intn(2) == 0
user2Color = !user1Color
)
queue = queue[2:]
c.NewRoom(roomID)
c.NotifyUserForPickedRoom(roomID, userID1, user1Color)
c.NotifyUserForPickedRoom(roomID, userID2, user2Color)
}
}
}
func (c *ChessHub) RunWorkerUserJoined() {
var wg sync.WaitGroup
runWorkerUserJoined := func(roomID string, ch chan bool) {
for range ch {
count := c.CountUserJoined[roomID]
count.Add(1)
if count.Load() == 2 {
c.Rooms[roomID].Game = chess.NewGame(chess.UseNotation(chess.LongAlgebraicNotation{}))
c.NotifyUsers(roomID)
delete(c.CountUserJoined, roomID)
return
}
}
close(ch)
wg.Done()
}
for room := range c.ChanRoomsUserJoined {
wg.Add(1)
go runWorkerUserJoined(room.ID, c.ChanUserJoined[room.ID])
}
wg.Wait()
}
func (c *ChessHub) NotifyUsers(roomID string) {
for _, user := range c.Rooms[roomID].Clients {
user.ChanNotifyWhenReady <- true
}
}
func (c *ChessHub) NewRoom(roomID string) {
c.Rooms[roomID] = &ChessRoom{
ID: roomID,
Clients: make(map[string]*ChessClient),
}
c.ChanUserJoined[roomID] = make(chan bool)
c.CountUserJoined[roomID] = &atomic.Int32{}
c.ChanRoomsUserJoined <- c.Rooms[roomID]
}
func (c *ChessHub) NewUser(userID string) {
c.ChanWaitingRoom[userID] = make(chan bool)
c.ChanNewUser <- userID
}
func (c *ChessHub) UserJoined(userID string, conn *websocket.Conn) error {
client, ok := c.Clients[userID]
if !ok {
// TODO
// return err
return nil
}
if client.ActiveConn != nil {
// TODO
// return err
return nil
}
client.ActiveConn = conn
client.ChanNotifyWhenReady = make(chan bool)
c.ChanUserJoined[client.RoomID] <- true
return nil
}
func (c *ChessHub) NotifyUserForPickedRoom(roomID, userID string, color bool) {
c.Clients[userID] = &ChessClient{
ID: userID,
RoomID: roomID,
}
c.Rooms[roomID].Clients[userID] = c.Clients[userID]
switch color {
case true:
c.Clients[userID].Color = ColorWhite
case false:
c.Clients[userID].Color = ColorBlack
}
c.ChanWaitingRoom[userID] <- true
}
func (c *ChessHub) WaitForRoom(userID string) {
if ch, ok := c.ChanWaitingRoom[userID]; ok {
<-ch
delete(c.ChanWaitingRoom, userID)
}
}
func (c *ChessHub) WaitForOthers(userID string) {
if client, ok := c.Clients[userID]; ok {
<-client.ChanNotifyWhenReady
}
}
func (c *ChessHub) StartGame(userID string) error {
var (
client = c.Clients[userID]
roomID = client.RoomID
movesOrder = []string{ColorWhite, ColorBlack}
game = c.Rooms[roomID].Game
index = 0
)
players, err := c.GetPlayers(roomID)
if err != nil {
return err
}
client.ActiveConn.WriteMessage(websocket.TextMessage, []byte(client.Color))
for game.Outcome() == chess.NoOutcome {
var (
color = movesOrder[index%2]
oppositeColor = movesOrder[(index+1)%2]
)
mt, message, err := players[color].ActiveConn.ReadMessage()
if err != nil || mt == websocket.CloseMessage {
break
}
if err := game.MoveStr(string(message)); err != nil {
players[color].ActiveConn.WriteMessage(websocket.TextMessage, []byte(err.Error()))
continue
}
if players[oppositeColor].ActiveConn == nil {
break
}
players[oppositeColor].ActiveConn.WriteMessage(websocket.TextMessage, message)
index++
}
client.ActiveConn.WriteMessage(websocket.TextMessage, []byte(game.Outcome()))
client.ActiveConn.WriteMessage(websocket.TextMessage, []byte(game.Method().String()))
return nil
}
func (c *ChessHub) GetPlayers(roomID string) (map[string]*ChessClient, error) {
players := make(map[string]*ChessClient)
for _, v := range c.Rooms[roomID].Clients {
players[v.Color] = v
}
return players, nil
}
type ChessRoom struct {
ID string
Clients map[string]*ChessClient
Game *chess.Game
}
type ChessClient struct {
ID string
Color string
RoomID string
ActiveConn *websocket.Conn
ChanNotifyWhenReady chan bool
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type Server struct {
ChessHub *ChessHub
}
func NewServer(chessHub *ChessHub) *Server {
return &Server{
ChessHub: chessHub,
}
}
func (s *Server) PickRoom(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
defer conn.Close()
userID := uuid.NewString()
s.ChessHub.NewUser(userID)
s.ChessHub.WaitForRoom(userID)
conn.WriteMessage(websocket.TextMessage, []byte(userID))
}
func (s *Server) JoinRoom(w http.ResponseWriter, r *http.Request) {
var (
clientID = mux.Vars(r)["client_id"]
)
conn, _ := upgrader.Upgrade(w, r, nil)
defer conn.Close()
defer func() {
client := s.ChessHub.Clients[clientID]
client.ActiveConn = nil
delete(s.ChessHub.Clients, clientID)
delete(s.ChessHub.Rooms, client.RoomID)
}()
s.ChessHub.UserJoined(clientID, conn)
s.ChessHub.WaitForOthers(clientID)
s.ChessHub.StartGame(clientID)
}
func main() {
var (
chessHub = NewChessHub()
server = NewServer(chessHub)
router = mux.NewRouter()
port = getenv("PORT", "8080")
)
go chessHub.RunWorkerNewUser()
go chessHub.RunWorkerUserJoined()
router.HandleFunc("/rooms", server.PickRoom)
router.HandleFunc("/rooms/{client_id}", server.JoinRoom)
http.Handle("/", router)
log.Printf("running chess server on port :%s...", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Println(err)
}
}
func getenv(key, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
}