五子棋PVP
项目链接:链接
(id:zhangsan key:123)
(id:lisi key:123)
项目背景
实现一个网页版五子棋对战程序.
支持以下核心功能:
- 用户模块: 用户注册, 用户登录, 用户天梯分数记录, 用户比赛场次记录.
- 匹配模块: 按照用户的天梯分数实现匹配机制.
- 对战模块: 实现两个玩家在网页端进行五子棋对战的功能.
核心技术
- Spring/SpringBoot/SpringMVC
- WebSocket
- MySQL
- MyBatis
- HTML/CSS/JS/AJAX
需求分析和概要设计
整个项目分成以下模块
- 用户模块
- 匹配模块
- 对战模块
用户模块
用户模块主要负责用户的注册, 登录, 分数记录功能.
使用 MySQL 数据库存储数据.
客户端提供一个登录页面+注册页面.
服务器端基于 Spring + MyBatis 来实现数据库的增删改查.
匹配模块
用户登录成功, 则进入游戏大厅页面.
游戏大厅中, 能够显示用户的名字, 天梯分数, 比赛场数和获胜场数.
同时显示一个 “匹配按钮”.
点击匹配按钮则用户进入匹配队列, 并且界面上显示为 “取消匹配” .
再次点击则把用户从匹配队列中删除.
如果匹配成功, 则跳转进入到游戏房间页面.
页面加载时和服务器建立 websocket 连接. 双方通过 websocket 来传输 “开始匹配”, “取消匹配”, “匹配成功” 这样的信息.
对战模块
玩家匹配成功, 则进入游戏房间页面.
每两个玩家在同一个游戏房间中.
在游戏房间页面中, 能够显示五子棋棋盘. 玩家点击棋盘上的位置实现落子功能.
并且五子连珠则触发胜负判定, 显示 “你赢了” “你输了”.
页面加载时和服务器建立 websocket 连接. 双方通过 websocket 来传输 “准备就绪”, “落子位置”, “胜负” 这样的信息.
- 准备就绪: 两个玩家均连上游戏房间的 websocket 时, 则认为双方准备就绪.
- 落子位置: 有一方玩家落子时, 会通过 websocket 给服务器发送落子的用户信息和落子位置, 同时服务器再将这样的信息返回给房间内的双方客户端. 然后客户端根据服务器的响应来绘制棋子位置.
- 胜负: 服务器判定这一局游戏的胜负关系. 如果某一方玩家落子, 产生了五子连珠, 则判定胜负并返回胜负信息. 或者如果某一方玩家掉线(比如关闭页面), 也会判定对方获胜.
项目创建
使用 IDEA 创建 SpringBoot 项目. 具体过程不再详细展开.
引入依赖如下:
依赖都是常规的 SpringBoot / Spring MVC / MyBatis 等, 没啥特别的依赖.
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gobang</name>
<description>联机对战五子棋</description>
<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-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
实现用户模块
编写数据库代码
数据库设计
创建 user 表, 表示用户信息和分数信息.
create database if not exists java_gobang;
use java_gobang;
drop table if exists user;
create table user(
userId int primary key auto_increment,
username varchar(50) unique,
password varchar(50),
score int, -- 天梯分数
totalCount int, -- 比赛总场次
winCount int -- 获胜场次
);
insert into user values(null, '张三', '123', 1000, 0, 0);
insert into user values(null, '李四', '123', 1000, 0, 0);
insert into user values(null, '王五', '123', 1000, 0, 0);
配置 MyBatis
编辑 application.yml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/java_gobang?characterEncoding=utf8&useSSL=false
username: root
password: 2222
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
创建实体类
创建 model.User
类
public class User {
private int userId;
private String username;
private String password;
private int score;
private int totalCount;
private int winCount;
}
创建 UserMapper
创建 model.UserMapper
接口.
此处主要提供四个方法:
- selectByName: 根据用户名查找用户信息. 用于实现登录.
- insert: 新增用户. 用户实现注册.
- userWin: 用于给获胜玩家修改分数.
- userLose: 用户给失败玩家修改分数.
@Mapper
public interface UserMapper {
User selectByName(String username);
int insert(User user);
void userWin(User user);
void userLose(User user);
}
实现 UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.model.UserMapper">
<select id="selectByName" resultType="com.example.demo.model.User">
select * from user where username = #{username}
</select>
<select id="selectById" resultType="com.example.demo.model.User">
select * from user where userId = #{userId}
</select>
<insert id="insert">
insert into user values(null, #{username}, #{password}, 1000, 0, 0)
</insert>
<update id="userWin">
update user set score = score + 25, totalCount = totalCount + 1, winCount = winCount + 1 where userId = #{userId}
</update>
<update id="userLose">
update user set score = score - 25, totalCount = totalCount + 1 where userId = #{userId}
</update>
</mapper>
前后端交互接口
需要明确用户模块的前后端交互接口. 这里主要涉及到三个部分.
登录接口
请求:
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=zhangsan&password=123
响应:
HTTP/1.1 200 OK
Content-Type: application/json
{
userId: 1,
username: 'zhangsan',
score: 1000,
totalCount: 10,
winCount: 5
}
如果登录失败, 返回的是一个 userId 为 0 的对象.
注册接口
请求:
POST /register HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=zhangsan&password=123
响应:
HTTP/1.1 200 OK
Content-Type: application/json
{
userId: 1,
username: 'zhangsan',
score: 1000,
totalCount: 10,
winCount: 5
}
如果注册失败(比如用户名重复), 返回的是一个 userId 为 0 的对象.
获取用户信息
请求:
GET /userInfo HTTP/1.1
响应:
HTTP/1.1 200 OK
Content-Type: application/json
{
userId: 1,
username: 'zhangsan',
score: 1000,
totalCount: 10,
winCount: 5
}
服务器开发
创建 api.UserAPI
类
主要实现三个方法:
- login: 用来实现登录逻辑.
- register: 用来实现注册逻辑.
- getUserInfo: 用来实现登录成功后显示用户分数的信息.
@RestController
public class UserAPI {
@Resource
private UserMapper userMapper;
@PostMapping("/login")
@ResponseBody
public Object login(String username, String password, HttpServletRequest req) {
User user = userMapper.selectByName(username);
System.out.println("login! user=" + user);
if (user == null || !user.getPassword().equals(password)) {
return new User();
}
HttpSession session = req.getSession(true);
session.setAttribute("user", user);
return user;
}
@PostMapping("/register")
@ResponseBody
public Object register(String username, String password) {
User user = null;
try {
user = new User();
user.setUsername(username);
user.setPassword(password);
System.out.println("register! user=" + user);
int ret = userMapper.insert(user);
System.out.println("ret: " + ret);
} catch (org.springframework.dao.DuplicateKeyException e) {
user = new User();
}
return user;
}
@GetMapping("/userInfo")
@ResponseBody
public Object getUserInfo(HttpServletRequest req) {
// 从 session 中拿到用户信息
HttpSession session = req.getSession(false);
if (session == null) {
return new User();
}
User user = (User) session.getAttribute("user");
if (user == null) {
return new User();
}
return user;
}
}
客户端开发
登录页面
创建 login.html
<div class="nav">
联机五子棋
</div>
<div class="login-container">
<div class="login-dialog">
<!-- 标题 -->
<h3>登录</h3>
<!-- 输入用户名 -->
<div class="row">
<span>用户名</span>
<input type="text" id="username" name="username">
</div>
<!-- 输入密码 -->
<div class="row">
<span>密码</span>
<input type="password" id="password" name="password">
</div>
<!-- 提交按钮 -->
<div class="row submit-row">
<button id="submit">提交</button>
</div>
</div>
</div>
创建 css/common.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
background-image: url(../image/back.png);
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
.nav {
width: 100%;
height: 50px;
background-color: rgb(51, 51, 51);
color: white;
display: flex;
align-items: center;
padding-left: 20px;
}
.container {
height: calc(100% - 50px);
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.7);
}
创建 css/login.css
.login-container {
width: 100%;
height: calc(100% - 50px);
display: flex;
justify-content: center;
align-items: center;
}
.login-dialog {
width: 400px;
height: 320px;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 10px;
}
.login-dialog h3 {
text-align: center;
padding: 50px 0;
}
.login-dialog .row {
width: 100%;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
}
.login-dialog .row span {
display: block;
/* 设置固定宽度, 能让文字和后面的输入框之间有间隙 */
width: 100px;
font-weight: 700;
}
.login-dialog #username,
.login-dialog #password {
width: 200px;
height: 40px;
font-size: 20px;
text-indent: 10px;
border-radius: 10px;
border: none;
outline: none;
}
.login-dialog .submit-row {
margin-top: 10px;
}
.login-dialog #submit {
width: 300px;
height: 50px;
color: white;
background-color: rgb(0, 128, 0);
border: none;
border-radius: 10px;
font-size: 20px;
}
.login-dialog #submit:active {
background-color: #666;
}
在 login.html 中编写 js 代码
- 通过 jQuery 中的 AJAX 和服务器进行交互.
<script src="http://lib.sinaapp.com/js/jquery/1.9.1/jquery-1.9.1.min.js"></script>
<script>
// 通