diff --git a/src/main/client/src/Game.jsx b/src/main/client/src/Game.jsx index 5c912a6..076677c 100644 --- a/src/main/client/src/Game.jsx +++ b/src/main/client/src/Game.jsx @@ -47,10 +47,14 @@ export const Game = () => { let stompClient = useContext(StompContext) let auth = useAuthStore(state => state.auth) let setGameState = useGameStore(state => state.setGameState) + let queueStatus = useGameStore(state => state.queueStatus) + let addMove = useGameStore(state => state.addMove) let { board, currentColor, currentPlayer, counting, forbidden } = useGameStore(state => state.gameState) let [forbidden_x, forbidden_y] = forbidden let initialized = useRef() let canvasRef = useRef() + let countingGroup = counting ? getCountingGroup(board, cursor_x, cursor_y) : undefined + let context = useMemo(() => { let dim = board.length if (!dim) { @@ -111,6 +115,7 @@ export const Game = () => { lastStoneYref: lastStoneYref, } }, [board.length, canvasRef, zoom]) + let onMouseMove = useCallback((e) => { if (!board.length) { return @@ -123,7 +128,7 @@ export const Game = () => { setCursor_x(cursor_x + 0) setCursor_y(cursor_y + 0) }, [context, currentPlayer, auth, board.length, counting]) - let countingGroup = counting ? getCountingGroup(board, cursor_x, cursor_y) : undefined + let onClick = useCallback((e) => { if (!board.length) { return @@ -159,6 +164,7 @@ export const Game = () => { }), }) }, [context, currentPlayer, currentColor, auth, board, gameId, stompClient, counting, forbidden_x, forbidden_y]) + useEffect(() => { if (!board.length) { return @@ -187,19 +193,11 @@ export const Game = () => { "rgba(255,255,255,0.25)" showShadow(context, cursor_x, cursor_y, style) }, [cursor_x, cursor_y, context, canvasRef, auth, currentColor, board, currentPlayer, counting, countingGroup, forbidden_x, forbidden_y]) + useEffect(() => { - if (initialized.current) { + if (queueStatus === "up_to_date") { return } - initialized.current = true - let sub1 = stompClient.subscribe("/topic/game/" + gameId, (message) => { - let game = JSON.parse(message.body) - setGameState(game) - }) - let sub2 = stompClient.subscribe("/topic/move/" + gameId, (message) => { - let move = JSON.parse(message.body) - console.log(move) // TODO - }) doTry(async () => { let game = await tfetch("/api/game/" + gameId, { headers: { @@ -208,14 +206,24 @@ export const Game = () => { }) setGameState(game) }) - return () => { - sub1.unsubscribe() - sub2.unsubscribe() + }, [setGameState, queueStatus, auth, gameId]) + + useEffect(() => { + if (initialized.current) { + return } - }, [setGameState, initialized, stompClient, gameId, auth]) + initialized.current = true + let sub = stompClient.subscribe("/topic/move/" + gameId, (message) => { + let move = JSON.parse(message.body) + addMove(move) + }) + return sub.unsubscribe + }, [setGameState, addMove, initialized, stompClient, gameId, auth]) + if (!board.length) { return
Loading...
} + return (
state.auth) - let { black, white} = useGameStore(state => state) + let black = useGameStore(state => state.black) + let white = useGameStore(state => state.white) let { board, currentPlayer, counting } = useGameStore(state => state.gameState) let navigate = useNavigate() let onExit = useCallback(() => { diff --git a/src/main/client/src/model/board.js b/src/main/client/src/model/board.js index f0602fd..ac50973 100644 --- a/src/main/client/src/model/board.js +++ b/src/main/client/src/model/board.js @@ -9,6 +9,8 @@ import { } from "./PointSet.js" import { hasStone, + BLACK, + WHITE, } from "../util.js" export function getGroup(board, xx, yy) { @@ -117,7 +119,7 @@ export function isForbidden(board, groupInfo, currentColor) { return true } if (y > 0) { - let { color, liberties, hasStone } = board[y - 1][x] + let {color, liberties, hasStone} = board[y - 1][x] if (!hasStone) { return false } @@ -129,7 +131,7 @@ export function isForbidden(board, groupInfo, currentColor) { } } if (y < dim - 1) { - let { color, liberties, hasStone } = board[y + 1][x] + let {color, liberties, hasStone} = board[y + 1][x] if (!hasStone) { return false } @@ -141,7 +143,7 @@ export function isForbidden(board, groupInfo, currentColor) { } } if (x > 0) { - let { color, liberties, hasStone } = board[y][x - 1] + let {color, liberties, hasStone} = board[y][x - 1] if (!hasStone) { return false } @@ -153,7 +155,7 @@ export function isForbidden(board, groupInfo, currentColor) { } } if (x < dim - 1) { - let { color, liberties, hasStone } = board[y][x + 1] + let {color, liberties, hasStone} = board[y][x + 1] if (!hasStone) { return false } @@ -166,3 +168,101 @@ export function isForbidden(board, groupInfo, currentColor) { } return true } + +export function updateBoard(board, move) { + let {pass, x, y, color} = move + if (pass) { + return board + } + board = applyMove(board, move) + let oppositeColor = color ^ (WHITE | BLACK) + board = removeDeadGroup(board, x, y - 1, oppositeColor) + board = removeDeadGroup(board, x, y + 1, oppositeColor) + board = removeDeadGroup(board, x - 1, y, oppositeColor) + board = removeDeadGroup(board, x + 1, y, oppositeColor) + return board +} + +function removeDeadGroup(board, xx, yy, color) { + let dim = board.length + if (Math.min(xx, yy) < 0 || Math.max(xx, yy) >= dim) { + return board + } + if (board[yy][xx] !== color) { + return board + } + if (yy > 0 && board[yy - 1][xx] == 0) { + return board + } + if (yy < dim - 1 && board[yy + 1][xx] == 0) { + return board + } + if (xx > 0 && board[yy][xx - 1] == 0) { + return board + } + if (xx < dim - 1 && board[yy][xx + 1] == 0) { + return board + } + let acc = new PointList(dim) + let pointsChecked = new PointSet(dim) + pointsChecked.add(xx, yy) + let pointsToCheck = new PointQueue(dim) + pointsToCheck.offer(xx, yy) + while (!pointsToCheck.isEmpty()) { + let ptId = pointsToCheck.poll() + let y = Math.trunc(ptId / dim) + let x = ptId % dim + acc.add(x, y) + if (y > 0) { + let bpt = board[y - 1][x] + if (bpt === 0) { + return board + } else if (bpt === color && !pointsChecked.has(x, y - 1)) { + pointsChecked.add(x, y - 1) + pointsToCheck.offer(x, y - 1) + } + } + if (y < dim - 1) { + let bpt = board[y + 1][x] + if (bpt === 0) { + return board + } else if (bpt === color && !pointsChecked.has(x, y + 1)) { + pointsChecked.add(x, y + 1) + pointsToCheck.offer(x, y + 1) + } + } + if (x > 0) { + let bpt = board[y][x - 1] + if (bpt === 0) { + return board + } else if (bpt === color && !pointsChecked.has(x - 1, y)) { + pointsChecked.add(x - 1, y) + pointsToCheck.offer(x - 1, y) + } + } + if (x < dim - 1) { + let bpt = board[y][x + 1] + if (bpt === 0) { + return board + } else if (bpt === color && !pointsChecked.has(x + 1, y)) { + pointsChecked.add(x + 1, y) + pointsToCheck.offer(x + 1, y) + } + } + } + let result = board.slice() + acc.forEach((x, y) => { + if (result[y] === board[y]) { + result[y] = board[y].slice() + } + result[y][x] = 0 + }) + return result +} + +function applyMove(board, {color, x, y}) { + let result = board.slice() + result[y] = board[y].slice() + result[y][x] = color + return result +} diff --git a/src/main/client/src/store.js b/src/main/client/src/store.js index 511a773..06c7016 100644 --- a/src/main/client/src/store.js +++ b/src/main/client/src/store.js @@ -6,9 +6,11 @@ import { } from "immer" import { BLACK, + WHITE, } from "./util.js" import { rehydrate, + updateBoard, } from "./model/board.js" export const useAuthStore = create((set) => ({ @@ -31,7 +33,10 @@ export const useAuthStore = create((set) => ({ }, })) -export const useGameStore = create((set) => ({ +export const useGameStore = create((set, get) => ({ + moves: [], + baseBoard: [], + queueStatus: "behind", editMode: false, black: { name: "", @@ -52,16 +57,41 @@ export const useGameStore = create((set) => ({ counting: false, forbidden: [-1, -1], }, + addMove: (move) => { + set(produce(state => { + if (get().moves.length < move.n) { + state.queueStatus = "behind" + return + } + state.queueStatus = "up_to_date" + state.moves.push(move) + if (move.counting) { + state.gameState.counting = true + state.baseBoard = move.board + state.gameState.board = rehydrate(move.board) + return + } + let updated = updateBoard(get().baseBoard, move) + state.baseBoard = updated + state.gameState.board = rehydrate(updated) + state.gameState.currentColor = get().gameState.currentColor ^ (BLACK | WHITE) + state.gameState.currentPlayer = get().gameState.currentPlayer === get().black.name ? get().white.name : get().black.name + state.gameState.forbidden = move.forbidden + })) + }, setGameState: (game) => { set(produce(state => { state.black = game.black state.white = game.white state.editMode = game.editMode + state.baseBoard = game.board + state.moves = game.moves state.gameState.board = rehydrate(game.board) state.gameState.currentPlayer = game.currentPlayer state.gameState.currentColor = game.currentColor state.gameState.counting = game.counting state.gameState.forbidden = game.forbidden + state.queueStatue = "up_to_date" })) }, })) diff --git a/src/main/java/com/bernd/GameController.java b/src/main/java/com/bernd/GameController.java index d9380d7..bb861c9 100644 --- a/src/main/java/com/bernd/GameController.java +++ b/src/main/java/com/bernd/GameController.java @@ -2,13 +2,13 @@ import com.bernd.model.AcceptRequest; import com.bernd.model.ActiveGame; +import com.bernd.model.CountingMove; import com.bernd.model.Game; import com.bernd.model.Move; import com.bernd.model.OpenGame; import com.bernd.model.ViewGame; import com.bernd.util.Auth; import com.bernd.util.RandomString; -import java.security.Principal; import org.springframework.http.ResponseEntity; import org.springframework.messaging.core.MessageSendingOperations; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -19,6 +19,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; +import java.security.Principal; + @Controller public class GameController { @@ -61,8 +63,11 @@ public void action(Move move, Principal principal) { } Game updated = game.update(move); games.put(updated); - operations.convertAndSend("/topic/move/" + game.id(), move.toView(color, moveNumber)); - operations.convertAndSend("/topic/game/" + game.id(), updated.toView()); + if (updated.counting()) { + operations.convertAndSend("/topic/move/" + game.id(), CountingMove.create(color, moveNumber, updated.board())); + } else { + operations.convertAndSend("/topic/move/" + game.id(), move.toView(color, moveNumber, updated.forbidden())); + } } @ResponseBody diff --git a/src/main/java/com/bernd/game/MoveList.java b/src/main/java/com/bernd/game/MoveList.java index cf727b5..9bdeed8 100644 --- a/src/main/java/com/bernd/game/MoveList.java +++ b/src/main/java/com/bernd/game/MoveList.java @@ -2,6 +2,7 @@ import com.bernd.model.GameMove; import com.bernd.model.Move; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -24,7 +25,7 @@ public final class MoveList { private MoveList( int dim, int[] buffer) { - this.capacity = dim * dim; + this.capacity = 2 * buffer.length; this.dim = dim; this.buffer = buffer; } @@ -39,7 +40,8 @@ public static MoveList create(int dim) { public void add(int color, Move move) { if (pos >= capacity) { - int newCapacity = 2 * capacity; + int boardSize = dim * dim; + int newCapacity = capacity < boardSize ? boardSize : capacity + boardSize; buffer = Arrays.copyOf(buffer, divUp(newCapacity, 2)); capacity = newCapacity; } @@ -61,12 +63,12 @@ public GameMove get(int i) { int ptId = i % 2 == 0 ? code & LO : (code >> 16); int color = (ptId & WHITE) != 0 ? Board.W : Board.B; if ((ptId & PASS) != 0) { - return new GameMove(i, color, true, -1, -1); + return new GameMove(i, color, true, -1, -1, new int[]{-1, -1}); } else { int data = ptId & DATA; int x = data % dim; int y = data / dim; - return new GameMove(i, color, true, x, y); + return new GameMove(i, color, true, x, y, new int[]{-1, -1}); } } diff --git a/src/main/java/com/bernd/model/CountingMove.java b/src/main/java/com/bernd/model/CountingMove.java new file mode 100644 index 0000000..1618846 --- /dev/null +++ b/src/main/java/com/bernd/model/CountingMove.java @@ -0,0 +1,13 @@ +package com.bernd.model; + +public record CountingMove( + int n, + int color, + boolean pass, + boolean counting, + int [][] board) { + + public static CountingMove create(int color, int n, int[][] board) { + return new CountingMove(n, color, true, true, board); + } +} \ No newline at end of file diff --git a/src/main/java/com/bernd/model/GameMove.java b/src/main/java/com/bernd/model/GameMove.java index 4312e9e..72d866f 100644 --- a/src/main/java/com/bernd/model/GameMove.java +++ b/src/main/java/com/bernd/model/GameMove.java @@ -5,5 +5,6 @@ public record GameMove( int color, boolean pass, int x, - int y) { + int y, + int[] forbidden) { } \ No newline at end of file diff --git a/src/main/java/com/bernd/model/Move.java b/src/main/java/com/bernd/model/Move.java index 3112018..2adc353 100644 --- a/src/main/java/com/bernd/model/Move.java +++ b/src/main/java/com/bernd/model/Move.java @@ -7,7 +7,7 @@ public record Move( int x, int y) { - public GameMove toView(int color, int moveNumber) { - return new GameMove(moveNumber, color, pass, x, y); + public GameMove toView(int color, int moveNumber, int[] forbidden) { + return new GameMove(moveNumber, color, pass, x, y, forbidden); } } diff --git a/src/test/java/com/bernd/game/MoveListTest.java b/src/test/java/com/bernd/game/MoveListTest.java index 19af4af..3a1daf6 100644 --- a/src/test/java/com/bernd/game/MoveListTest.java +++ b/src/test/java/com/bernd/game/MoveListTest.java @@ -8,7 +8,7 @@ class MoveListTest { @Test - void get() { + void testGet() { MoveList list = MoveList.create(9); list.add(Board.B, move(0, 1)); list.add(Board.W, move(2, 3)); @@ -21,6 +21,17 @@ void get() { assertEquals(Board.W, list.get(1).color()); } + @Test + void testGrow() { + MoveList list = MoveList.create(9); + for (int y = 0; y < 9; y++) { + for (int x = 0; x < 9; x++) { + list.add(Board.B, move(x, y)); + } + } + assertEquals(81, list.size()); + } + private Move move(int x, int y) { return new Move("", false, false, x, y); }