Browse Source

docs(chess): adding the resources

making a bonus question mandatory
pull/2048/head
miguel 1 year ago committed by Niccolò Primo
parent
commit
0ee0e52c1c
  1. 3
      subjects/mobile-dev/chess/README.md
  2. 6
      subjects/mobile-dev/chess/audit/README.md
  3. 15
      subjects/mobile-dev/chess/resources/Dockerfile
  4. 109
      subjects/mobile-dev/chess/resources/README.md
  5. 11
      subjects/mobile-dev/chess/resources/go.mod
  6. 10
      subjects/mobile-dev/chess/resources/go.sum
  7. 306
      subjects/mobile-dev/chess/resources/main.go

3
subjects/mobile-dev/chess/README.md

@ -26,6 +26,7 @@ Make sure that your app has the following requirements:
- Must have the ability for players to invite each other to a game or join a public game.
- Must have a waiting room where players can wait for opponents and initiate game sessions.
- Must have a notification system to alert players when it is their turn to make a move.
- When the game reaches a terminal state (checkmate, stalemate, or draw), you need to send a message to both players indicating that the game is over and which player has won.
### Hints
@ -37,8 +38,6 @@ Make sure that your app has the following requirements:
### Bonus
- **Game Over**, when the game reaches a terminal state (checkmate, stalemate, or draw), we can send a message to both players indicating that the game is over.
- **Player X Won**, after determining the winner of the game, send a message to both players indicating which player has won.
- **Restart**, to allow players to restart the game
- **Story Mode**: Design a single-player story mode where players can engage in chess matches against AI opponents, progressing through a series of challenges or levels.
- **Bots**: Include AI-powered bots that players can play against in offline mode or when waiting for online opponents.

6
subjects/mobile-dev/chess/audit/README.md

@ -32,14 +32,12 @@
###### Does the game end and the player who mated the other win?
###### When the game reaches a terminal state (checkmate, stalemate, or draw), does the app send a message to both players indicating that the game is over and the reason of being over?
###### Does the backend generate a unique identifier (UUID) for each game session when a player joins the waiting room, ensuring proper identification and management of individual game sessions?
#### Bonus
###### +When the game reaches a terminal state (checkmate, stalemate, or draw), does the app send a message to both players indicating that the game is over and the reason of being over?
###### +After determining the winner of the game, does the app send a message to both players indicating which player has won?
###### +Does the app allow players to restart the game?
###### +Is there a single-player story mode where players can engage in chess matches against AI opponents, progressing through a series of challenges or levels?

15
subjects/mobile-dev/chess/resources/Dockerfile

@ -0,0 +1,15 @@
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"]

109
subjects/mobile-dev/chess/resources/README.md

@ -0,0 +1,109 @@
# ♟ 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).

11
subjects/mobile-dev/chess/resources/go.mod

@ -0,0 +1,11 @@
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

10
subjects/mobile-dev/chess/resources/go.sum

@ -0,0 +1,10 @@
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=

306
subjects/mobile-dev/chess/resources/main.go

@ -0,0 +1,306 @@
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
}
Loading…
Cancel
Save