五子棋本地对战的逐步实现-前后交互

目录

一. Spring静态资源实现五子棋对战以及简单控制器访问

二. 引入thymeleaf模板引擎进行渲染以及控制器的正常使用

三. 基于控制器和javascript的异步交互


一. Spring静态资源访问以及简单控制器访问

首先我简单简述一下在springboot框架的目录结构中静态资源和动态内容的区别,在项目结构src/main下有文件夹resources用来存放资源,而resources分为static与templates:

templates/                 static/
├── index.html         ├── css/
├── game.html         ├── js/
└── error.html           └── images/            #示例

显而易见,templates用于放html文件,static用于放代码和图片等静态资源,

但现在我们先在static下创建index.html文件,并写好springboot启动类。

package com.example.gobang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GobangApplication {
    public static void main(String[] args) {
        SpringApplication.run(GobangApplication.class, args);
    }
} 

(依赖就自己去找好吧)

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>五子棋对战</title>
    <style>
        #chessboard {
            width: 450px;
            height: 450px;
            background-color: #DEB887;
            position: relative;
            margin: 50px auto;
            border: 2px solid #8B4513;
        }
        .chess-line {
            position: absolute;
            background-color: #000;
        }
        .vertical {
            width: 1px;
            height: 450px;
        }
        .horizontal {
            width: 450px;
            height: 1px;
        }
        .chess {
            position: absolute;
            width: 28px;
            height: 28px;
            border-radius: 50%;
            transform: translate(-50%, -50%);
        }
        .black {
            background-color: #000;
        }
        .white {
            background-color: #fff;
            border: 1px solid #000;
        }
        #status {
            text-align: center;
            font-size: 24px;
            margin: 20px;
        }
        #restart {
            display: block;
            margin: 20px auto;
            padding: 10px 20px;
            font-size: 18px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div id="status">黑棋回合</div>
    <div id="chessboard"></div>
    <button id="restart">重新开始</button>

    <script>
        const board = document.getElementById('chessboard');
        const status = document.getElementById('status');
        const restart = document.getElementById('restart');
        const GRID_SIZE = 30;
        const BOARD_SIZE = 15;
        let isBlackTurn = true;
        let gameOver = false;
        let chessBoard = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0));

        // 创建棋盘
        function createBoard() {
            // 创建网格线
            for (let i = 0; i < BOARD_SIZE; i++) {
                // 垂直线
                const vertical = document.createElement('div');
                vertical.className = 'chess-line vertical';
                vertical.style.left = i * GRID_SIZE + 'px';
                board.appendChild(vertical);

                // 水平线
                const horizontal = document.createElement('div');
                horizontal.className = 'chess-line horizontal';
                horizontal.style.top = i * GRID_SIZE + 'px';
                board.appendChild(horizontal);
            }
        }

        // 放置棋子
        function placeChess(x, y) {
            if (gameOver || chessBoard[y][x] !== 0) return;
            
            const chess = document.createElement('div');
            chess.className = `chess ${isBlackTurn ? 'black' : 'white'}`;
            chess.style.left = x * GRID_SIZE + 'px';
            chess.style.top = y * GRID_SIZE + 'px';
            board.appendChild(chess);
            
            chessBoard[y][x] = isBlackTurn ? 1 : 2;
            
            if (checkWin(x, y)) {
                status.textContent = `${isBlackTurn ? '黑棋' : '白棋'}获胜!`;
                gameOver = true;
                return;
            }
            
            isBlackTurn = !isBlackTurn;
            status.textContent = `${isBlackTurn ? '黑棋' : '白棋'}回合`;
        }

        // 检查是否获胜
        function checkWin(x, y) {
            const directions = [
                [[0, 1], [0, -1]], // 垂直
                [[1, 0], [-1, 0]], // 水平
                [[1, 1], [-1, -1]], // 主对角线
                [[1, -1], [-1, 1]] // 副对角线
            ];
            
            const currentPlayer = isBlackTurn ? 1 : 2;
            
            for (const direction of directions) {
                let count = 1;
                for (const [dx, dy] of direction) {
                    let newX = x + dx;
                    let newY = y + dy;
                    while (
                        newX >= 0 && newX < BOARD_SIZE &&
                        newY >= 0 && newY < BOARD_SIZE &&
                        chessBoard[newY][newX] === currentPlayer
                    ) {
                        count++;
                        newX += dx;
                        newY += dy;
                    }
                }
                if (count >= 5) return true;
            }
            return false;
        }

        // 重新开始游戏
        function restartGame() {
            // 清除所有棋子
            while (board.children.length > BOARD_SIZE * 2) {
                board.removeChild(board.lastChild);
            }
            // 重置棋盘状态
            chessBoard = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0));
            isBlackTurn = true;
            gameOver = false;
            status.textContent = '黑棋回合';
        }

        // 初始化游戏
        createBoard();

        // 事件监听
        board.addEventListener('click', (e) => {
            const rect = board.getBoundingClientRect();
            const x = Math.round((e.clientX - rect.left) / GRID_SIZE);
            const y = Math.round((e.clientY - rect.top) / GRID_SIZE);
            
            if (x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE) {
                placeChess(x, y);
            }
        });

        restart.addEventListener('click', restartGame);
    </script>
