TP03
This commit is contained in:
261
battleship_server.py
Normal file
261
battleship_server.py
Normal file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Bataille Navale - Serveur LAN (arbitre)
|
||||
Lance le serveur, attend 2 clients, échange messages JSON ligne par ligne.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import json
|
||||
import time
|
||||
|
||||
HOST = "0.0.0.0"
|
||||
PORT = 5000
|
||||
|
||||
# Board constants
|
||||
ROWS = 10
|
||||
COLS = 10
|
||||
SHIPS_DEF = {
|
||||
"Carrier": 5,
|
||||
"Battleship": 4,
|
||||
"Cruiser": 3,
|
||||
"Submarine": 3,
|
||||
"Destroyer": 2
|
||||
}
|
||||
|
||||
# Helper: send JSON line
|
||||
def send_line(conn, data):
|
||||
try:
|
||||
conn.sendall((json.dumps(data) + "\n").encode())
|
||||
except Exception as e:
|
||||
print("Send error:", e)
|
||||
|
||||
# Helper: read JSON line from file-like socket
|
||||
def recv_line(f):
|
||||
line = f.readline()
|
||||
if not line:
|
||||
return None
|
||||
return json.loads(line.strip())
|
||||
|
||||
class Player:
|
||||
def __init__(self, conn, addr, idx):
|
||||
self.conn = conn
|
||||
self.f = conn.makefile(mode="r", encoding="utf-8")
|
||||
self.addr = addr
|
||||
self.idx = idx
|
||||
self.ready = False
|
||||
self.board_ships = None # dict of ship_id -> list of (r,c)
|
||||
self.hits_received = set() # positions hit by opponent
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self.f.close()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
self.conn.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
class GameServer:
|
||||
def __init__(self, host, port):
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.bind((host, port))
|
||||
self.sock.listen(4)
|
||||
print(f"[SERVER] Listening on {host}:{port}")
|
||||
self.players = []
|
||||
self.lock = threading.Lock()
|
||||
self.turn = 0 # player index whose turn it is
|
||||
|
||||
def accept_players(self):
|
||||
while len(self.players) < 2:
|
||||
conn, addr = self.sock.accept()
|
||||
with self.lock:
|
||||
idx = len(self.players)
|
||||
player = Player(conn, addr, idx)
|
||||
self.players.append(player)
|
||||
print(f"[SERVER] Player {idx+1} connected from {addr}")
|
||||
threading.Thread(target=self.handle_player, args=(player,), daemon=True).start()
|
||||
|
||||
# when two players connected start handshake
|
||||
self.broadcast({"type": "info", "msg": "2 players connected. Place your ships."})
|
||||
# ask for placements
|
||||
self.broadcast({"type": "start_place", "ships": SHIPS_DEF})
|
||||
|
||||
def broadcast(self, data):
|
||||
with self.lock:
|
||||
for p in list(self.players):
|
||||
try:
|
||||
send_line(p.conn, data)
|
||||
except:
|
||||
pass
|
||||
|
||||
def handle_player(self, player):
|
||||
try:
|
||||
send_line(player.conn, {"type": "welcome", "player_idx": player.idx, "msg": f"You are player {player.idx+1}"})
|
||||
while True:
|
||||
data = recv_line(player.f)
|
||||
if data is None:
|
||||
print(f"[SERVER] Player {player.idx+1} disconnected")
|
||||
break
|
||||
self.process_message(player, data)
|
||||
except Exception as e:
|
||||
print("Player handler error:", e)
|
||||
finally:
|
||||
player.close()
|
||||
|
||||
def process_message(self, player, data):
|
||||
t = data.get("type")
|
||||
if t == "place":
|
||||
# expects 'ships': {name: [[r,c],...], ...}
|
||||
ships = data.get("ships")
|
||||
if not self.validate_placement(ships):
|
||||
send_line(player.conn, {"type": "error", "msg": "Invalid placement"})
|
||||
return
|
||||
player.board_ships = ships
|
||||
player.ready = True
|
||||
send_line(player.conn, {"type": "placed_ok"})
|
||||
print(f"[SERVER] Player {player.idx+1} placed ships")
|
||||
# If both ready, start game
|
||||
if all(p.ready for p in self.players) and len(self.players) >= 2:
|
||||
time.sleep(0.2)
|
||||
self.start_game()
|
||||
elif t == "shot":
|
||||
# only process if both ready
|
||||
if not all(p.ready for p in self.players):
|
||||
send_line(player.conn, {"type": "error", "msg": "Both players not ready"})
|
||||
return
|
||||
if player.idx != self.turn:
|
||||
send_line(player.conn, {"type": "error", "msg": "Not your turn"})
|
||||
return
|
||||
r = data.get("r")
|
||||
c = data.get("c")
|
||||
self.handle_shot(player.idx, r, c)
|
||||
elif t == "forfeit":
|
||||
# opponent wins
|
||||
winner = 1 - player.idx
|
||||
self.end_game(winner, reason=f"Player {player.idx+1} forfeited")
|
||||
else:
|
||||
send_line(player.conn, {"type": "error", "msg": "Unknown message type"})
|
||||
|
||||
def validate_placement(self, ships):
|
||||
# ships is a dict name->list of [r,c]
|
||||
if not isinstance(ships, dict):
|
||||
return False
|
||||
occupied = set()
|
||||
for name, coords in ships.items():
|
||||
if name not in SHIPS_DEF:
|
||||
return False
|
||||
if not isinstance(coords, list) or len(coords) != SHIPS_DEF[name]:
|
||||
return False
|
||||
# each coord valid and not overlapping, must form straight contiguous line
|
||||
rows = [p[0] for p in coords]
|
||||
cols = [p[1] for p in coords]
|
||||
if any(not (0 <= rr < ROWS and 0 <= cc < COLS) for rr, cc in coords):
|
||||
return False
|
||||
# contiguous line check: either same row or same col
|
||||
if not (len(set(rows)) == 1 or len(set(cols)) == 1):
|
||||
return False
|
||||
# ensure consecutive
|
||||
if len(set(rows)) == 1:
|
||||
sorted_cols = sorted(cols)
|
||||
if sorted_cols != list(range(min(cols), max(cols)+1)):
|
||||
return False
|
||||
else:
|
||||
sorted_rows = sorted(rows)
|
||||
if sorted_rows != list(range(min(rows), max(rows)+1)):
|
||||
return False
|
||||
for p in coords:
|
||||
if tuple(p) in occupied:
|
||||
return False
|
||||
occupied.add(tuple(p))
|
||||
return True
|
||||
|
||||
def start_game(self):
|
||||
# tell players game starts and whose turn
|
||||
self.turn = 0
|
||||
for p in self.players:
|
||||
send_line(p.conn, {"type": "start_game", "your_idx": p.idx})
|
||||
self.notify_turn()
|
||||
|
||||
def notify_turn(self):
|
||||
# notify both players whose turn it is
|
||||
for p in self.players:
|
||||
send_line(p.conn, {"type": "turn", "current": self.turn})
|
||||
|
||||
def handle_shot(self, shooter_idx, r, c):
|
||||
shooter = self.players[shooter_idx]
|
||||
target = self.players[1 - shooter_idx]
|
||||
pos = (r, c)
|
||||
# check bounds
|
||||
if not (0 <= r < ROWS and 0 <= c < COLS):
|
||||
send_line(shooter.conn, {"type": "error", "msg": "Shot out of bounds"})
|
||||
return
|
||||
# if already shot there? check by looking at hits_received of target
|
||||
if pos in target.hits_received:
|
||||
send_line(shooter.conn, {"type": "error", "msg": "Position already shot"})
|
||||
return
|
||||
# mark hit
|
||||
target.hits_received.add(pos)
|
||||
# determine result
|
||||
hit_ship = None
|
||||
for name, coords in target.board_ships.items():
|
||||
coords_t = [tuple(p) for p in coords]
|
||||
if pos in coords_t:
|
||||
hit_ship = name
|
||||
break
|
||||
if hit_ship is None:
|
||||
# miss
|
||||
send_line(shooter.conn, {"type": "result", "res": "miss", "r": r, "c": c})
|
||||
send_line(target.conn, {"type": "update", "opponent_shot": {"r": r, "c": c, "res": "miss"}})
|
||||
# switch turn
|
||||
self.turn = 1 - self.turn
|
||||
self.notify_turn()
|
||||
else:
|
||||
# check if ship sunk
|
||||
coords_t = [tuple(p) for p in target.board_ships[hit_ship]]
|
||||
sunk = all(p in target.hits_received for p in coords_t)
|
||||
if sunk:
|
||||
send_line(shooter.conn, {"type": "result", "res": "sunk", "ship": hit_ship, "r": r, "c": c})
|
||||
send_line(target.conn, {"type": "update", "opponent_shot": {"r": r, "c": c, "res": "sunk", "ship": hit_ship}})
|
||||
# check win: all ships coords in hits_received?
|
||||
all_coords = []
|
||||
for coords in target.board_ships.values():
|
||||
all_coords.extend([tuple(p) for p in coords])
|
||||
if set(all_coords).issubset(target.hits_received):
|
||||
# shooter wins
|
||||
self.end_game(shooter_idx, reason="All ships sunk")
|
||||
return
|
||||
else:
|
||||
# shooter keeps the turn after hit (classic rule variations exist; we keep shooter on turn)
|
||||
self.notify_turn()
|
||||
else:
|
||||
send_line(shooter.conn, {"type": "result", "res": "hit", "r": r, "c": c})
|
||||
send_line(target.conn, {"type": "update", "opponent_shot": {"r": r, "c": c, "res": "hit"}})
|
||||
# shooter keeps turn
|
||||
self.notify_turn()
|
||||
|
||||
def end_game(self, winner_idx, reason=""):
|
||||
print(f"[SERVER] Game ended. Winner: Player {winner_idx+1}. Reason: {reason}")
|
||||
for p in self.players:
|
||||
send_line(p.conn, {"type": "end", "winner": winner_idx, "reason": reason})
|
||||
# close sockets after a short pause
|
||||
time.sleep(1.0)
|
||||
for p in self.players:
|
||||
try:
|
||||
p.close()
|
||||
except:
|
||||
pass
|
||||
# reset server for new game? here we exit
|
||||
print("[SERVER] Shutting down.")
|
||||
try:
|
||||
self.sock.close()
|
||||
except:
|
||||
pass
|
||||
exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
server = GameServer(HOST, PORT)
|
||||
server.accept_players()
|
||||
Reference in New Issue
Block a user