├── .gitignore
├── back-end
├── src
│ ├── socketio
│ │ ├── entities
│ │ │ └── socketio.entity.ts
│ │ ├── dto
│ │ │ ├── create-socketio.dto.ts
│ │ │ └── update-socketio.dto.ts
│ │ ├── socketio.service.ts
│ │ ├── socketio.module.ts
│ │ ├── socketio.service.spec.ts
│ │ ├── socketio.gateway.spec.ts
│ │ └── socketio.gateway.ts
│ ├── app.service.ts
│ ├── main.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ └── app.controller.spec.ts
├── .prettierrc
├── tsconfig.build.json
├── nest-cli.json
├── test
│ ├── jest-e2e.json
│ └── app.e2e-spec.ts
├── tsconfig.json
├── .eslintrc.js
├── .gitignore
└── package.json
├── front-end
├── .vscode
│ └── extensions.json
├── src
│ ├── lang
│ │ ├── zh_CN.js
│ │ ├── en.js
│ │ └── i18n.js
│ ├── assets
│ │ └── sounds
│ │ │ ├── pop_down1.mp3
│ │ │ └── pop_down2.mp3
│ ├── plugins
│ │ └── socket.io.js
│ ├── components
│ │ ├── PlayMode.vue
│ │ ├── game
│ │ │ ├── GameControls.vue
│ │ │ ├── GameBoard.vue
│ │ │ └── PieceSelection.vue
│ │ └── index.vue
│ ├── main.js
│ ├── pages
│ │ └── index.vue
│ ├── style.css
│ ├── composables
│ │ ├── useGameState.js
│ │ ├── useChessboard.js
│ │ └── useAI.js
│ └── App.vue
├── postcss.config.js
├── tailwind.config.js
├── vite.config.js
├── .gitignore
├── index.html
├── package.json
└── public
│ ├── 小鸡.svg
│ ├── 仓鼠.svg
│ ├── 白猫.svg
│ ├── 可达鸭.svg
│ ├── 橘猫.svg
│ ├── 三花猫.svg
│ ├── 柴犬.svg
│ ├── 金毛.svg
│ ├── 咕噜噜.svg
│ ├── 哈士奇.svg
│ └── 柯基.svg
├── pnpm-workspace.yaml
├── README.md
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/back-end/src/socketio/entities/socketio.entity.ts:
--------------------------------------------------------------------------------
1 | export class Socketio {}
2 |
--------------------------------------------------------------------------------
/back-end/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/back-end/src/socketio/dto/create-socketio.dto.ts:
--------------------------------------------------------------------------------
1 | export class CreateSocketioDto {}
2 |
--------------------------------------------------------------------------------
/front-end/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "vue.volar",
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/front-end/src/lang/zh_CN.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'standaloneMode':'单机模式',
3 | "lanMode":"局域网模式"
4 | }
--------------------------------------------------------------------------------
/front-end/src/lang/en.js:
--------------------------------------------------------------------------------
1 | export default {
2 | "standaloneMode": "Standalone Mode",
3 | "lanMode":"Lan Mode"
4 | }
--------------------------------------------------------------------------------
/front-end/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/front-end/src/assets/sounds/pop_down1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitbitdown/wuziqi/HEAD/front-end/src/assets/sounds/pop_down1.mp3
--------------------------------------------------------------------------------
/front-end/src/assets/sounds/pop_down2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitbitdown/wuziqi/HEAD/front-end/src/assets/sounds/pop_down2.mp3
--------------------------------------------------------------------------------
/back-end/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 |
3 | # all packages in subdirs of components/
4 |
5 | - "common"
6 |
7 | - "front-end"
8 |
9 | - "back-end"
10 |
--------------------------------------------------------------------------------
/back-end/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'Hello World!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/back-end/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/nest-cli",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src",
5 | "compilerOptions": {
6 | "deleteOutDir": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/back-end/src/socketio/socketio.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class SocketioService {
5 | chessboard(location:object,belongsTo:string){
6 | return {location,belongsTo}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/back-end/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/back-end/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 |
4 | async function bootstrap() {
5 | const app = await NestFactory.create(AppModule);
6 | await app.listen(3000);
7 | }
8 | bootstrap();
9 |
--------------------------------------------------------------------------------
/front-end/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{vue,js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
--------------------------------------------------------------------------------
/back-end/src/socketio/dto/update-socketio.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types';
2 | import { CreateSocketioDto } from './create-socketio.dto';
3 |
4 | export class UpdateSocketioDto extends PartialType(CreateSocketioDto) {
5 | id: number;
6 | }
7 |
--------------------------------------------------------------------------------
/back-end/src/socketio/socketio.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { SocketioService } from './socketio.service';
3 | import { SocketioGateway } from './socketio.gateway';
4 |
5 | @Module({
6 | providers: [SocketioGateway, SocketioService],
7 | })
8 | export class SocketioModule {}
9 |
--------------------------------------------------------------------------------
/front-end/src/plugins/socket.io.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | import { io } from "socket.io-client";
4 |
5 | export default {
6 | install: (app, { connection, options }) => {
7 | const socket = io(connection, options);
8 | app.config.globalProperties.$socket = socket;
9 | app.provide("socket", socket);
10 | },
11 | };
--------------------------------------------------------------------------------
/back-end/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 | import { AppService } from './app.service';
3 |
4 | @Controller()
5 | export class AppController {
6 | constructor(private readonly appService: AppService) {}
7 |
8 | @Get()
9 | getHello(): string {
10 | return this.appService.getHello();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/front-end/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | server: {
7 | port: 8888,
8 | host: '0.0.0.0' // 绑定到所有接口
9 | },
10 | plugins: [vue()],
11 | resolve: {
12 | alias: {
13 | '@': '/src'
14 | }
15 | }
16 | })
17 |
18 |
--------------------------------------------------------------------------------
/front-end/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/back-end/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { SocketioModule } from './socketio/socketio.module';
5 |
6 | @Module({
7 | imports: [SocketioModule],
8 | controllers: [AppController],
9 | providers: [AppService],
10 | })
11 | export class AppModule {}
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # * About the project
2 |
3 | * The project is a Gomoku game practice project that supports local and LAN connectivity
4 | * Project stack
5 | * front vue3+tailwindcss
6 | * end nestjs+websocket+redis
7 | * Monorepo architecture based on PNPM makes project management more convenient
8 |
9 | # Getting Started
10 |
11 | run the front end and back end
12 |
13 | `pnpm -F front-end dev`
14 |
15 | `pnpm -F back-end start`
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wuziqi-fullstack",
3 | "version": "1.0.0",
4 | "description": "五子棋全栈应用",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": ["wuziqi", "game"],
10 | "author": "",
11 | "license": "ISC",
12 | "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a"
13 | }
14 |
--------------------------------------------------------------------------------
/front-end/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + Vue
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/back-end/src/socketio/socketio.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { SocketioService } from './socketio.service';
3 |
4 | describe('SocketioService', () => {
5 | let service: SocketioService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [SocketioService],
10 | }).compile();
11 |
12 | service = module.get(SocketioService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/front-end/src/lang/i18n.js:
--------------------------------------------------------------------------------
1 | import { createI18n } from "vue-i18n";
2 | import en from "./en";
3 | import zh_CN from "./zh_CN";
4 |
5 | const messages = {
6 | en: {
7 | ...en,
8 | },
9 | zh_CN: {
10 | ...zh_CN,
11 | },
12 | };
13 |
14 | let local = 'zh_CN'
15 | const i18n = createI18n({
16 | locale: local, // 设置当前语言类型
17 | legacy: false, // 如果要支持compositionAPI,此项必须设置为false;
18 | globalInjection: true, // 全局注册$t方法
19 | silentTranslationWarn: true,// 去掉警告
20 | missingWarn: false,
21 | silentFallbackWarn: true,//抑制警告
22 | missing: (locale, key) => {
23 | },
24 | messages,
25 | });
26 |
27 | export default i18n;
--------------------------------------------------------------------------------
/front-end/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "front-end",
3 | "private": true,
4 | "version": "1.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@vueuse/sound": "^2.1.3",
13 | "moment": "^2.30.1",
14 | "socket.io-client": "^4.7.5",
15 | "vue": "^3.4.29",
16 | "vue-i18n": "^9.13.1"
17 | },
18 | "devDependencies": {
19 | "@vitejs/plugin-vue": "^5.0.5",
20 | "autoprefixer": "^10.4.19",
21 | "postcss": "^8.4.38",
22 | "tailwindcss": "^3.4.4",
23 | "vite": "^5.3.1"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/back-end/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "ES2021",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true,
14 | "skipLibCheck": true,
15 | "strictNullChecks": false,
16 | "noImplicitAny": false,
17 | "strictBindCallApply": false,
18 | "forceConsistentCasingInFileNames": false,
19 | "noFallthroughCasesInSwitch": false
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/back-end/src/socketio/socketio.gateway.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { SocketioGateway } from './socketio.gateway';
3 | import { SocketioService } from './socketio.service';
4 |
5 | describe('SocketioGateway', () => {
6 | let gateway: SocketioGateway;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | providers: [SocketioGateway, SocketioService],
11 | }).compile();
12 |
13 | gateway = module.get(SocketioGateway);
14 | });
15 |
16 | it('should be defined', () => {
17 | expect(gateway).toBeDefined();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/back-end/src/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 |
5 | describe('AppController', () => {
6 | let appController: AppController;
7 |
8 | beforeEach(async () => {
9 | const app: TestingModule = await Test.createTestingModule({
10 | controllers: [AppController],
11 | providers: [AppService],
12 | }).compile();
13 |
14 | appController = app.get(AppController);
15 | });
16 |
17 | describe('root', () => {
18 | it('should return "Hello World!"', () => {
19 | expect(appController.getHello()).toBe('Hello World!');
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/back-end/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from './../src/app.module';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/front-end/src/components/PlayMode.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
17 | {{ $t("standaloneMode") }}
18 |
19 |
24 | {{ $t("lanMode") }}
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/back-end/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | tsconfigRootDir: __dirname,
6 | sourceType: 'module',
7 | },
8 | plugins: ['@typescript-eslint/eslint-plugin'],
9 | extends: [
10 | 'plugin:@typescript-eslint/recommended',
11 | 'plugin:prettier/recommended',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | ignorePatterns: ['.eslintrc.js'],
19 | rules: {
20 | '@typescript-eslint/interface-name-prefix': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | '@typescript-eslint/no-explicit-any': 'off',
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/front-end/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import './style.css'
3 | import App from './App.vue'
4 | import i18n from './lang/i18n.js' //语言
5 | import Socketio from "@/plugins/socket.io";
6 |
7 | // 创建应用实例
8 | const app = createApp(App)
9 |
10 | // 注册插件
11 | app.use(i18n)
12 | app.use(Socketio, {
13 | // connection:"http://192.168.1.102:3000",// "/* 这里填写服务端地址,如 http://localhost:3000 */",
14 | connection:"http://192.168.1.23:3000",// "/* 这里填写服务端地址,如 http://localhost:3000 */",
15 | options: {
16 | autoConnect: false, //关闭自动连接
17 | // ...其它选项
18 | },
19 | });
20 |
21 | // 全局属性
22 | app.config.globalProperties.$theme = {
23 | primary: '#f59e0b',
24 | secondary: '#ffe4c7',
25 | textPrimary: '#333333',
26 | textSecondary: '#666666'
27 | };
28 |
29 | // 挂载应用
30 | app.mount('#app')
31 |
--------------------------------------------------------------------------------
/back-end/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 | /build
5 |
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 | pnpm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | lerna-debug.log*
14 |
15 | # OS
16 | .DS_Store
17 |
18 | # Tests
19 | /coverage
20 | /.nyc_output
21 |
22 | # IDEs and editors
23 | /.idea
24 | .project
25 | .classpath
26 | .c9/
27 | *.launch
28 | .settings/
29 | *.sublime-workspace
30 |
31 | # IDE - VSCode
32 | .vscode/*
33 | !.vscode/settings.json
34 | !.vscode/tasks.json
35 | !.vscode/launch.json
36 | !.vscode/extensions.json
37 |
38 | # dotenv environment variable files
39 | .env
40 | .env.development.local
41 | .env.test.local
42 | .env.production.local
43 | .env.local
44 |
45 | # temp directory
46 | .temp
47 | .tmp
48 |
49 | # Runtime data
50 | pids
51 | *.pid
52 | *.seed
53 | *.pid.lock
54 |
55 | # Diagnostic reports (https://nodejs.org/api/report.html)
56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
57 |
--------------------------------------------------------------------------------
/front-end/src/pages/index.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | wuziqi game
24 |
28 | 开始游戏
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/back-end/src/socketio/socketio.gateway.ts:
--------------------------------------------------------------------------------
1 | import { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
2 | import { SocketioService } from './socketio.service';
3 | import { CreateSocketioDto } from './dto/create-socketio.dto';
4 | import { UpdateSocketioDto } from './dto/update-socketio.dto';
5 |
6 | @WebSocketGateway({ cors: true })
7 | export class SocketioGateway {
8 | @WebSocketServer() server;
9 | constructor(private readonly socketioService: SocketioService) {}
10 | // 接受来自客户端的棋盘状态并广播
11 | @SubscribeMessage('chessboard')
12 | chessboard(@MessageBody() { location, belongsTo }: { location: object, belongsTo: string },@ConnectedSocket() socket) {
13 | const updatedChessboard = this.socketioService.chessboard(location, belongsTo);
14 | // console.log('Current connected clients:', this.server.sockets.sockets);
15 | this.server.sockets.sockets.forEach((socket) => {
16 | // console.log('Client ID:', socket.id);
17 | socket.emit('currentChessboard', updatedChessboard)
18 | });
19 | console.log('Sender socket ID:', socket.id);
20 | return {
21 | event: 'currentChessboard',
22 | data: { ...updatedChessboard,socketId: socket.id}
23 | };
24 | }
25 |
26 |
27 | }
--------------------------------------------------------------------------------
/front-end/src/style.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | /* 全局样式优化 */
6 | body {
7 | background-color: #f5f5f5;
8 | font-family: 'Helvetica Neue', Arial, sans-serif;
9 | color: #333;
10 | }
11 |
12 | /* 自定义组件样式 */
13 | .game-container {
14 | max-width: 800px;
15 | margin: 0 auto;
16 | padding: 20px;
17 | background-color: white;
18 | border-radius: 12px;
19 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
20 | }
21 |
22 | .game-title {
23 | font-size: 2rem;
24 | font-weight: bold;
25 | text-align: center;
26 | margin-bottom: 1.5rem;
27 | color: #333;
28 | }
29 |
30 | .game-board {
31 | background-color: #ffe4c7;
32 | border-radius: 8px;
33 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
34 | overflow: hidden;
35 | }
36 |
37 | .player-info {
38 | display: flex;
39 | justify-content: space-between;
40 | align-items: center;
41 | margin-bottom: 1rem;
42 | padding: 0.75rem 1rem;
43 | background-color: #f8f8f8;
44 | border-radius: 8px;
45 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
46 | }
47 |
48 | .timer {
49 | font-size: 1.25rem;
50 | font-weight: 600;
51 | padding: 0.5rem 1rem;
52 | background-color: #fff;
53 | border-radius: 6px;
54 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
55 | }
56 |
57 | .btn {
58 | padding: 0.5rem 1.5rem;
59 | font-weight: 600;
60 | border-radius: 6px;
61 | transition: all 0.2s;
62 | cursor: pointer;
63 | }
64 |
65 | .btn-primary {
66 | background-color: #f59e0b;
67 | color: white;
68 | }
69 |
70 | .btn-primary:hover {
71 | background-color: #d97706;
72 | transform: translateY(-1px);
73 | }
--------------------------------------------------------------------------------
/front-end/src/composables/useGameState.js:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue';
2 | import moment from 'moment';
3 |
4 | export function useGameState(socket, props) {
5 | const gameState = ref('pieceSelection'); // 'pieceSelection', 'playing'
6 | const active = ref('whitePlayer');
7 | const disabled = ref(false);
8 | const countdown = ref('');
9 |
10 | let countdownTime = moment().add(15, "minutes");
11 | let countdownInterval;
12 |
13 | function updateCountdown() {
14 | const now = moment();
15 | const duration = moment.duration(countdownTime.diff(now));
16 | const minutes = Math.floor(duration.asMinutes());
17 | const seconds = Math.floor(duration.asSeconds()) % 60;
18 | countdown.value = `${minutes}:${seconds.toString().padStart(2, "0")}`;
19 |
20 | if (minutes <= 0 && seconds <= 0) {
21 | clearInterval(countdownInterval);
22 | alert("Time is up!");
23 | }
24 | }
25 |
26 | function resetGame() {
27 | // 重置当前玩家
28 | active.value = "whitePlayer";
29 |
30 | // 重置禁用状态
31 | disabled.value = false;
32 |
33 | // 重置计时器
34 | clearInterval(countdownInterval);
35 | countdownTime = moment().add(15, "minutes");
36 | countdownInterval = setInterval(updateCountdown, 1000);
37 |
38 | // 如果是联机模式,可能需要通知服务器重置游戏
39 | if (props.mode === "lan") {
40 | socket.emit("resetGame");
41 | }
42 | }
43 |
44 | function backToSelection() {
45 | // 清除计时器
46 | clearInterval(countdownInterval);
47 |
48 | // 切换回选择界面
49 | gameState.value = 'pieceSelection';
50 | }
51 |
52 | function startGame() {
53 | gameState.value = 'playing';
54 |
55 | // 在AI模式下,玩家总是红方(whitePlayer),AI总是黑方(blackPlayer)
56 | active.value = "whitePlayer";
57 |
58 | // 初始化计时器
59 | countdownInterval = setInterval(updateCountdown, 1000);
60 | }
61 |
62 | return {
63 | gameState,
64 | active,
65 | disabled,
66 | countdown,
67 | resetGame,
68 | backToSelection,
69 | startGame,
70 | updateCountdown
71 | };
72 | }
--------------------------------------------------------------------------------
/front-end/src/components/game/GameControls.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
黑方:
20 |
![]()
21 |
思考中
22 |
23 |
24 |
红方:
25 |
![]()
26 |
思考中
27 |
28 |
29 |
30 |
31 |
32 | ⏱️ 剩余时间
33 | {{ countdown }}
34 |
35 |
36 |
37 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/back-end/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "back-end",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "build": "nest build",
10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11 | "start": "nest start",
12 | "start:dev": "nest start --watch",
13 | "start:debug": "nest start --debug --watch",
14 | "start:prod": "node dist/main",
15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16 | "test": "jest",
17 | "test:watch": "jest --watch",
18 | "test:cov": "jest --coverage",
19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
20 | "test:e2e": "jest --config ./test/jest-e2e.json"
21 | },
22 | "dependencies": {
23 | "@nestjs/common": "^10.0.0",
24 | "@nestjs/core": "^10.0.0",
25 | "@nestjs/mapped-types": "*",
26 | "@nestjs/platform-express": "^10.0.0",
27 | "@nestjs/platform-socket.io": "^10.3.9",
28 | "@nestjs/websockets": "^10.3.9",
29 | "reflect-metadata": "^0.2.0",
30 | "rxjs": "^7.8.1"
31 | },
32 | "devDependencies": {
33 | "@nestjs/cli": "^10.0.0",
34 | "@nestjs/schematics": "^10.0.0",
35 | "@nestjs/testing": "^10.0.0",
36 | "@types/express": "^4.17.17",
37 | "@types/jest": "^29.5.2",
38 | "@types/node": "^20.3.1",
39 | "@types/supertest": "^6.0.0",
40 | "@typescript-eslint/eslint-plugin": "^6.0.0",
41 | "@typescript-eslint/parser": "^6.0.0",
42 | "eslint": "^8.42.0",
43 | "eslint-config-prettier": "^9.0.0",
44 | "eslint-plugin-prettier": "^5.0.0",
45 | "jest": "^29.5.0",
46 | "prettier": "^3.0.0",
47 | "source-map-support": "^0.5.21",
48 | "supertest": "^6.3.3",
49 | "ts-jest": "^29.1.0",
50 | "ts-loader": "^9.4.3",
51 | "ts-node": "^10.9.1",
52 | "tsconfig-paths": "^4.2.0",
53 | "typescript": "^5.1.3"
54 | },
55 | "jest": {
56 | "moduleFileExtensions": [
57 | "js",
58 | "json",
59 | "ts"
60 | ],
61 | "rootDir": "src",
62 | "testRegex": ".*\\.spec\\.ts$",
63 | "transform": {
64 | "^.+\\.(t|j)s$": "ts-jest"
65 | },
66 | "collectCoverageFrom": [
67 | "**/*.(t|j)s"
68 | ],
69 | "coverageDirectory": "../coverage",
70 | "testEnvironment": "node"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/front-end/src/App.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
🎮 五子棋游戏 🎲
18 |
19 |
20 |
26 |
27 |
33 |
34 |
35 |
36 | 👇
37 | 选择游戏模式开始游戏
38 | 👇
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
82 |
--------------------------------------------------------------------------------
/front-end/src/components/game/GameBoard.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/front-end/public/小鸡.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/front-end/src/composables/useChessboard.js:
--------------------------------------------------------------------------------
1 | import { ref, computed } from 'vue';
2 |
3 | export function useChessboard(socket, active, disabled, resetGame) {
4 | // 行数和列数
5 | const rows = ref(12);
6 | const cols = ref(12);
7 |
8 | // 初始化棋盘
9 | const boxMap = new Map();
10 | let row = 1;
11 | let col = 1;
12 | while (row <= rows.value) {
13 | while (col <= cols.value + 1) {
14 | boxMap.set(`row${row}col${col}`, { empty: true, belongsTo: null });
15 | col++;
16 | }
17 | row++;
18 | col = 1;
19 | }
20 |
21 | // 检查格子是否为空
22 | const isEmpty = (row, col) => {
23 | return boxMap.get(`row${row}col${col}`)?.empty;
24 | };
25 |
26 | // 检查格子属于谁
27 | const belongsToWho = (row, col) => {
28 | return boxMap.get(`row${row}col${col}`)?.belongsTo;
29 | };
30 |
31 | // 初始化位置样式
32 | function initLocaltion(location) {
33 | return {
34 | position: "absolute",
35 | top: "-50%",
36 | [location === "left-top" ? "left" : "right"]: "-50%",
37 | };
38 | }
39 |
40 | // 获取单元格样式
41 | function getCellStyle(row, col) {
42 | const prop = "1px solid #655b51";
43 | const style = {
44 | "border-left": prop,
45 | "border-top": prop,
46 | };
47 | row === rows.value && (style["border-bottom"] = prop);
48 | col === cols.value && (style["border-right"] = prop);
49 | return style;
50 | }
51 |
52 | // 获取点击的角落
53 | function getCornerClicked(element, clientX, clientY) {
54 | const rect = element.getBoundingClientRect();
55 | // 计算点击位置相对于元素四角的距离
56 | const topLeftDistance = Math.sqrt(
57 | Math.pow(clientX - rect.left, 2) + Math.pow(clientY - rect.top, 2)
58 | );
59 | const topRightDistance = Math.sqrt(
60 | Math.pow(clientX - rect.right, 2) + Math.pow(clientY - rect.top, 2)
61 | );
62 | const bottomLeftDistance = Math.sqrt(
63 | Math.pow(clientX - rect.left, 2) + Math.pow(clientY - rect.bottom, 2)
64 | );
65 | const bottomRightDistance = Math.sqrt(
66 | Math.pow(clientX - rect.right, 2) + Math.pow(clientY - rect.bottom, 2)
67 | );
68 |
69 | // 找出最短距离对应的角落
70 | const minDistance = Math.min(
71 | topLeftDistance,
72 | topRightDistance,
73 | bottomLeftDistance,
74 | bottomRightDistance
75 | );
76 | if (minDistance === topLeftDistance) {
77 | return "topLeft";
78 | } else if (minDistance === topRightDistance) {
79 | return "topRight";
80 | } else if (minDistance === bottomLeftDistance) {
81 | return "bottomLeft";
82 | } else if (minDistance === bottomRightDistance) {
83 | return "bottomRight";
84 | } else {
85 | return "none";
86 | }
87 | }
88 |
89 | // 发送棋盘信息
90 | function emitChessboard(location, belongsTo) {
91 | socket.emit("chessboard", { location, belongsTo }, (data) => {
92 | console.log("chessboard:", data);
93 | });
94 | }
95 |
96 | // 放下棋子
97 | function putDownPiece(row, col, event) {
98 | const location = getCornerClicked(event.target, event.clientX, event.clientY);
99 | //处理行列,由单元格行变为边框,每个单元格跨2行2列
100 | if (["bottomLeft", "bottomRight"].includes(location)) {
101 | row += 1;
102 | }
103 | if (["topRight", "bottomRight"].includes(location)) {
104 | col += 1;
105 | }
106 | if (boxMap.get(`row${row}col${col}`)?.empty) {
107 | boxMap.set(`row${row}col${col}`, {
108 | empty: false,
109 | belongsTo: active.value,
110 | location,
111 | });
112 | emitChessboard({ row, col }, active.value);
113 | if (validSuccess(row, col, active.value)) {
114 | alert(`${active.value} 获胜!`);
115 | resetGame();
116 | } else {
117 | active.value =
118 | active.value === "whitePlayer" ? "blackPlayer" : "whitePlayer";
119 | }
120 | }
121 | }
122 |
123 | // 判断是否获胜
124 | function validSuccess(row, col, active) {
125 | // 检查水平方向
126 | let count = 1;
127 |
128 | // 水平方向最小列,最大列,默认已经有一个棋子了,所以只需要增减4
129 | const minCol = col - 4 > 0 ? col - 4 : 0;
130 | const maxCol = col + 4 < cols.value ? col + 4 : cols.value;
131 |
132 | // 水平方向最小列,最大列
133 | const minRow = row - 4 > 0 ? row - 4 : 0;
134 | const maxRow = row + 4 < rows.value ? row + 4 : rows.value;
135 |
136 | //向左检查
137 | for (
138 | let i = col - 1;
139 | i >= minCol && boxMap.get(`row${row}col${i}`)?.belongsTo === active;
140 | i--
141 | ) {
142 | count++;
143 | if (count === 5) return true;
144 | }
145 | for (
146 | let i = col + 1;
147 | i <= maxCol && boxMap.get(`row${row}col${i}`)?.belongsTo === active;
148 | i++
149 | ) {
150 | count++;
151 | if (count === 5) return true;
152 | }
153 |
154 | // 检查垂直方向
155 | count = 1;
156 |
157 | for (
158 | let i = row - 1;
159 | i >= minRow && boxMap.get(`row${i}col${col}`)?.belongsTo === active;
160 | i--
161 | ) {
162 | count++;
163 | if (count === 5) return true;
164 | }
165 | for (
166 | let i = row + 1;
167 | i <= maxRow && boxMap.get(`row${i}col${col}`)?.belongsTo === active;
168 | i++
169 | ) {
170 | count++;
171 | if (count === 5) return true;
172 | }
173 | // 检查斜线方向(左上到右下)
174 | count = 1;
175 | for (
176 | let i = 1;
177 | row - i >= minRow &&
178 | col - i >= minCol &&
179 | boxMap.get(`row${row - i}col${col - i}`)?.belongsTo === active;
180 | i++
181 | ) {
182 | count++;
183 | }
184 | for (
185 | let i = 1;
186 | row + i <= maxRow &&
187 | col + i <= maxCol &&
188 | boxMap.get(`row${row + i}col${col + i}`)?.belongsTo === active;
189 | i++
190 | ) {
191 | count++;
192 | }
193 | if (count >= 5) return true;
194 |
195 | // 检查斜线方向(右上到左下)
196 | count = 1;
197 | for (
198 | let i = 1;
199 | row - i >= minRow &&
200 | col + i <= maxCol &&
201 | boxMap.get(`row${row - i}col${col + i}`)?.belongsTo === active;
202 | i++
203 | ) {
204 | count++;
205 | }
206 | for (
207 | let i = 1;
208 | row + i <= maxRow &&
209 | col - i >= minCol &&
210 | boxMap.get(`row${row + i}col${col - i}`)?.belongsTo === active;
211 | i++
212 | ) {
213 | count++;
214 | }
215 | if (count >= 5) return true;
216 |
217 | return false;
218 | }
219 |
220 | // 重置棋盘
221 | function resetChessboard() {
222 | for (let r = 1; r <= rows.value; r++) {
223 | for (let c = 1; c <= cols.value + 1; c++) {
224 | boxMap.set(`row${r}col${c}`, { empty: true, belongsTo: null });
225 | }
226 | }
227 | }
228 |
229 | return {
230 | rows,
231 | cols,
232 | boxMap,
233 | isEmpty,
234 | belongsToWho,
235 | putDownPiece,
236 | validSuccess,
237 | getCellStyle,
238 | initLocaltion,
239 | emitChessboard,
240 | resetChessboard
241 | };
242 | }
--------------------------------------------------------------------------------
/front-end/src/components/game/PieceSelection.vue:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
61 |
62 |
63 | 个性化设置
64 |
65 |
66 |
请选择您喜欢的棋子
67 |
68 |
69 |
70 |
71 |
72 |
![]()
73 |
74 |
黑方
75 |
76 |
VS
77 |
78 |
79 |
![]()
80 |
81 |
红方
82 |
83 |
84 |
85 |
86 |
128 |
129 |
130 |
131 |
132 |
151 |
152 |
153 |
172 |
173 |
174 |
175 |
176 |
185 |
186 |
187 |
188 |
189 | 请为黑方和红方各选择一种棋子
190 |
191 |
192 |
193 |
194 |
--------------------------------------------------------------------------------
/front-end/src/composables/useAI.js:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue';
2 |
3 | export function useAi(boxMap, rows, cols) {
4 | const isAIMode = ref(false);
5 | const aiDifficulty = ref('medium'); // 'easy', 'medium', 'hard'
6 |
7 | // 切换AI模式
8 | function toggleAIMode() {
9 | isAIMode.value = !isAIMode.value;
10 | }
11 |
12 | // 设置AI难度
13 | function setAIDifficulty(difficulty) {
14 | aiDifficulty.value = difficulty;
15 | }
16 |
17 | // AI下棋逻辑
18 | function aiMove(active, emitChessboard, validSuccess, resetGame) {
19 | if (!isAIMode.value || active.value !== 'blackPlayer') return;
20 |
21 | // 延迟一下,模拟AI思考时间
22 | return new Promise(resolve => {
23 | setTimeout(() => {
24 | // 根据难度选择不同的AI策略
25 | let move;
26 |
27 | switch(aiDifficulty.value) {
28 | case 'easy':
29 | move = findRandomMove();
30 | break;
31 | case 'medium':
32 | move = findBetterMove();
33 | break;
34 | case 'hard':
35 | move = findBestMove();
36 | break;
37 | default:
38 | move = findBetterMove();
39 | }
40 |
41 | if (move) {
42 | // 直接使用简化版本设置棋子
43 | const location = 'topLeft'; // 默认位置
44 |
45 | boxMap.set(`row${move.row}col${move.col}`, {
46 | empty: false,
47 | belongsTo: active.value,
48 | location,
49 | });
50 |
51 | // 如果需要,发送联机消息
52 | emitChessboard({ row: move.row, col: move.col }, active.value);
53 |
54 | if (validSuccess(move.row, move.col, active.value)) {
55 | alert(`${active.value} 获胜!`);
56 | resetGame();
57 | } else {
58 | active.value = 'whitePlayer';
59 | }
60 |
61 | resolve();
62 | }
63 | }, 800); // 思考时间800毫秒
64 | });
65 | }
66 |
67 | // 随机找一个空位下棋(简单难度)
68 | function findRandomMove() {
69 | const emptyPositions = [];
70 |
71 | // 收集所有空位
72 | for (let r = 1; r <= rows.value; r++) {
73 | for (let c = 1; c <= cols.value; c++) {
74 | if (boxMap.get(`row${r}col${c}`)?.empty) {
75 | emptyPositions.push({ row: r, col: c });
76 | }
77 | }
78 | }
79 |
80 | // 随机选择一个空位
81 | if (emptyPositions.length > 0) {
82 | const randomIndex = Math.floor(Math.random() * emptyPositions.length);
83 | return emptyPositions[randomIndex];
84 | }
85 |
86 | return null;
87 | }
88 |
89 | // 寻找更好的位置(中等难度)
90 | function findBetterMove() {
91 | // 先检查是否有可以连成五子的位置
92 | for (let r = 1; r <= rows.value; r++) {
93 | for (let c = 1; c <= cols.value; c++) {
94 | if (boxMap.get(`row${r}col${c}`)?.empty) {
95 | // 模拟AI下在这个位置
96 | boxMap.set(`row${r}col${c}`, { empty: false, belongsTo: 'blackPlayer' });
97 |
98 | // 检查是否能赢
99 | if (checkWinningCondition(r, c, 'blackPlayer')) {
100 | // 恢复原状态
101 | boxMap.set(`row${r}col${c}`, { empty: true, belongsTo: null });
102 | return { row: r, col: c };
103 | }
104 |
105 | // 恢复原状态
106 | boxMap.set(`row${r}col${c}`, { empty: true, belongsTo: null });
107 | }
108 | }
109 | }
110 |
111 | // 检查是否需要阻止对手连成五子
112 | for (let r = 1; r <= rows.value; r++) {
113 | for (let c = 1; c <= cols.value; c++) {
114 | if (boxMap.get(`row${r}col${c}`)?.empty) {
115 | // 模拟玩家下在这个位置
116 | boxMap.set(`row${r}col${c}`, { empty: false, belongsTo: 'whitePlayer' });
117 |
118 | // 检查玩家是否能赢
119 | if (checkWinningCondition(r, c, 'whitePlayer')) {
120 | // 恢复原状态
121 | boxMap.set(`row${r}col${c}`, { empty: true, belongsTo: null });
122 | return { row: r, col: c };
123 | }
124 |
125 | // 恢复原状态
126 | boxMap.set(`row${r}col${c}`, { empty: true, belongsTo: null });
127 | }
128 | }
129 | }
130 |
131 | // 如果没有紧急情况,使用战略性随机位置
132 | return findStrategicRandomMove();
133 | }
134 |
135 | // 寻找最佳位置(困难难度)
136 | function findBestMove() {
137 | // 实现更复杂的AI算法,例如极小极大算法或评估函数
138 | // 这里简化为增强版的中等难度
139 |
140 | // 先检查是否有可以连成五子的位置
141 | for (let r = 1; r <= rows.value; r++) {
142 | for (let c = 1; c <= cols.value; c++) {
143 | if (boxMap.get(`row${r}col${c}`)?.empty) {
144 | // 模拟AI下在这个位置
145 | boxMap.set(`row${r}col${c}`, { empty: false, belongsTo: 'blackPlayer' });
146 |
147 | // 检查是否能赢
148 | if (checkWinningCondition(r, c, 'blackPlayer')) {
149 | // 恢复原状态
150 | boxMap.set(`row${r}col${c}`, { empty: true, belongsTo: null });
151 | return { row: r, col: c };
152 | }
153 |
154 | // 恢复原状态
155 | boxMap.set(`row${r}col${c}`, { empty: true, belongsTo: null });
156 | }
157 | }
158 | }
159 |
160 | // 检查是否需要阻止对手连成五子
161 | for (let r = 1; r <= rows.value; r++) {
162 | for (let c = 1; c <= cols.value; c++) {
163 | if (boxMap.get(`row${r}col${c}`)?.empty) {
164 | // 模拟玩家下在这个位置
165 | boxMap.set(`row${r}col${c}`, { empty: false, belongsTo: 'whitePlayer' });
166 |
167 | // 检查玩家是否能赢
168 | if (checkWinningCondition(r, c, 'whitePlayer')) {
169 | // 恢复原状态
170 | boxMap.set(`row${r}col${c}`, { empty: true, belongsTo: null });
171 | return { row: r, col: c };
172 | }
173 |
174 | // 恢复原状态
175 | boxMap.set(`row${r}col${c}`, { empty: true, belongsTo: null });
176 | }
177 | }
178 | }
179 |
180 | // 检查是否有可以连成四子的位置
181 | for (let r = 1; r <= rows.value; r++) {
182 | for (let c = 1; c <= cols.value; c++) {
183 | if (boxMap.get(`row${r}col${c}`)?.empty) {
184 | // 模拟AI下在这个位置
185 | boxMap.set(`row${r}col${c}`, { empty: false, belongsTo: 'blackPlayer' });
186 |
187 | // 检查是否能形成四子连线
188 | if (checkConsecutivePieces(r, c, 'blackPlayer', 4)) {
189 | // 恢复原状态
190 | boxMap.set(`row${r}col${c}`, { empty: true, belongsTo: null });
191 | return { row: r, col: c };
192 | }
193 |
194 | // 恢复原状态
195 | boxMap.set(`row${r}col${c}`, { empty: true, belongsTo: null });
196 | }
197 | }
198 | }
199 |
200 | // 如果没有特殊情况,使用战略性随机位置
201 | return findStrategicRandomMove();
202 | }
203 |
204 | // 检查是否有连续的棋子
205 | function checkConsecutivePieces(row, col, player, n) {
206 | // 检查八个方向
207 | const directions = [
208 | [0, 1], // 水平
209 | [1, 0], // 垂直
210 | [1, 1], // 对角线
211 | [1, -1] // 反对角线
212 | ];
213 |
214 | for (const [dr, dc] of directions) {
215 | let count = 1; // 当前位置已经有一个棋子
216 |
217 | // 正向检查
218 | for (let i = 1; i < 5; i++) {
219 | const r = row + dr * i;
220 | const c = col + dc * i;
221 |
222 | if (r >= 1 && r <= rows.value && c >= 1 && c <= cols.value) {
223 | if (boxMap.get(`row${r}col${c}`)?.belongsTo === player) {
224 | count++;
225 | } else {
226 | break;
227 | }
228 | } else {
229 | break;
230 | }
231 | }
232 |
233 | // 反向检查
234 | for (let i = 1; i < 5; i++) {
235 | const r = row - dr * i;
236 | const c = col - dc * i;
237 |
238 | if (r >= 1 && r <= rows.value && c >= 1 && c <= cols.value) {
239 | if (boxMap.get(`row${r}col${c}`)?.belongsTo === player) {
240 | count++;
241 | } else {
242 | break;
243 | }
244 | } else {
245 | break;
246 | }
247 | }
248 |
249 | // 如果连线数量等于n,返回true
250 | if (count >= n) {
251 | return true;
252 | }
253 | }
254 |
255 | return false;
256 | }
257 |
258 | // 检查是否获胜
259 | function checkWinningCondition(row, col, player) {
260 | return checkConsecutivePieces(row, col, player, 5);
261 | }
262 |
263 | // 寻找战略性随机位置(靠近已有棋子的空位)
264 | function findStrategicRandomMove() {
265 | const allEmptyPositions = [];
266 | const strategicPositions = [];
267 |
268 | // 收集所有空位和战略位置
269 | for (let r = 1; r <= rows.value; r++) {
270 | for (let c = 1; c <= cols.value; c++) {
271 | if (boxMap.get(`row${r}col${c}`)?.empty) {
272 | allEmptyPositions.push({ row: r, col: c });
273 |
274 | // 检查周围是否有棋子
275 | if (hasAdjacentPiece(r, c)) {
276 | strategicPositions.push({ row: r, col: c });
277 | }
278 | }
279 | }
280 | }
281 |
282 | // 优先选择战略位置,如果没有则随机选择
283 | if (strategicPositions.length > 0) {
284 | const randomIndex = Math.floor(Math.random() * strategicPositions.length);
285 | return strategicPositions[randomIndex];
286 | } else if (allEmptyPositions.length > 0) {
287 | const randomIndex = Math.floor(Math.random() * allEmptyPositions.length);
288 | return allEmptyPositions[randomIndex];
289 | }
290 |
291 | return null;
292 | }
293 |
294 | // 检查周围是否有棋子
295 | function hasAdjacentPiece(row, col) {
296 | for (let dr = -1; dr <= 1; dr++) {
297 | for (let dc = -1; dc <= 1; dc++) {
298 | if (dr === 0 && dc === 0) continue;
299 |
300 | const r = row + dr;
301 | const c = col + dc;
302 |
303 | if (r >= 1 && r <= rows.value && c >= 1 && c <= cols.value) {
304 | if (!boxMap.get(`row${r}col${c}`)?.empty) {
305 | return true;
306 | }
307 | }
308 | }
309 | }
310 |
311 | return false;
312 | }
313 |
314 | return {
315 | isAIMode,
316 | aiDifficulty,
317 | toggleAIMode,
318 | setAIDifficulty,
319 | aiMove
320 | };
321 | }
--------------------------------------------------------------------------------
/front-end/public/仓鼠.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/front-end/public/白猫.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/front-end/src/components/index.vue:
--------------------------------------------------------------------------------
1 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 | 🎮 五子棋游戏 🎲
161 | 热门
162 |
163 |
164 |
165 |
177 |
178 |
179 |
180 |
190 |
191 |
192 |
193 |
234 |
235 |
236 |
237 | 当前轮到:
238 |
239 | {{ active === 'whitePlayer' ? '红方 🔴' : '黑方 ⚫' }}
240 |
241 |
242 |
243 |
244 |
245 |
246 |
252 |
253 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
喜欢这个游戏?请分享给朋友!
269 |
270 |
273 |
276 |
279 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
358 |
--------------------------------------------------------------------------------
/front-end/public/可达鸭.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/front-end/public/橘猫.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/front-end/public/三花猫.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/front-end/public/柴犬.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/front-end/public/金毛.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/front-end/public/咕噜噜.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/front-end/public/哈士奇.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/front-end/public/柯基.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------