目录
引言
在软件开发领域,通过构建简单的游戏项目来学习和实践新的技术框架是一种高效且有趣的方式。本文将详细介绍如何使用 Spring Boot 和 Maven 框架开发一个简单的井字棋(Tic - Tac - Toe)游戏。这个项目不仅能帮助你熟悉 Spring Boot 的基本使用,还能让你了解如何构建一个基于 RESTful 接口的交互式应用。
项目概述
我们要开发的井字棋游戏是一个基于 Web 的应用,玩家可以通过浏览器或工具(如 Postman)发送请求来与电脑进行游戏。游戏规则遵循传统的井字棋规则,玩家和电脑轮流在 3x3 的棋盘上落子,先在横、竖、斜方向连成一线的一方获胜,如果棋盘填满且没有一方获胜,则为平局。
项目搭建
1. 环境准备
确保你已经安装了 Java 和 Maven。Java 是运行 Spring Boot 应用的基础,而 Maven 则用于管理项目的依赖和构建。
2. 创建 Spring Boot 项目
可以使用 Spring Initializr(https://start.spring.io/)来快速创建一个 Spring Boot 项目,选择以下配置:
- Project: Maven Project
- Language: Java
- Spring Boot: 2.7.5
- Dependencies: Spring Web
3. 项目结构
项目的主要结构如下:
src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── demo
│ │ ├── DemoApplication.java
│ │ └── controller
│ │ └── TicTacToeController.java
│ └── resources
│ └── application.properties
└── pom.xml
代码实现
1. DemoApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
这是 Spring Boot 的主应用类,包含
main
方法,用于启动 Spring Boot 应用。@SpringBootApplication
注解是一个组合注解,它包含了@Configuration
、@EnableAutoConfiguration
和@ComponentScan
注解,用于自动配置 Spring Boot 应用。
2. TicTacToeController.java
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@RestController
public class TicTacToeController {
private static final int BOARD_SIZE = 3;
private char[][] board = new char[BOARD_SIZE][BOARD_SIZE];
private boolean isPlayerTurn = true;
private boolean gameOver = false;
private char winner = ' ';
public TicTacToeController() {
initializeBoard();
}
private void initializeBoard() {
for (int i = 0; i < BOARD_SIZE; i++) {
for (int j = 0; j < BOARD_SIZE; j++) {
board[i][j] = ' ';
}
}
isPlayerTurn = true;
gameOver = false;
winner = ' ';
}
@GetMapping("/start")
public String startGame() {
initializeBoard();
return "新游戏开始!你先手,使用 /move?row=x&col=y 落子(x 和 y 范围 0 - 2)。";
}
@GetMapping("/move")
public String makeMove(@RequestParam int row, @RequestParam int col) {
if (gameOver) {
return "游戏已结束,请使用 /start 开始新游戏。";
}
if (!isPlayerTurn) {
return "现在是电脑回合,请等待。";
}
if (!isValidMove(row, col)) {
return "无效的落子位置,请重新选择。";
}
board[row][col] = 'X';
checkGameStatus();
if (!gameOver) {
computerMove();
checkGameStatus();
}
return getBoardStatus();
}
private boolean isValidMove(int row, int col) {
return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE && board[row][col] == ' ';
}
private void computerMove() {
List<int[]> availableMoves = getAvailableMoves();
if (availableMoves.isEmpty()) {
return;
}
int[] bestMove = findBestMove();
board[bestMove[0]][bestMove[1]] = 'O';
isPlayerTurn = true;
}
private List<int[]> getAvailableMoves() {
List<int[]> moves = new ArrayList<>();
for (int i = 0; i < BOARD_SIZE; i++) {
for (int j = 0; j < BOARD_SIZE; j++) {
if (board[i][j] == ' ') {
moves.add(new int[]{i, j});
}
}
}
return moves;
}
private int[] findBestMove() {
int bestScore = Integer.MIN_VALUE;
int[] bestMove = null;
for (int[] move : getAvailableMoves()) {
board[move[0]][move[1]] = 'O';
int score = minimax(board, 0, false);
board[move[0]][move[1]] = ' ';
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
}
return bestMove;
}
private int minimax(char[][] board, int depth, boolean isMaximizing) {
if (hasWon('O')) {
return 1;
} else if (hasWon('X')) {
return -1;
} else if (getAvailableMoves().isEmpty()) {
return 0;
}
if (isMaximizing) {
int bestScore = Integer.MIN_VALUE;
for (int[] move : getAvailableMoves()) {
board[move[0]][move[1]] = 'O';
int score = minimax(board, depth + 1, false);
board[move[0]][move[1]] = ' ';
bestScore = Math.max(score, bestScore);
}
return bestScore;
} else {
int bestScore = Integer.MAX_VALUE;
for (int[] move : getAvailableMoves()) {
board[move[0]][move[1]] = 'X';
int score = minimax(board, depth + 1, true);
board[move[0]][move[1]] = ' ';
bestScore = Math.min(score, bestScore);
}
return bestScore;
}
}
private boolean hasWon(char player) {
// 检查行
for (int i = 0; i < BOARD_SIZE; i++) {
if (board[i][0] == player && board[i][0] == board[i][1] && board[i][1] == board[i][2]) {
return true;
}
}
// 检查列
for (int j = 0; j < BOARD_SIZE; j++) {
if (board[0][j] == player && board[0][j] == board[1][j] && board[1][j] == board[2][j]) {
return true;
}
}
// 检查对角线
if (board[0][0] == player && board[0][0] == board[1][1] && board[1][1] == board[2][2]) {
return true;
}
if (board[0][2] == player && board[0][2] == board[1][1] && board[1][1] == board[2][0]) {
return true;
}
return false;
}
private void checkGameStatus() {
if (hasWon('X')) {
winner = 'X';
gameOver = true;
return;
}
if (hasWon('O')) {
winner = 'O';
gameOver = true;
return;
}
// 检查平局
if (getAvailableMoves().isEmpty()) {
gameOver = true;
winner = ' ';
}
}
private String getBoardStatus() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < BOARD_SIZE; i++) {
for (int j = 0; j < BOARD_SIZE; j++) {
sb.append(board[i][j]);
if (j < BOARD_SIZE - 1) {
sb.append(" | ");
}
}
sb.append("\n");
if (i < BOARD_SIZE - 1) {
sb.append("---------\n");
}
}
if (gameOver) {
if (winner == 'X') {
sb.append("你赢了!使用 /start 开始新游戏。");
} else if (winner == 'O') {
sb.append("电脑赢了!使用 /start 开始新游戏。");
} else {
sb.append("平局!使用 /start 开始新游戏。");
}
} else {
sb.append("轮到你落子,使用 /move?row=x&col=y。");
}
return sb.toString();
}
}
这是一个 RESTful 控制器,包含两个主要的端点:
/start
:用于开始新游戏,调用initializeBoard
方法初始化棋盘,并返回提示信息。/move
:用于玩家落子,接收玩家的落子位置,验证其有效性,更新棋盘状态,检查游戏是否结束,如果未结束则调用computerMove
方法让电脑落子,最后返回当前棋盘状态。
3. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
这是 Maven 项目的配置文件,包含了项目的依赖和构建配置。
spring-boot-starter-web
依赖用于支持 Spring Web 开发,spring-boot-starter-test
依赖用于测试。
电脑落子策略 - Minimax 算法
电脑落子使用了 Minimax 算法来寻找最优落子位置。Minimax 算法是一种递归算法,用于在零和博弈中寻找最优策略。在井字棋游戏中,电脑会模拟所有可能的落子情况,通过递归调用
minimax
方法计算每个落子的得分,然后选择得分最高的落子位置。
findBestMove
方法
该方法遍历所有可用的落子位置,对每个位置调用
minimax
方法计算得分,最终选择得分最高的位置作为最佳落子位置。
minimax
方法
- 如果电脑获胜,返回 1。
- 如果玩家获胜,返回 -1。
- 如果平局,返回 0。
- 如果是电脑回合(
isMaximizing
为true
),选择得分最高的落子。- 如果是玩家回合(
isMaximizing
为false
),选择得分最低的落子。
运行游戏
- 打开终端,进入项目根目录,运行以下命令启动应用:
mvn spring-boot:run
- 打开浏览器或使用工具(如 Postman)访问以下 URL 来玩游戏:
- 开始游戏:
http://localhost:8080/start
- 落子:
http://localhost:8080/move?row=0&col=0
(将0
替换为你想要的落子位置)
总结
通过这个项目,我们学习了如何使用 Spring Boot 和 Maven 框架开发一个简单的井字棋游戏。同时,我们还了解了 Minimax 算法在游戏开发中的应用,通过该算法可以让电脑做出更智能的决策。这个项目不仅可以作为学习 Spring Boot 的入门示例,还可以进一步扩展,例如添加用户界面、支持多人游戏等。