diff --git a/src/main/client/src/feature/lobby/Lobby.jsx b/src/main/client/src/feature/lobby/Lobby.jsx index ee1ebdc..7453c8b 100644 --- a/src/main/client/src/feature/lobby/Lobby.jsx +++ b/src/main/client/src/feature/lobby/Lobby.jsx @@ -1,6 +1,7 @@ import { useRef, useState, + useEffect, useContext, useCallback, } from "react" @@ -38,6 +39,7 @@ import { import { getZindex, setNewGameOpen, + setOpenGameId, handleLobbyClick, closeLobbyPopup, initialState, @@ -59,26 +61,26 @@ export function Lobby() { let [lobbyState, setLobbyState] = useState(initialState()) let zNewGame = getZindex(lobbyState, "newgame") let [detail, setDetail] = useState("open") - let stompClient = useContext(StompContext) let navigate = useNavigate() let auth = useAuthStore(state => state.auth) let newGameRef = useRef() - let onNewGame = useCallback((d) => doTry(async () => { - let response = await tfetch("/api/create", { - method: "POST", - headers: { - "Authorization": "Bearer " + auth.token, - "Content-Type": "application/json", - }, - body: JSON.stringify(d), + let stompClient = useContext(StompContext) + let initialized = useRef() + useEffect(() => { + if (initialized.current) { + return + } + initialized.current = true + let sub = stompClient.subscribe("/topic/gamestart", (message) => { + let r = JSON.parse(message.body) + if (r.opponent === auth.name) { + navigate(base + "/game/" + r.id) + } }) - let sub = stompClient.subscribe("/topic/game/" + response.id, (message) => { - let game = JSON.parse(message.body) - navigate(base + "/game/" + game.id) + return () => { sub.unsubscribe() - }) - setLobbyState(closeLobbyPopup(lobbyState)) - }), [auth.token, navigate, stompClient, lobbyState]) + } + }, [auth, initialized, stompClient, navigate]) let onStartEdit = useCallback((d) => doTry(async () => { let response = await tfetch("/api/start_edit", { method: "POST", @@ -100,7 +102,6 @@ export function Lobby() { lobbyState={lobbyState} setLobbyState={setLobbyState} newGameRef={newGameRef} - onNewGame={onNewGame} onStartEdit={onStartEdit} /> <button disabled={zNewGame !== 0} className={twJoin( "ml-2 border-2 border-transparent px-4 py-2 rounded-lg", @@ -131,10 +132,23 @@ export function Lobby() { ) } -function NewGameDialog({zNewGame, lobbyState, setLobbyState, onNewGame, onStartEdit, newGameRef}) { +function NewGameDialog({zNewGame, lobbyState, setLobbyState, onStartEdit, newGameRef}) { let dimRef = useRef(9) let timeRef = useRef(10) let [edit, setEdit] = useState(false) + let auth = useAuthStore(state => state.auth) + let onNewGame = useCallback((d) => doTry(async () => { + let response = await tfetch("/api/create", { + method: "POST", + headers: { + "Authorization": "Bearer " + auth.token, + "Content-Type": "application/json", + }, + body: JSON.stringify(d), + }) + let newState = closeLobbyPopup(lobbyState) + setLobbyState(setOpenGameId(newState, response.id)) + }), [auth.token, lobbyState, setLobbyState]) return ( <form onSubmit={(e) => { e.preventDefault() diff --git a/src/main/client/src/feature/lobby/OpenGames.jsx b/src/main/client/src/feature/lobby/OpenGames.jsx index 9ab7fb7..c2f89d6 100644 --- a/src/main/client/src/feature/lobby/OpenGames.jsx +++ b/src/main/client/src/feature/lobby/OpenGames.jsx @@ -3,7 +3,6 @@ import { useState, useEffect, useContext, - useCallback, } from "react" import { FaAngleLeft, @@ -15,9 +14,6 @@ import { import { twJoin, } from "tailwind-merge" -import { - useNavigate, -} from "react-router-dom" import { Form, } from "src/component/Form.jsx" @@ -28,7 +24,6 @@ import { BabyStone, } from "src/component/BabyStone.jsx" import { - base, StompContext, tfetch, doTry, @@ -37,6 +32,7 @@ import { import { getZindex, getAcceptData, + closeLobbyPopup, setAcceptDialogOpen, } from "./lobbyState.js" import { @@ -47,7 +43,6 @@ export function OpenGames({lobbyState, setLobbyState}) { let [openGames, setOpenGames] = useState([]) let acceptableGame = getAcceptData(lobbyState) let stompClient = useContext(StompContext) - let navigate = useNavigate() let auth = useAuthStore(state => state.auth) let initialized = useRef() let acceptDialogRef = useRef() @@ -71,18 +66,7 @@ export function OpenGames({lobbyState, setLobbyState}) { return () => { sub1.unsubscribe() } - }, [auth, initialized, stompClient, navigate]) - let onAccept = useCallback((d) => doTry(async () => { - await tfetch("/api/accept", { - method: "POST", - headers: { - "Authorization": "Bearer " + auth.token, - "Content-Type": "application/json", - }, - body: JSON.stringify(d), - }) - navigate(base + "/game/" + d.game.id) - }), [auth, navigate]) + }, [auth, initialized, stompClient]) return ( <div> <div className="grid grid-cols-[max-content_max-content_max-content]"> @@ -96,10 +80,10 @@ export function OpenGames({lobbyState, setLobbyState}) { key={game.id} /> ))} </div> - <AcceptDialog + <ChallengeDialog lobbyState={lobbyState} + setLobbyState={setLobbyState} acceptableGame={acceptableGame} - onAccept={onAccept} acceptDialogRef={acceptDialogRef} /> </div> ) @@ -138,7 +122,7 @@ function OpenGame({game, onClick}) { ) } -function AcceptDialog({lobbyState, onAccept, acceptableGame, acceptDialogRef}) { +function ChallengeDialog({lobbyState, setLobbyState, acceptableGame, acceptDialogRef}) { let [isFlip, setFlip] = useState(false) let [handi, setHandi] = useState(1) let auth = useAuthStore(state => state.auth) @@ -146,10 +130,20 @@ function AcceptDialog({lobbyState, onAccept, acceptableGame, acceptDialogRef}) { return ( <Form forwardedRef={acceptDialogRef} - onSubmit={() => onAccept({ - game: acceptableGame?.game, - flip: isFlip, - handicap: handi === 1 ? 0 : handi, + onSubmit={() => doTry(async () => { + await tfetch("/api/challenge", { + method: "POST", + headers: { + "Authorization": "Bearer " + auth.token, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + game: acceptableGame?.game, + flip: isFlip, + handicap: handi === 1 ? 0 : handi, + }), + }) + setLobbyState(closeLobbyPopup(lobbyState)) })} style={{ zIndex: zAccept, diff --git a/src/main/client/src/feature/lobby/Requests.jsx b/src/main/client/src/feature/lobby/Requests.jsx index f1b0e39..e7aeea9 100644 --- a/src/main/client/src/feature/lobby/Requests.jsx +++ b/src/main/client/src/feature/lobby/Requests.jsx @@ -38,7 +38,7 @@ export function Requests({lobbyState}) { "Authorization": "Bearer " + auth.token, }, }) - setRequests(r.requests.filter(request => request.gameId === openGameId)) + setRequests(r.requests) }) let sub1 = stompClient.subscribe("/topic/lobby/requests", (message) => { let r = JSON.parse(message.body) @@ -53,10 +53,9 @@ export function Requests({lobbyState}) { } return ( <div> - <div className="grid grid-cols-[max-content_max-content_max-content_max-content]"> + <div className="grid grid-cols-[max-content_max-content_max-content_max-content_max-content]"> {requests.map((request) => ( <Request - lobbyState={lobbyState} request={request} key={request.id} /> ))} @@ -65,45 +64,44 @@ export function Requests({lobbyState}) { ) } -function Request({lobbyState, request}) { +function Request({request}) { let navigate = useNavigate() let auth = useAuthStore(state => state.auth) - let openGameId = lobbyState.openGameId - let classes = twJoin( - "contents", - "*:py-3", - "cursor-pointer *:hover:bg-sky-200 *:hover:text-black", - ) return ( <div onClick={() => { doTry(async () => { - let r = await tfetch("/api/lobby/start", { + await tfetch("/api/lobby/start", { + method: "POST", headers: { - "method": "POST", "Authorization": "Bearer " + auth.token, + "Content-Type": "application/json", }, - body: JSON.stringify({ - gameId: openGameId, - request: request.id, - }), + body: JSON.stringify(request), }) - navigate(base + "/game/" + r.id) + navigate(base + "/game/" + request.game.id) }) }} - className={classes} - key={request.id}> + className={twJoin( + "contents", + "*:py-3", + "cursor-pointer *:hover:bg-sky-200 *:hover:text-black", + )} + key={request.game.id}> <div className="pl-3 pr-1 rounded-l-lg"> - {request.white} + {request.flip ? "B" : "W"}: {request.opponent} + </div> + <div className="px-1"> + {request.flip ? "W" : "B"}: {request.game.user} </div> <div className="px-1"> - {request.black} + {request.game.dim}x{request.game.dim} </div> <div className="px-1"> - {request.dim}x{request.dim} + T: {request.game.timesetting} </div> <div className="pl-1 pr-3 rounded-r-lg"> - H{request.handi} + H: {request.handicap} </div> </div> ) diff --git a/src/main/java/com/bernd/GameController.java b/src/main/java/com/bernd/GameController.java index 8a70cee..f17f131 100644 --- a/src/main/java/com/bernd/GameController.java +++ b/src/main/java/com/bernd/GameController.java @@ -12,6 +12,10 @@ import com.bernd.model.ViewGame; import com.bernd.util.RandomString; import com.bernd.util.SgfCreator; +import java.security.Principal; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -25,9 +29,6 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.server.ResponseStatusException; -import java.security.Principal; -import java.time.LocalDate; - import static com.bernd.util.Auth.getPrincipal; import static com.bernd.util.Util.COLORS; @@ -140,24 +141,43 @@ public OpenGame newGame(@RequestBody OpenGame game) { return result; } - @PostMapping(value = "/api/accept", consumes = "application/json") - public ResponseEntity<?> accept(@RequestBody AcceptRequest acceptRequest) { + @PostMapping(value = "/api/lobby/start", consumes = "application/json") + public ResponseEntity<?> start(@RequestBody AcceptRequest acceptRequest) { String principal = getPrincipal(); - openGames.remove(principal); - OpenGame openGame = openGames.remove(acceptRequest.game().user()); - Game fullGame = games.put(openGame.accept(principal, acceptRequest)); + openGames.remove(acceptRequest.opponent()); + OpenGame openGame = openGames.remove(principal); + Game fullGame = games.put(openGame.accept(acceptRequest)); activeGames.put(ActiveGame.fromGame(fullGame)); Chat chat = chats.get(openGame.id()); ChatMessage startMessage = ChatMessage.createStartMessage(chat, fullGame); chat.messages().add(startMessage); operations.convertAndSend("/topic/chat/" + chat.id(), startMessage); - operations.convertAndSend("/topic/game/" + fullGame.id(), fullGame.toView()); + operations.convertAndSend("/topic/gamestart", Map.of( + "opponent", acceptRequest.opponent(), + "id", openGame.id())); operations.convertAndSend("/topic/lobby/open_games", openGames.games()); operations.convertAndSend("/topic/lobby/active_games", activeGames.games()); return ResponseEntity.ok().build(); } + @ResponseBody + @PostMapping(value = "/api/challenge", consumes = "application/json") + public Map<String, List<AcceptRequest>> challenge(@RequestBody AcceptRequest acceptRequest) { + String principal = getPrincipal(); + openGames.remove(principal); + OpenGame openGame = openGames.addRequest(acceptRequest.game().user(), acceptRequest, principal); + operations.convertAndSend("/topic/lobby/requests", Map.of("requests", openGame.requests())); + return Map.of("requests", openGame.requests()); + } + + @ResponseBody + @GetMapping(value = "/api/lobby/requests") + public Map<String, List<AcceptRequest>> getRequests() { + String principal = getPrincipal(); + return Map.of("requests", openGames.getRequests(principal)); + } + @GetMapping("/api/sgf/{id}/{black}_vs_{white}.sgf") public ResponseEntity<String> getSgf( @PathVariable String id) { diff --git a/src/main/java/com/bernd/OpenGames.java b/src/main/java/com/bernd/OpenGames.java index a663630..2ab4ce4 100644 --- a/src/main/java/com/bernd/OpenGames.java +++ b/src/main/java/com/bernd/OpenGames.java @@ -1,12 +1,12 @@ package com.bernd; +import com.bernd.model.AcceptRequest; import com.bernd.model.OpenGame; import com.bernd.model.OpenGameList; -import org.springframework.stereotype.Component; - import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.springframework.stereotype.Component; @Component public class OpenGames { @@ -17,6 +17,16 @@ OpenGame put(OpenGame game) { return game; } + OpenGame addRequest(String name, AcceptRequest request, String opponent) { + OpenGame openGame = map.get(name); + if (openGame == null) { + return null; + } + OpenGame result = openGame.addRequest(request, opponent); + map.put(name, result); + return result; + } + OpenGameList games() { return new OpenGameList(List.copyOf(map.values())); } @@ -24,4 +34,12 @@ OpenGameList games() { OpenGame remove(String name) { return map.remove(name); } + + List<AcceptRequest> getRequests(String name) { + OpenGame openGame = map.get(name); + if (openGame == null) { + return List.of(); + } + return openGame.requests(); + } } diff --git a/src/main/java/com/bernd/model/OpenGame.java b/src/main/java/com/bernd/model/OpenGame.java index ae111ba..885b63e 100644 --- a/src/main/java/com/bernd/model/OpenGame.java +++ b/src/main/java/com/bernd/model/OpenGame.java @@ -1,6 +1,7 @@ package com.bernd.model; import com.bernd.game.MoveList; +import java.util.ArrayList; import java.util.List; import static com.bernd.LobbyController.createEmptyBoard; @@ -20,6 +21,13 @@ public OpenGame withUser(String user) { return new OpenGame(id, user, requests, dim, timesetting).sanitize(); } + public OpenGame addRequest(AcceptRequest request, String opponent) { + List<AcceptRequest> updated = new ArrayList<>(requests.size() + 1); + updated.addAll(requests); + updated.add(request.withOpponent(opponent)); + return new OpenGame(id, user, updated, dim, timesetting); + } + private OpenGame sanitize() { if (requests == null) { return new OpenGame(id, user, List.of(), dim, timesetting); @@ -27,9 +35,9 @@ private OpenGame sanitize() { return this; } - public Game accept(String opponent, AcceptRequest acceptRequest) { - String userBlack = acceptRequest.flip() ? opponent : user; - String userWhite = acceptRequest.flip() ? user : opponent; + public Game accept(AcceptRequest acceptRequest) { + String userBlack = acceptRequest.flip() ? acceptRequest.opponent() : user; + String userWhite = acceptRequest.flip() ? user : acceptRequest.opponent(); return new Game( id, userBlack,