diff --git a/subjects/mobile-dev/chess/README.md b/subjects/mobile-dev/chess/README.md index a86117ffd..14ac852d4 100644 --- a/subjects/mobile-dev/chess/README.md +++ b/subjects/mobile-dev/chess/README.md @@ -12,7 +12,7 @@ Today, chess continues to be a beloved pastime, played by millions of people wor ### Instructions -Your task is to develop a mobile app that allows users to play chess with each other. This is a fullstack app so you will need to implement both backend and frontend. You may use any desired backend technology as long as you're following the [backend routes.](resources/README.md). +Your task is to develop a mobile app that allows users to play chess with each other. This is a fullstack app so you will need to implement both backend and frontend. You may use any desired backend technology as long as you're following the [backend routes.](https://github.com/alem-01/chess). Make sure that your app has the following requirements: diff --git a/subjects/mobile-dev/chess/resources/Dockerfile b/subjects/mobile-dev/chess/resources/Dockerfile deleted file mode 100644 index 060fd5c0d..000000000 --- a/subjects/mobile-dev/chess/resources/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM golang:1.19.1-buster AS builder - -WORKDIR /app -COPY go.mod . -COPY go.sum . -RUN go mod download -COPY . . -RUN go build -o /app/main . - - -FROM ubuntu:20.04 - -WORKDIR /app -COPY --from=builder /app/main . -CMD ["/app/main"] diff --git a/subjects/mobile-dev/chess/resources/README.md b/subjects/mobile-dev/chess/resources/README.md deleted file mode 100644 index 8c3d34e69..000000000 --- a/subjects/mobile-dev/chess/resources/README.md +++ /dev/null @@ -1,109 +0,0 @@ -# ♟️ chess websocket server - -This project represents a simple websocket server to play chess. - -- [Local setup](#local-setup) -- [Interacting with websocket server to play chess](#interacting-with-websocket-server-to-play-chess) -- [Chess moves](#chess-moves) -- [Features](#features) -- [Limitations](#limitations) -- [References](#references) - -## Local setup - -There are two major ways to locally setup the project to have it up and running: - -- [Build from source](#build-from-source) -- [Build with Docker](#build-from-docker) - -### Build from source - -Requirements: - -- `golang 1.19` -- `PORT` environment exported - -Export `PORT` environmental variable. The port can be any port you would like. - -``` -export PORT=8080 -``` - -Install dependencies: - -```bash -go mod download -``` - -Run the project: - -```bash -go run . -``` - -### Build with Docker - -Requirements: - -- `docker` - -Build the docker image: - -```bash -docker build -t chess . -``` - -Run the project with `PORT` env set: - -```bash -docker run -d -e PORT=8080 -p 8080:8080 chess -``` - -## Interacting with websocket server to play chess - -To play chess, players need to be matched with other player. - -Firstly, connect using websocket to the endpoint `ws://localhost:8080/rooms`. -After successful connection, client (e.g. player) needs to wait for response from the server. -The response will be of the following type: - -``` -9c954450-ad7b-4dcc-ab2f-6c556c0835ef -``` - -This is the UUID of the game session. - -Secondly, when UUID is received connect to the next endpoint - `ws://localhost:8080/rooms/9c954450-ad7b-4dcc-ab2f-6c556c0835ef`. -The UUID should be placed after `/rooms/` path. - -After successful connection, the client will receive its own chess color, either `white` or `black`. - -From now on, players can exchange moves to play chess. - -> If only one user connects, then the server will not send any color information. -> That's because the server waits for second player to join. After both players join, -> the players will receive own colors. - -## Chess moves - -The players exchange with text messages to indicate their chess move. -As of chess move notation - [Long algebraic notation](long-algebraic-notation) is used. - -Players need to send chess move messages strictly by the defined notion. If the chess engine fails -to identify the chess move, then the player will receive an error message - the user will be prompted -to send a valid move. - -[long-algebraic-notation]: https://en.wikipedia.org/wiki/Algebraic_notation_(chess)#Long_algebraic_notation - -## Features - -- Supports many concurrent games. - -## Limitations - -- No player reconnect mechanism. If one of user's connection interrupts, then the game session will end. - -## References - -- Chess Engine - [notnil/chess](https://github.com/notnil/chess). -- Websocket library - [gorilla/websocket](https://github.com/gorilla/websocket). diff --git a/subjects/mobile-dev/chess/resources/go.mod b/subjects/mobile-dev/chess/resources/go.mod deleted file mode 100644 index 384741e7e..000000000 --- a/subjects/mobile-dev/chess/resources/go.mod +++ /dev/null @@ -1,11 +0,0 @@ -module github.com/alem-01/chess - -go 1.19 - -require ( - github.com/google/uuid v1.3.0 - github.com/gorilla/mux v1.8.0 - github.com/gorilla/websocket v1.5.0 -) - -require github.com/notnil/chess v1.9.0 diff --git a/subjects/mobile-dev/chess/resources/go.sum b/subjects/mobile-dev/chess/resources/go.sum deleted file mode 100644 index 58ac2fd9a..000000000 --- a/subjects/mobile-dev/chess/resources/go.sum +++ /dev/null @@ -1,10 +0,0 @@ -github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca h1:kWzLcty5V2rzOqJM7Tp/MfSX0RMSI1x4IOLApEefYxA= -github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/notnil/chess v1.9.0 h1:YMxR5kUVjtwcuFptGU0/3q7eG3MSHQNbg0VUekvRKV0= -github.com/notnil/chess v1.9.0/go.mod h1:cRuJUIBFq9Xki05TWHJxHYkC+fFpq45IWwk94DdlCrA= diff --git a/subjects/mobile-dev/chess/resources/main.go b/subjects/mobile-dev/chess/resources/main.go deleted file mode 100644 index f3ef01d36..000000000 --- a/subjects/mobile-dev/chess/resources/main.go +++ /dev/null @@ -1,306 +0,0 @@ -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 -}