</body>
</html> 

启动成功后运行代码你会发现可以自己和自己下棋了,这就是使用spring静态资源实现静态功能了

现在给你讲讲什么是控制器,控制器有普通控制器和rest控制器,分别是返回页面和数据的,暂时讲这么多,我们现在创建一个重定向控制器:

package com.example.gobang.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class RedirectController {
    
    @GetMapping("/go")
    public String gobang() {
        return "redirect:/gobang.html";  // 重定向到静态HTML文件
    }

这个属于返回页面的普通控制器,作用很明显,原先我们进入http://localhost:8080/gobang.html来进入页面,现在他对/go进行gobang.html的返回那我们访问http://localhost:8080/go会返回http://localhost:8080/gobang.html的页面。

二. 引入thymeleaf模板引擎进行渲染以及控制器的正常使用

1.什么是thymeleaf动态渲染

我们先对pom.xml导入thymeleaf的依赖。然后我进行简单讲解,前面给出了html文件,可以看到他的结构为:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>已而不在其然</title>
    <style></style>
</head>
<body>
     <div id="ddd"> </div>
     <script>
        function six(){}
     </script>
</body>
</html>

其中style部分放的就是css代码,scipt放javascript代码,

css用来改变页面的颜色位置形状等属性,

javascript用来实现方法比如判断五子棋的获胜,棋盘的生成,落子,

这些在方法html里面就已经全部完成了。

而现在我们来进行一个操作-thymeleaf进行渲染的话,举例:

<!-- 静态HTML -->
<div id="status">黑棋回合</div>

<!-- Thymeleaf版本 -->
<div id="status" th:text="${board.status}">无棋回合</div>

th:text即为动态渲染,你可能好奇${board.status}这个是什么,

以下是$的常见用法:

  • ${变量名}:获取变量值
  • ${条件 ? 值1 : 值2}:条件判断
  • ${对象.属性}:访问对象属性
  • ${方法调用}:调用方法

在这里是一个后端的数据,这个数据是会动态更新的,div内的内容由text的board.status来决定并且会将静态内容“无棋回合”覆盖。

2.动态渲染初始棋盘状态

以下是用静态资源和动态thymeleaf实现的初始棋盘,什么是棋盘,我们现在假定一个可以放[15]×[15]个棋子的棋盘,那我们就要使用spring控制器来实现棋盘:

注意:在使用javascript的function完成静态资源实现五子棋对战功能后要是想直接将static下的html移到templates目录下使用控制器动态渲染五子棋状态建议先把function删除,不然你的控制器没有完成的功能可能在javascript中已经完成导致你无法辨别。

两种不同方法实现初始化棋盘,静态与动态:

1.静态使用javascript创建空的初始棋盘状态:


    <script>
        const board = document.getElementById('chessboard');
        const BOARD_SIZE = 15;
        let chessBoard = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0));
    </script>

2.控制器创建初始棋盘并使用thymeleaf动态渲染:

public class GameController {
    
    private static final String BOARD_SESSION_KEY = "gameBoard";

    @GetMapping("/")
    public String index(Model model, HttpSession session) {
        Board board = (Board) session.getAttribute(BOARD_SESSION_KEY);
        if (board == null) {
            board = new Board();
            session.setAttribute(BOARD_SESSION_KEY, board);
        }
        model.addAttribute("board", board);
        return "index";
    }
<!-- 渲染棋子 -->
<div th:each="row, rowStat : ${board.grid}">
    <div th:each="cell, colStat : ${row}" 
         class="cell"
         th:classappend="${cell != 0} ? (${cell == 1} ? 'black' : 'white') : ''"
         th:style="'left: ' + ${colStat.index * 30} + 'px; top: ' + ${rowStat.index * 30} + 'px'"
         th:attr="data-row=${rowStat.index}, data-col=${colStat.index}">
    </div>
</div>
public class Board {       #Board类,控制器引用他
    private int[][] grid;
    private boolean isBlackTurn;
    private boolean gameOver;
    private String status;
    private static final int BOARD_SIZE = 15;

