267 lines
5.4 KiB
Go
267 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type Player struct {
|
|
ID string
|
|
Conn net.Conn
|
|
}
|
|
|
|
type GameRoom struct {
|
|
White *Player
|
|
Black *Player
|
|
Game *Game
|
|
Mutex sync.Mutex
|
|
}
|
|
|
|
type Server struct {
|
|
Listener net.Listener
|
|
Waiting []*Player
|
|
Rooms map[string]*GameRoom
|
|
RoomMutex sync.Mutex
|
|
RoomCount int
|
|
}
|
|
|
|
func NewServer() *Server {
|
|
return &Server{
|
|
Rooms: make(map[string]*GameRoom),
|
|
}
|
|
}
|
|
|
|
func (s *Server) Start(addr string) error {
|
|
var err error
|
|
s.Listener, err = net.Listen("tcp", addr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to listen on %s: %v", addr, err)
|
|
}
|
|
fmt.Printf("=== CHESS SERVER ===\n")
|
|
fmt.Printf("Listening on %s\n", addr)
|
|
fmt.Printf("Type 'status' to show active games, 'quit' to exit.\n\n")
|
|
go func() {
|
|
for {
|
|
conn, err := s.Listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go s.handleClient(conn)
|
|
}
|
|
}()
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
switch line {
|
|
case "status":
|
|
s.RoomMutex.Lock()
|
|
fmt.Printf("Active games: %d | Waiting: %d\n", len(s.Rooms), len(s.Waiting))
|
|
for _, room := range s.Rooms {
|
|
fmt.Printf(" %s vs %s (move %d)\n", room.White.ID, room.Black.ID, room.Game.MoveNum)
|
|
}
|
|
s.RoomMutex.Unlock()
|
|
case "quit", "q":
|
|
s.Listener.Close()
|
|
fmt.Println("Server shutting down.")
|
|
return nil
|
|
case "":
|
|
continue
|
|
default:
|
|
fmt.Printf("Unknown: %s (try 'status' or 'quit')\n", line)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleClient(conn net.Conn) {
|
|
defer conn.Close()
|
|
reader := bufio.NewReader(conn)
|
|
playerID := fmt.Sprintf("Player-%d", s.RoomCount+1)
|
|
fmt.Printf("[+] %s connected (%s)\n", playerID, conn.RemoteAddr())
|
|
player := &Player{ID: playerID, Conn: conn}
|
|
fmt.Fprintln(conn, "CHESS-SERVER v1.0")
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return
|
|
}
|
|
if strings.TrimSpace(line) != "HELLO" {
|
|
fmt.Fprintln(conn, "ERROR expected HELLO")
|
|
return
|
|
}
|
|
s.RoomMutex.Lock()
|
|
s.Waiting = append(s.Waiting, player)
|
|
foundOpponent := false
|
|
for i, p := range s.Waiting {
|
|
if p == player {
|
|
continue
|
|
}
|
|
s.Waiting = append(s.Waiting[:i], s.Waiting[i+1:]...)
|
|
foundOpponent = true
|
|
s.RoomCount++
|
|
roomID := fmt.Sprintf("game-%d", s.RoomCount)
|
|
room := &GameRoom{
|
|
White: player,
|
|
Black: p,
|
|
Game: NewGame(),
|
|
}
|
|
s.Rooms[roomID] = room
|
|
fmt.Fprintf(player.Conn, "MATCHED %s WHITE\n", roomID)
|
|
fmt.Fprintf(p.Conn, "MATCHED %s BLACK\n", roomID)
|
|
boardStr := room.Game.Render()
|
|
fmt.Fprintf(player.Conn, "BOARD\n%s\n", boardStr)
|
|
fmt.Fprintf(p.Conn, "BOARD\n%s\n", boardStr)
|
|
go s.gameLoop(room)
|
|
break
|
|
}
|
|
s.RoomMutex.Unlock()
|
|
if !foundOpponent {
|
|
fmt.Fprintln(player.Conn, "WAITING")
|
|
fmt.Printf(" %s waiting for opponent...\n", playerID)
|
|
}
|
|
}
|
|
|
|
func (s *Server) gameLoop(room *GameRoom) {
|
|
whiteReader := bufio.NewReader(room.White.Conn)
|
|
blackReader := bufio.NewReader(room.Black.Conn)
|
|
fmt.Printf("[=] Game: %s (W) vs %s (B)\n", room.White.ID, room.Black.ID)
|
|
whiteCh := make(chan string, 4)
|
|
blackCh := make(chan string, 4)
|
|
doneCh := make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(whiteCh)
|
|
for {
|
|
line, err := whiteReader.ReadString('\n')
|
|
if err != nil {
|
|
close(doneCh)
|
|
return
|
|
}
|
|
line = strings.TrimSpace(line)
|
|
if line == "RESIGN" {
|
|
room.Mutex.Lock()
|
|
room.Game.Over = true
|
|
room.Game.Result = BlackWins
|
|
room.Mutex.Unlock()
|
|
close(doneCh)
|
|
return
|
|
}
|
|
if line == "QUIT" {
|
|
close(doneCh)
|
|
return
|
|
}
|
|
whiteCh <- line
|
|
}
|
|
}()
|
|
go func() {
|
|
defer close(blackCh)
|
|
for {
|
|
line, err := blackReader.ReadString('\n')
|
|
if err != nil {
|
|
close(doneCh)
|
|
return
|
|
}
|
|
line = strings.TrimSpace(line)
|
|
if line == "RESIGN" {
|
|
room.Mutex.Lock()
|
|
room.Game.Over = true
|
|
room.Game.Result = WhiteWins
|
|
room.Mutex.Unlock()
|
|
close(doneCh)
|
|
return
|
|
}
|
|
if line == "QUIT" {
|
|
close(doneCh)
|
|
return
|
|
}
|
|
blackCh <- line
|
|
}
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-doneCh:
|
|
s.finishRoom(room)
|
|
return
|
|
default:
|
|
}
|
|
room.Mutex.Lock()
|
|
if room.Game.Over {
|
|
room.Mutex.Unlock()
|
|
s.sendBoard(room)
|
|
s.finishRoom(room)
|
|
return
|
|
}
|
|
var moveStr string
|
|
var ok bool
|
|
if room.Game.Turn == White {
|
|
select {
|
|
case moveStr, ok = <-whiteCh:
|
|
if !ok {
|
|
room.Mutex.Unlock()
|
|
s.finishRoom(room)
|
|
return
|
|
}
|
|
case <-doneCh:
|
|
room.Mutex.Unlock()
|
|
s.finishRoom(room)
|
|
return
|
|
}
|
|
} else {
|
|
select {
|
|
case moveStr, ok = <-blackCh:
|
|
if !ok {
|
|
room.Mutex.Unlock()
|
|
s.finishRoom(room)
|
|
return
|
|
}
|
|
case <-doneCh:
|
|
room.Mutex.Unlock()
|
|
s.finishRoom(room)
|
|
return
|
|
}
|
|
}
|
|
_, err := room.Game.MakeMove(moveStr)
|
|
over := room.Game.Over
|
|
room.Mutex.Unlock()
|
|
if err != nil {
|
|
if room.Game.Turn == White {
|
|
fmt.Fprintf(room.White.Conn, "ERROR %v\n", err)
|
|
} else {
|
|
fmt.Fprintf(room.Black.Conn, "ERROR %v\n", err)
|
|
}
|
|
continue
|
|
}
|
|
fmt.Printf(" %s: %s\n", moveStr, room.Game.Turn)
|
|
s.sendBoard(room)
|
|
if over {
|
|
s.finishRoom(room)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) sendBoard(room *GameRoom) {
|
|
boardStr := room.Game.Render()
|
|
fmt.Fprintf(room.White.Conn, "BOARD\n%s\n", boardStr)
|
|
fmt.Fprintf(room.Black.Conn, "BOARD\n%s\n", boardStr)
|
|
}
|
|
|
|
func (s *Server) finishRoom(room *GameRoom) {
|
|
s.RoomMutex.Lock()
|
|
for k, r := range s.Rooms {
|
|
if r == room {
|
|
delete(s.Rooms, k)
|
|
break
|
|
}
|
|
}
|
|
s.RoomMutex.Unlock()
|
|
room.White.Conn.Close()
|
|
room.Black.Conn.Close()
|
|
fmt.Printf("[=] Game ended: %s vs %s\n", room.White.ID, room.Black.ID)
|
|
}
|
|
|