├── .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 | 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 | 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 | 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 | 51 | 52 | 82 | -------------------------------------------------------------------------------- /front-end/src/components/game/GameBoard.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 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 | 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 | 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 | --------------------------------------------------------------------------------