    public Board() {
        this.grid = new int[BOARD_SIZE][BOARD_SIZE];
        this.isBlackTurn = true;
        this.gameOver = false;
        this.status = "黑棋回合";
    }
}

提示:上动态渲染文件是放在templates下的,普通控制器返回页面时不需要加上.html也能找到index.html所以return index就可以了。我的控制器中的为棋盘的模型专门创建了一个Board类封装了模型所有的属性,你也可以选择直接在控制类将属性写好。这个控制器使用for循环语句创建了15×15的空棋盘并将模型属性使用model.addAttribute("board",board);传递到了前端的 ${boardState}上,而下面的前端代码则是给棋盘的[15][15]个cell格子赋予了序列值Board

各部分含义:

  • rowStat.index:当前行的索引(0, 1, 2, ...)
  • colStat.index:当前列的索引(0, 1, 2, ...)
  • data-row:自定义属性,存储行号
  • data-col:自定义属性,存储列号

    棋盘上可以放棋子了却还没有线条,我们可以使用thymeleaf循环语句加css创建棋盘的线条:

    <!-- Thymeleaf版本 -->
    <div id="chessboard">
        <div th:each="i : ${#numbers.sequence(0, 14)}">
            <div class="chess-line vertical" th:style="'left: ' + ${i * 30} + 'px'"></div>
            <div class="chess-line horizontal" th:style="'top: ' + ${i * 30} + 'px'"></div>
        </div>
    </div>

    .chess-line {
        position: absolute;
        background-color: #000;
    }
    .vertical {
        width: 1px;
        height: 100%;
    }
    .horizontal {
        width: 100%;
        height: 1px;
    }

    三. 基于控制器和javascript的异步交互

    如果想要实现前端页面点击后向后端发送数据我们有两种方式解决

    1.表单提交举例

    <!-- 1. HTML中定义表单 -->
    <form th:action="@{/move}" method="post" id="moveForm">
        <input type="hidden" name="x" id="moveX">
        <input type="hidden" name="y" id="moveY">
    </form>
    
    <!-- 2. JavaScript中获取点击位置并提交表单 -->
    board.addEventListener('click', (e) => {
        const rect = board.getBoundingClientRect();
        const x = Math.round((e.clientX - rect.left) / 30);
        const y = Math.round((e.clientY - rect.top) / 30);
        
        if (x >= 0 && x < 15 && y >= 0 && y < 15) {
            moveX.value = x;
            moveY.value = y;
            moveForm.submit();  // 提交表单,页面会刷新
        }
    });
    
    <!-- 3. 控制器处理请求 -->
    @PostMapping("/move")
    public String makeMove(@RequestParam int x, @RequestParam int y, 
                          Model model, HttpSession session) {
        Board board = (Board) session.getAttribute(BOARD_SESSION_KEY);
        board.makeMove(x, y);
        model.addAttribute("board", board);
        return "index";  // 返回页面,导致页面刷新
    }

    2.fetch异步请求举例

    <!-- 1. HTML中直接定义单元格 -->
    <div class="cell" th:attr="data-row=${rowStat.index}, data-col=${colStat.index}"></div>
    
    <!-- 2. JavaScript中发送异步请求 -->
    document.querySelectorAll('.cell').forEach(cell => {
        cell.addEventListener('click', async function(event) {
            const row = event.target.dataset.row;
            const col = event.target.dataset.col;
            
            const response = await fetch('/move', {
                method: 'POST',
                body: `x=${col}&y=${row}`
            });
            const result = await response.json();
            // 更新界面,不刷新页面
        });
    });
    
    <!-- 3. 控制器返回JSON数据 -->
    @PostMapping("/move")
    @ResponseBody
    public Map<String, Object> makeMove(@RequestParam int x, @RequestParam int y, 
                                      HttpSession session) {
        Board board = (Board) session.getAttribute(BOARD_SESSION_KEY);
        board.makeMove(x, y);
        return response;  // 返回JSON数据,不刷新页面
    }

    由于老师的要求,我们选择第二种基于javascript的fetch异步请求:

    用户点击 -> JavaScript发送请求 -> 控制器处理 -> 返回结果 -> JavaScript更新界面

    我们可以通过上述步骤实现棋盘棋子状态的更新,

    1.用户点击的单元格

    <div class="cell"
         th:classappend="${cell != 0} ? (${cell == 1} ? 'black' : 'white') : ''"
         th:style="'left: ' + ${colStat.index * 30} + 'px; top: ' + ${rowStat.index * 30} + 'px'"
         th:attr="data-row=${rowStat.index}, data-col=${colStat.index}">
    </div>

    2.javascript为每个单元格添加点击事件,在前端页面点击就会产生反应,并发送请求到后端

    document.querySelectorAll('.cell').forEach(cell => {
        cell.addEventListener('click', async function(event) {
            const row = event.target.dataset.row;
            const col = event.target.dataset.col;
            
            const response = await fetch('/move', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
                body: `x=${col}&y=${row}`
            });
        });
    });

    3.控制器处理接受请求,处理落子逻辑并返回结果给前端

    @PostMapping("/move")
    @ResponseBody
    public Map<String, Object> makeMove(@RequestParam int x, @RequestParam int y, 
                                      HttpSession session) {
        Board board = (Board) session.getAttribute(BOARD_SESSION_KEY);
        boolean success = board.makeMove(x, y);
        
        Map<String, Object> response = new HashMap<>();
        response.put("success", success);
        response.put("status", board.getStatus());
        response.put("isBlackTurn", board.isBlackTurn());
        response.put("gameOver", board.isGameOver());
        response.put("grid", board.getGrid());
        return response;                      //对应下面json的各项属性,你可以访问/move页面查看数据
    }

    4.控制器处理后返回json格式数据

    {
            "isBlackTurn":false,
            "success":true,
            "grid":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],
            "gameOver":false,
            "status":"白棋回合"
    }

    5.前端页面通过javascript进行更新

    const result = await response.json();
    if (result.success) {
        // 更新状态显示
        document.getElementById('status').textContent = result.status;
        // 更新棋盘显示
        const grid = result.grid;
        for (let i = 0; i < grid.length; i++) {
            for (let j = 0; j < grid[i].length; j++) {
                const cell = document.querySelector(`[data-row="${i}"][data-col="${j}"]`);
                if (cell) {
                    cell.classList.remove('black', 'white');
                    if (grid[i][j] === 1) {
                        cell.classList.add('black');
                    } else if (grid[i][j] === 2) {
                        cell.classList.add('white');
                    }
                }
            }
        }
    }

    简单的基于控制器和javascript的异步交互功能实现了,你可以自由探索胜负判定功能和游戏重置功能,主要在控制器和前端页面添加对应数据属性就行了。我在这将我的Board类给出来吧。

    package com.example.gobang.model;
    
    public class Board {
        private int[][] grid;
        private boolean isBlackTurn;
        private boolean gameOver;
        private String status;
        private static final int BOARD_SIZE = 15;
    
        public Board() {
            this.grid = new int[BOARD_SIZE][BOARD_SIZE];
            this.isBlackTurn = true;
            this.gameOver = false;
            this.status = "黑棋回合";
        }
    
        public boolean makeMove(int x, int y) {
            if (gameOver || x < 0 || x >= BOARD_SIZE || y < 0 || y >= BOARD_SIZE || grid[y][x] != 0) {
                return false;
            }
    
            grid[y][x] = isBlackTurn ? 1 : 2;
    
            if (checkWin(x, y)) {
                gameOver = true;
                status = (isBlackTurn ? "黑棋" : "白棋") + "获胜!";
                return true;
            }
    
            isBlackTurn = !isBlackTurn;
            status = (isBlackTurn ? "黑棋" : "白棋") + "回合";
            return true;
        }
    
        private boolean checkWin(int x, int y) {
            int[][] directions = {
                {0, 1},   // 垂直
                {1, 0},   // 水平
                {1, 1},   // 主对角线
                {1, -1}   // 副对角线
            };
    
            int currentPlayer = isBlackTurn ? 1 : 2;
    
            for (int[] direction : directions) {
                int count = 1;
                // 正向检查
                count += countDirection(x, y, direction[0], direction[1], currentPlayer);
                // 反向检查
                count += countDirection(x, y, -direction[0], -direction[1], currentPlayer);
                
                if (count >= 5) {
                    return true;
                }
            }
            return false;
        }
    
        private int countDirection(int x, int y, int dx, int dy, int player) {
            int count = 0;
            int newX = x + dx;
            int newY = y + dy;
            while (newX >= 0 && newX < BOARD_SIZE && 
                   newY >= 0 && newY < BOARD_SIZE && 
                   grid[newY][newX] == player) {
                count++;
                newX += dx;
                newY += dy;
            }
            return count;
        }
    
        public void reset() {
            this.grid = new int[BOARD_SIZE][BOARD_SIZE];
            this.isBlackTurn = true;
            this.gameOver = false;
            this.status = "黑棋回合";
        }
    
        // Getters and Setters
        public int[][] getGrid() {
            return grid;
        }
    
        public void setGrid(int[][] grid) {
            this.grid = grid;
        }
    
        public boolean isBlackTurn() {
            return isBlackTurn;
        }
    
        public void setBlackTurn(boolean blackTurn) {
            isBlackTurn = blackTurn;
        }
    
        public boolean isGameOver() {
            return gameOver;
        }
    
        public void setGameOver(boolean gameOver) {
            this.gameOver = gameOver;
        }
    
        public String getStatus() {
            return status;
        }
    
        public void setStatus(String status) {
            this.status = status;
        }
    } 

    后面我会专门关于Spring Data JPA和数据库实现游戏保存和加载功能以及Spring Data JPA整合为Mybatis写一篇。

    评论 1
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值