diff --git a/sudukuing/README.md b/sudukuing/README.md new file mode 100644 index 0000000..ddbd81e --- /dev/null +++ b/sudukuing/README.md @@ -0,0 +1,21 @@ +# pointgaming +### 1、项目说明 +总体思路:扫雷程序项目开发的总体思路如图1.1示意,拟实现交互界面与交互控制、数据结构和扫雷算法等。 + 图1.1 项目开发的总体思路图 + + + +### 2、开发环境说明 +(1)系统版本:OpenHarmony 4.0 Release +(2)SDK版本:API 10 +(3)Model类型:Stage +(4)DevEco Studio版本:Deveco Studio 4.0 release +(5)硬件环境:Unionpi Whale、RK3566开发板 +(6)预览器参数:(1)DeviceType:default;(2)Resolution:560-1280;(3)DPI:240;其余参数自定。 +### 3、项目主要函数调用关系图与项目输出界面演进图 + 图3.1 项目主要函数调用关系图 + + + + 图3.2 项目输出界面演进图 + \ No newline at end of file diff --git a/sudukuing/figs/evolution.png b/sudukuing/figs/evolution.png new file mode 100644 index 0000000..732bfbf Binary files /dev/null and b/sudukuing/figs/evolution.png differ diff --git a/sudukuing/figs/overall.png b/sudukuing/figs/overall.png new file mode 100644 index 0000000..214b181 Binary files /dev/null and b/sudukuing/figs/overall.png differ diff --git a/sudukuing/figs/relation.png b/sudukuing/figs/relation.png new file mode 100644 index 0000000..8441f6a Binary files /dev/null and b/sudukuing/figs/relation.png differ diff --git a/sudukuing/sudoku/.gitignore b/sudukuing/sudoku/.gitignore new file mode 100644 index 0000000..fbabf77 --- /dev/null +++ b/sudukuing/sudoku/.gitignore @@ -0,0 +1,11 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test \ No newline at end of file diff --git a/sudukuing/sudoku/AppScope/app.json5 b/sudukuing/sudoku/AppScope/app.json5 new file mode 100644 index 0000000..8f8eb49 --- /dev/null +++ b/sudukuing/sudoku/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.example.sudoku", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:app_icon", + "label": "$string:app_name" + } +} diff --git a/sudukuing/sudoku/AppScope/resources/base/element/string.json b/sudukuing/sudoku/AppScope/resources/base/element/string.json new file mode 100644 index 0000000..b124805 --- /dev/null +++ b/sudukuing/sudoku/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "Sudoku" + } + ] +} diff --git a/sudukuing/sudoku/AppScope/resources/base/media/app_icon.png b/sudukuing/sudoku/AppScope/resources/base/media/app_icon.png new file mode 100644 index 0000000..cd45acc Binary files /dev/null and b/sudukuing/sudoku/AppScope/resources/base/media/app_icon.png differ diff --git a/sudukuing/sudoku/build-profile.json5 b/sudukuing/sudoku/build-profile.json5 new file mode 100644 index 0000000..cc9434a --- /dev/null +++ b/sudukuing/sudoku/build-profile.json5 @@ -0,0 +1,36 @@ +{ + "app": { + "signingConfigs": [], + "products": [ + { + "name": "default", + "signingConfig": "default", + "compileSdkVersion": 10, + "compatibleSdkVersion": 9, + "runtimeOS": "OpenHarmony", + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/.gitignore b/sudukuing/sudoku/entry/.gitignore new file mode 100644 index 0000000..e2713a2 --- /dev/null +++ b/sudukuing/sudoku/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/sudukuing/sudoku/entry/build-profile.json5 b/sudukuing/sudoku/entry/build-profile.json5 new file mode 100644 index 0000000..197e83c --- /dev/null +++ b/sudukuing/sudoku/entry/build-profile.json5 @@ -0,0 +1,31 @@ +{ + "apiType": "stageMode", + "buildOption": { + "arkOptions": { + // "apPath": "./modules.ap" /* Profile used for profile-guided optimization (PGO), a compiler optimization technique to improve app runtime performance. */ + } + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": true, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/hvigorfile.ts b/sudukuing/sudoku/entry/hvigorfile.ts new file mode 100644 index 0000000..c6edcd9 --- /dev/null +++ b/sudukuing/sudoku/entry/hvigorfile.ts @@ -0,0 +1,6 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/sudukuing/sudoku/entry/obfuscation-rules.txt b/sudukuing/sudoku/entry/obfuscation-rules.txt new file mode 100644 index 0000000..985b2ae --- /dev/null +++ b/sudukuing/sudoku/entry/obfuscation-rules.txt @@ -0,0 +1,18 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://gitee.com/openharmony/arkcompiler_ets_frontend/blob/master/arkguard/README.md + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope \ No newline at end of file diff --git a/sudukuing/sudoku/entry/oh-package.json5 b/sudukuing/sudoku/entry/oh-package.json5 new file mode 100644 index 0000000..225946c --- /dev/null +++ b/sudukuing/sudoku/entry/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "license": "", + "devDependencies": {}, + "author": "", + "name": "entry", + "description": "Please describe the basic information.", + "main": "", + "version": "1.0.0", + "dependencies": {} +} diff --git a/sudukuing/sudoku/entry/src/main/ets/classes/Mytimer.ets b/sudukuing/sudoku/entry/src/main/ets/classes/Mytimer.ets new file mode 100644 index 0000000..3f81eee --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/classes/Mytimer.ets @@ -0,0 +1,54 @@ +export default class MyTimer { + private elapsed: number; + private timer: number | null; + private starttime: number; + + constructor() { + this.elapsed = 0; + this.timer = null; + this.starttime = Date.now(); + this.start(); + } + + start() { // 时间开始 + this.starttime = Date.now() - this.elapsed * 1000; + if (this.timer === null) { + this.update(); + } + } + + update() { // 实现时间每秒的更新 + const now = Date.now(); + this.elapsed = Math.floor((now - this.starttime) / 1000); + if (this.timer !== null) { + clearTimeout(this.timer); + } + this.timer = setTimeout((): void => this.update(), 1000); + } + + + + getMinutes(): number { + return Math.floor(this.elapsed / 60); + } + + getSeconds(): number { + return this.elapsed % 60; + } + + restart() { // 时间重置 + this.elapsed = 0; + this.starttime = Date.now(); + if (this.timer !== null) { + clearTimeout(this.timer); + } + this.update(); + } + + stop() { // 时间停止 + if (this.timer !== null) { + clearTimeout(this.timer); + this.timer = null; + } + } +} diff --git a/sudukuing/sudoku/entry/src/main/ets/classes/SudoKu.ets b/sudukuing/sudoku/entry/src/main/ets/classes/SudoKu.ets new file mode 100644 index 0000000..86bc6b3 --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/classes/SudoKu.ets @@ -0,0 +1,190 @@ +// 数独核心类 +export default class SudoKu{ + martix:number[][] // 数独矩阵 + + // 构造函数 + constructor() { + // 初始化9x9的数独矩阵 + this.martix=new Array(9) + + for(let i=0;i<9;i++){ + this.martix[i]=new Array(9) + for(let j=0;j<9;j++){ + this.martix[i][j]=0 // 初始化为0表示空格 + } + } + } + + // 初始化矩阵(随机填充数字,用于测试) + init(){ + for(let i=0;i<9;i++){ + for(let j=0;j<9;j++){ + this.martix[i][j]=Math.floor(Math.random() * (9 - 1 + 1))+ 1 + } + } + } + + // 计算整个数组是否符合数独要求 + judge(){ + for(let i=0;i<9;i++){ + for(let j=0;j<9;j++){ + if(this.martix[i][j]==0){ + continue // 跳过空格 + } + let m=Math.floor(i/3) // 计算所在3x3宫格的行索引 + let n=Math.floor(j/3) // 计算所在3x3宫格的列索引 + + // 检查行是否有重复数字 + let row_martix=this.martix[i] + let row_set=row_martix.filter(num=>num!=0) + // 检查列是否有重复数字 + let col_martix=this.martix[j] + let col_set=col_martix.filter(num=>num!=0) + + // 检查3x3宫格是否有重复数字 + let subBlock:number[][]=[] + for(let i=m*3;inum!=0) + + // 统计0的数目 + let rowNum0=row_martix.filter(num=>num==0).length + // 若集合里的数字个数(不包括0)加上0的个数小于9,说明有非0的数字重复 + if(row_martix.length!=row_set.length+rowNum0){ + return false + } + + let colNum0=col_martix.filter(num=>num==0).length + if(col_martix.length!=col_set.length+colNum0){ + return false + } + + let blockNum0=block_martix.filter(num=>num==0).length + if(block_martix.length!=block_set.length+blockNum0){ + return false + } + } + } + return true // 所有检查通过,符合数独规则 + } + + // 获取一个格子里可能可以填的值 + getPossible(row:number,col:number): Set{ + let Nums:Set=new Set([1,2,3,4,5,6,7,8,9]) // 1-9的完整集合 + let m=Math.floor(row/3) // 计算所在3x3宫格的行索引 + let n=Math.floor(col/3) // 计算所在3x3宫格的列索引 + let set:Set=new Set() // 创建集合用于存入出现的数字 + + // 收集所在列出现的数字 + for(let i=0;i<9;i++){ + if(i==row){ + continue // 跳过当前位置 + } + set.add(this.martix[i][col])// 将所在列出现数字加入集合 + } + + // 收集所在行出现的数字 + for(let j=0;j<9;j++){ + if(j==col){ + continue // 跳过当前位置 + } + set.add(this.martix[row][j])// 将所在行出现数字加入集合 + } + + // 收集所在3x3宫格出现的数字 + for(let i=m*3;i, setB:Set) { + // 创建一个新的Set对象,包含setA中但不在setB中的元素 + let difference = new Set(setA); + for (let elem of setB) { + difference.delete(elem); + } + return difference; + } + + // 数独求解算法(深度优先搜索) + solve(martix:number[][]):boolean{ + // 遍历数独矩阵 + for(let i =0;i<9;i++){ + let row = (i+5)%9 // 行索引偏移,提高求解效率 + for(let j =0;j<9;j++){ + let col = (j+4)%9 // 列索引偏移,提高求解效率 + // 如果当前位置为空格 + if(martix[row][col]==0){ + let possible = this.getPossible(row,col)//获取所有可能的数字 + // 尝试每个可能的数字 + for(let value of possible){ + martix[row][col] = value//将可能的数字填入 + // 递归求解,如果找到解则返回true + if(this.solve(martix)){ + return true + } + // 如果当前填入的数字会导致后面无解则依然填入0表示空白待填 + martix[row][col]=0 + } + return false // 所有可能数字都尝试过但无解 + } + } + } + return true // 所有格子都已填满,求解完成 + } + + // 初始化数独矩阵 + InitMartix(){ + let flag = false // 初始化一个标志,用于判断数独是否可以解决 + // 使用循环来确保生成的数独矩阵是可解的 + while(!flag){ + // 将整个数独矩阵初始化为0 + for(let i = 0;i<9;i++){ + for(let j =0;j<9;j++){ + this.martix[i][j]=0 + } + } + + // 随机在三个对角位置填入1-9的随机数(提供初始条件) + this.martix[0][0]=Math.floor(Math.random()*9)+1 + this.martix[3][3]=Math.floor(Math.random()*9)+1 + this.martix[6][6]=Math.floor(Math.random()*9)+1 + + flag = this.solve(this.martix) // 检查当前矩阵是否可解 + + // 随机清除一些数字以生成数独谜题 + for (let i = 0; i < 9; i++) { + for (let j = 0; j < 9; j++) { + let ran = Math.floor(Math.random() * 151) - 50 + if (ran < 0) { + this.martix[i][j] = 0; + } + } + } + } + } + + // 计算空格数量 + countEmptyCells():number{ + let count=0 + for(let i = 0;i<9;i++){ + for(let j=0;j<9;j++){ + if(this.martix[i][j]==0){ + count++ + } + } + } + return count + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/ets/common/constants/CommonConstants.ets b/sudukuing/sudoku/entry/src/main/ets/common/constants/CommonConstants.ets new file mode 100644 index 0000000..aaa5cec --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/common/constants/CommonConstants.ets @@ -0,0 +1,26 @@ +// 公共常量定义文件 + +// 棋盘行列数(注释掉的9行9列是标准数独,当前使用27可能是为了适配特定显示需求) +export const boardNum: number = 27; //行列数 +// export const boardNum: number = 9; //行列数 + +// 格子大小定义 +export const miniGridSize: number = 12; // 小格子大小 +export const gridSize: number = 36; // 格子大小 + +// 偏移值大小 +export const offSet:number=5 + +// Canvas渲染上下文设置 +export const settings: RenderingContextSettings = new RenderingContextSettings(true); +// 主要Canvas渲染上下文 +export const context: CanvasRenderingContext2D = new CanvasRenderingContext2D(settings); + +// 第二个Canvas渲染上下文设置 +export const settings2: RenderingContextSettings = new RenderingContextSettings(true); +// 用于绘制数独数字的Canvas渲染上下文 +export const context2: CanvasRenderingContext2D = new CanvasRenderingContext2D(settings2); + +// 图片资源定义 +export const imgWhiteBlock:ImageBitmap = new ImageBitmap("common/pictures/icon_block_white.png");//白色方块背景 +export const imgYellowBlock:ImageBitmap = new ImageBitmap("common/pictures/icon_block_yellow.png");//黄色方块背景 \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/ets/common/constants/globalState.ets b/sudukuing/sudoku/entry/src/main/ets/common/constants/globalState.ets new file mode 100644 index 0000000..2a7d48e --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/common/constants/globalState.ets @@ -0,0 +1,136 @@ +import SudoKu from '../../classes/SudoKu' +import MyTimer from '../../classes/Mytimer' + +// 定义展示消息接口 +interface ShowMsg { + no: number // 序号 + row: number // 行索引 + col: number // 列索引 + selectNum: number // 选择的数字 +} + +// 定义撤销状态接口 +interface UndoState { + martix: number[][] // 数独矩阵 + step: ShowMsg[] // 步骤记录 +} + +// 全局状态管理类 +class GlobalState { + public sudokuInstance: SudoKu = new SudoKu() // 数独实例 + public myTimer: MyTimer = new MyTimer() // 计时器实例 + public showArr: ShowMsg[] = [] // 展示数组 + public showArrItem: ShowMsg = { no: 1, row: 0, col: 0, selectNum: 0 } // 展示项 + private subscribers: (() => void)[] = [] // 订阅者数组 + public selectedNum: number = 0 // 选中的数字 + public initMatrix: number[][] = [] // 存储初始化数独数据 + private undoStack: UndoState[] = [] // 存储步骤信息用于撤销 + public stepCount: number = 0 // 步骤计数 + public undoflag:boolean = false // 标记撤回模式 + public ifsave:boolean = false // 切换笔记模式 + public ifnote:boolean = false // 提示记录标志 + public ifrecord:boolean = false // 一键笔记模式标志 + public noteMatrix:Set[][] // 存储每个位置的笔记数字 + public waittime:number = 500 // 设置睡眠时间 + public emptycell:number=0 // 计算空格数 + public GameOver:boolean = false // 游戏结束标志 + public iswork:boolean = false // 判断自动求解状态 + + // 构造函数 + constructor() { + this.initState() // 初始化状态 + // 初始化笔记矩阵 + this.noteMatrix=[] + for(let i = 0;i < 9;i++){ + this.noteMatrix[i] = [] + for(let j = 0;j < 9;j++){ + this.noteMatrix[i][j] = new Set() + } + } + } + + // 初始化状态 + public initState() { + this.iswork = false // 重置自动求解标志 + this.GameOver = false // 重置游戏结束标志 + this.sudokuInstance.InitMartix() // 刷新数独 + this.emptycell = this.sudokuInstance.countEmptyCells() // 重新计算空格数,保证难度计算的正确 + this.initMatrix = JSON.parse(JSON.stringify(this.sudokuInstance.martix)) // 深拷贝 + this.showArr = [] // 清空存储展示数据 + this.showArrItem = { no: 0, row: 0, col: 0, selectNum: 0 } + this.selectedNum = -1 + this.myTimer = new MyTimer() // 刷新计时器 + this.stepCount = 0 + this.undoStack = [] + this.notify()//通知更新 + // 刷新笔记矩阵 + this.noteMatrix=[] + for(let i = 0;i < 9;i++){ + this.noteMatrix[i] = [] + for(let j = 0;j < 9;j++){ + this.noteMatrix[i][j] = new Set() + } + } + } + + // 数独重置函数 + public reset() { + this.initState() + } + + // 订阅事件 + public subscribe(callback: () => void) { + this.subscribers.push(callback) + } + + // 通知事件 + public notify() { + console.log("GlobalState notified:", this.sudokuInstance.martix); + for (const subscriber of this.subscribers) { + subscriber() + } + } + + // 记录当前状态用于撤销功能 + public recordState() { + const martix: number[][] = JSON.parse(JSON.stringify(this.sudokuInstance.martix)); // 深拷贝以防止引用问题 + const step: ShowMsg[] = JSON.parse(JSON.stringify(this.showArr))//深拷贝 + const undo: UndoState = { martix, step } + this.undoStack.push(undo) + } + + // 撤回方法 + public undoAction() { + // 如果撤销栈中有多个状态 + if (this.undoStack.length > 1) { + this.undoStack.pop() // 移除当前状态 + const lastState = this.undoStack[this.undoStack.length - 1] // 回到上一步的状态 + this.sudokuInstance.martix = lastState.martix + this.showArr = lastState.step + console.log("Undo action performed, restored state:", lastState) + } + // 如果撤销栈中只有一个状态(初始状态) + else if (this.undoStack.length === 1) { + this.undoStack.pop()// 移除当前状态 + this.sudokuInstance.martix = JSON.parse(JSON.stringify(this.initMatrix))// 恢复到初始状态 + this.showArr = [] + console.log("Undo action performed, restored to initial state") + } + this.undoflag = true + this.notify() + } + + // 添加笔记数字 + addNoteNumber(row:number,col:number,num:number){ + this.noteMatrix[row][col].add(num) + } + + // 清除一个格子的全部笔记 + clearNotes(row:number,col:number){ + this.noteMatrix[row][col].clear() + } + +} + +// 导出全局状态实例 +export const globalState = new GlobalState() \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/ets/common/pictures/icon_block_white.png b/sudukuing/sudoku/entry/src/main/ets/common/pictures/icon_block_white.png new file mode 100644 index 0000000..376365b Binary files /dev/null and b/sudukuing/sudoku/entry/src/main/ets/common/pictures/icon_block_white.png differ diff --git a/sudukuing/sudoku/entry/src/main/ets/common/pictures/icon_block_yellow.png b/sudukuing/sudoku/entry/src/main/ets/common/pictures/icon_block_yellow.png new file mode 100644 index 0000000..d442929 Binary files /dev/null and b/sudukuing/sudoku/entry/src/main/ets/common/pictures/icon_block_yellow.png differ diff --git a/sudukuing/sudoku/entry/src/main/ets/components/Header.ets b/sudukuing/sudoku/entry/src/main/ets/components/Header.ets new file mode 100644 index 0000000..4922ce6 --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/components/Header.ets @@ -0,0 +1,58 @@ +import SetDialog from './SetDialog' +import SolveDialog from './SolveDialog' + +// 头部功能组件 +@Component +export default struct Header { + + // 弹窗控制器 + SetDialogController: CustomDialogController = new CustomDialogController({ + builder: SetDialog(), + gridCount: 4 + }) + + SolveDialogController: CustomDialogController = new CustomDialogController({ + builder: SolveDialog(), + gridCount: 4 + }) + + build() { + Row({space:5}) { + // 游戏设置按钮 + Text('游戏设置') + .fontSize(15) + .fontWeight(FontWeight.Bold) + .margin({left:5,right:5}) + .onClick(()=>{ + this.SetDialogController.open() // 打开设置对话框 + }) + + // 自动求解按钮 + Text('自动求解') + .fontSize(15) + .fontWeight(FontWeight.Bold) + .margin({right:5}) + .onClick(()=>{ + this.SolveDialogController.open() // 打开求解对话框 + }) + + // 空白占位符,用于布局调整 + Blank() + .width('60%') + + // Logo图片 + Image($r('app.media.icon_logo')) + .objectFit(ImageFit.Contain) + .width(20) + + // 标题文本 + Text('数独') + .fontSize(15) + .fontWeight(FontWeight.Bold) + } + .padding({left:5,right:5}) + .justifyContent(FlexAlign.Start) + .width('100%') + .height('100%') + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/ets/components/Main.ets b/sudukuing/sudoku/entry/src/main/ets/components/Main.ets new file mode 100644 index 0000000..76ea900 --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/components/Main.ets @@ -0,0 +1,195 @@ +import { globalState } from '../common/constants/globalState' +import * as CommonConstants from '../common/constants/CommonConstants' +import { drawLine, drawBlockBoard, drawGrayBackground, drawSudoku ,drawNote,fillnoteMatrix,clearnoteMatrix} from '../utils/util' +import { checkWin } from './solvefunc' + +// 定义展示消息接口 +interface ShowMsg { + no: number // 序号 + row: number // 行索引 + col: number // 列索引 + selectNum: number // 选择的数字 +} + +// 棋盘以及数独的绘制逻辑组件 +@Component +export default struct Main { + @Link showArr: ShowMsg[] // 链接父组件的展示数组 + private arrIndex: number = 1 // 数组索引 + @Link showArrItem: ShowMsg // 链接父组件的展示项 + private playX: number = 0 // 玩家点击的X坐标 + private playY: number = 0 // 玩家点击的Y坐标 + public selectedRow: number = -1 // 选中的行索引 + public selectedCol: number = -1 // 选中的列索引 + + // 组件即将出现时的生命周期回调 + aboutToAppear() { + // 订阅全局状态变化事件 + globalState.subscribe(() => { + this.updateCanvas() // 更新画布,使得每次操作数独都能正确展示 + const selectedNumber = globalState.selectedNum; + + // 撤回操作则重置row和num,避免选择位置的影响 + if(globalState.undoflag){ + this.resetslection() + globalState.undoflag=false + this.drawNotes() + } + + // 一键笔记模式 + if(globalState.ifrecord){ + clearnoteMatrix() // 先清空笔记矩阵 + fillnoteMatrix() // 填满笔记,实现一键笔记 + this.drawNotes() + globalState.ifrecord=false + } + + // 判断是否点击数字输入 + if (this.selectedRow !== -1 && this.selectedCol !== -1 && selectedNumber > 0) { + // 判断是否是笔记模式 + if(globalState.ifsave){ + globalState.addNoteNumber(this.selectedRow,this.selectedCol,selectedNumber) // 添加笔记数字 + this.drawNotes() + } + + // 正常输入模式则更新数独展示数字 + if(this.selectedRow !== -1 && this.selectedCol !== -1&&globalState.ifsave===false){ + this.drawNumber(this.selectedRow, this.selectedCol, selectedNumber) // 绘制数字 + globalState.clearNotes(this.selectedRow,this.selectedCol) // 清除该位置的笔记 + this.drawNotes() + } + } + + // 擦除模式 + if (this.selectedRow !== -1 && this.selectedCol !== -1 && selectedNumber === 0 && !globalState.iswork) { + this.eraseNumber(this.selectedRow, this.selectedCol) // 擦除数字 + // 如果该位置有笔记则清除 + if(globalState.noteMatrix[this.selectedRow][this.selectedCol]){ + globalState.clearNotes(this.selectedRow,this.selectedCol) + this.drawNotes() + } + } + + // 提示模式 + if( globalState.ifnote){ + // 获取可能的数字并添加到笔记中 + for(const num of globalState.sudokuInstance.getPossible(this.selectedRow,this.selectedCol)){ + globalState.addNoteNumber(this.selectedRow,this.selectedCol,num) + } + this.drawNotes() + globalState.ifnote = false + } + }) + } + + // 更新画布 + updateCanvas() { + const context2 = CommonConstants.context2 + context2.clearRect(0, 0, context2.width, context2.height) // 清除画布 + drawBlockBoard() // 绘制区块边框 + drawLine() // 绘制线条 + drawSudoku(globalState.sudokuInstance.martix, context2) // 绘制数独数字 + drawGrayBackground(this.selectedRow,this.selectedCol,globalState.sudokuInstance.martix) // 绘制选中格子的灰色背景 + this.updateShowArr() // 更新展示数组 + console.log("Canvas updated with matrix:", globalState.sudokuInstance.martix) + } + + // 重设选中行和列,使得撤回操作不受到选择位置的显示影响 + resetslection(){ + this.selectedRow=-1 + this.selectedCol=-1 + } + + build() { + Column() { + Stack() { + // 绘制棋盘背景 + Canvas(CommonConstants.context) + .width(CommonConstants.miniGridSize * CommonConstants.boardNum + CommonConstants.miniGridSize) + .height(CommonConstants.miniGridSize * CommonConstants.boardNum + CommonConstants.miniGridSize) + .onReady(() => { + drawBlockBoard() // 绘制区块边框 + drawLine() // 绘制线条 + }) + + // 实时绘制数独 + Canvas(CommonConstants.context2) + .width(CommonConstants.miniGridSize * CommonConstants.boardNum + CommonConstants.miniGridSize) + .height(CommonConstants.miniGridSize * CommonConstants.boardNum + CommonConstants.miniGridSize) + .onReady(() => { + this.updateCanvas() + }) + .onTouch((event) => { + // 处理触摸事件 + if (event.type === TouchType.Up) { + this.handleTouch(event.touches[0].x, event.touches[0].y) + } + }) + } + } + .alignItems(HorizontalAlign.Center) + .justifyContent(FlexAlign.Center) + .height('100%') + .width('100%') + } + + // 处理棋盘格子的点击事件 + handleTouch(x: number, y: number) { + // 计算点击位置对应的格子坐标 + this.playX = x - CommonConstants.miniGridSize / 2 + this.playY = y - CommonConstants.miniGridSize / 2 + this.selectedRow = Math.floor(this.playY / CommonConstants.gridSize) + this.selectedCol = Math.floor(this.playX / CommonConstants.gridSize) + + // 确保点击位置在有效范围内 + if (this.selectedRow >= 0 && this.selectedRow < 9 && this.selectedCol >= 0 && this.selectedCol < 9) { + // 只有初始为空的格子才能被编辑 + if (globalState.initMatrix[this.selectedRow][this.selectedCol] === 0) { + drawGrayBackground(this.selectedRow, this.selectedCol, globalState.sudokuInstance.martix) // 绘制选中格子背景 + this.drawNotes() // 绘制笔记 + } + } + } + + // 绘制数字 + drawNumber(row: number, col: number, num: number) { + globalState.sudokuInstance.martix[row][col] = num // 更新数独矩阵 + this.updateCanvas() // 更新画布 + // 添加到展示数组 + const newShowArrItem: ShowMsg = { no: this.arrIndex, row: row, col: col, selectNum: num } + globalState.showArr = globalState.showArr.concat(newShowArrItem) + globalState.recordState() // 记录当前状态用于撤销 + this.updateShowArr() + this.arrIndex++ + globalState.stepCount++; // 增加步骤计数 + checkWin() // 检查是否游戏胜利 + } + + // 擦除数字 + eraseNumber(row: number, col: number) { + globalState.sudokuInstance.martix[row][col] = 0 // 将该格子上的数字重新赋值为0 + this.updateCanvas() // 更新画布,重画数独 + // 从展示数组中移除 + globalState.showArr = globalState.showArr.filter(item => !(item.row === row && item.col === col)) + globalState.recordState() // 记录当前状态用于撤销 + globalState.stepCount++ // 增加步骤计数 + } + + // 更新展示数组 + updateShowArr() { + this.showArr = globalState.showArr.slice() + // 更新数组索引 + this.arrIndex = this.showArr.length > 0 ? this.showArr[this.showArr.length - 1].no + 1 : 1 + } + + // 绘制笔记 + drawNotes(){ + // 遍历所有格子绘制笔记 + for(let row = 0;row<9;row++){ + for(let col=0;col<9;col++){ + const notes = globalState.noteMatrix[row][col] + drawNote(row,col,notes) // 绘制每个格子的笔记 + } + } + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/ets/components/SetDialog.ets b/sudukuing/sudoku/entry/src/main/ets/components/SetDialog.ets new file mode 100644 index 0000000..c904bc6 --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/components/SetDialog.ets @@ -0,0 +1,52 @@ +import { globalState } from '../common/constants/globalState' + +// 设置对话框组件 +@CustomDialog +export default struct SetDialog { + controller: CustomDialogController // 对话框控制器 + + build() { + Column() { + Column({ space: 10 }) { + // 重新开始按钮 + Text('重新开始') + .fontColor(Color.White) + .fontWeight(500) + .fontSize(20) + .onClick(() => { + globalState.initState() // 初始化游戏状态 + globalState.showArr = []; // 清空步骤记录 + globalState.selectedNum = -1; // 重置选中的数字 + globalState.notify(); // 通知状态更新 + console.log("Game reset."); + this.controller.close(); // 关闭对话框 + }) + + // 暂停计时按钮 + Text('暂停计时') + .fontColor(Color.White) + .fontWeight(500) + .fontSize(20) + .onClick(() => { + globalState.myTimer.stop() // 停止计时器 + this.controller.close() // 关闭对话框 + }) + + // 载入题目按钮 + Text('载入题目') + .fontColor(Color.White) + .fontWeight(500) + .fontSize(20) + .onClick(() => { + globalState.reset() // 重置游戏 + globalState.notify() // 通知状态更新 + this.controller.close() // 关闭对话框 + }) + } + } + .backgroundColor('#1E90FF') // 设置背景颜色 + .justifyContent(FlexAlign.Center) + .width(400) + .height(120) + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/ets/components/ShowContent.ets b/sudukuing/sudoku/entry/src/main/ets/components/ShowContent.ets new file mode 100644 index 0000000..43e5afb --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/components/ShowContent.ets @@ -0,0 +1,149 @@ +import { globalState } from '../common/constants/globalState' + +// 定义展示消息接口 +interface ShowMsg { + no: number // 序号 + row: number // 行索引 + col: number // 列索引 + selectNum: number // 选择的数字 +} + +// 内容展示组件 +@Component +export default struct ShowContent { + @Link ifSave: boolean // 链接父组件的笔记模式标志 + @Link showArrItem: ShowMsg // 链接父组件的展示项 + @Link showArr: ShowMsg[] // 链接父组件的展示数 + @State private timerText: string = '' // 计时器文本 + @State private difficultyText: string = '' // 难度文本 + @State private gameOver: boolean = false // 游戏结束标志 + private timerInterval: number | null = null // 计时器ID + + // 组件即将出现时的生命周期回调 + aboutToAppear() { + this.updateData() // 更新数据 + this.startTimer() // 启动计时器 + // 订阅全局状态变化事件 + globalState.subscribe(() => { + this.updateData() + }) + } + + // 组件即将消失时的生命周期回调 + aboutToDisappear() { + // 清除计时器避免内存泄漏 + if (this.timerInterval !== null) { + clearInterval(this.timerInterval) + } + } + + // 启动计时器 + startTimer() { + // 实现时间的每隔一秒更新,实现实时更新显示效果 + if (this.timerInterval === null) { + this.timerInterval = setInterval(() => { + this.updateData() // 每秒更新一次数据 + }, 1000) + } + } + + // 更新数据函数 + updateData() { + // 更新计时器文本 + this.timerText = `运行时间: ${globalState.myTimer.getMinutes()} 分 ${globalState.myTimer.getSeconds()} 秒` + // 更新难度文本 + this.difficultyText = `难度: ${(globalState.emptycell / 81).toFixed(2)}` + // 更新游戏结束标志 + this.gameOver = globalState.GameOver + } + + build() { + Column({ space: 5 }) { + // 游戏没结束,正常绘制内容展示界面 + if (!this.gameOver) { + // 显示运行时间 + Text(this.timerText) + .fontSize(15) + .fontWeight(FontWeight.Bold) + .margin({ right: 5 }) + + // 显示难度等级 + Text(this.difficultyText) + .fontSize(15) + .fontWeight(FontWeight.Bold) + .margin({ right: 5 }) + + // 显示笔记模式状态 + Text('False时保存笔记,当前笔记:' + this.ifSave) + .fontSize(15) + .fontWeight(FontWeight.Bold) + .fontColor(Color.Red) + .margin({ right: 5 }) + + // 显示步骤记录标题 + Text('步骤记录:') + .fontSize(15) + .fontWeight(FontWeight.Bold) + .margin({ right: 5 }) + + // 步骤记录列表 + Scroll() { + List({ space: 2 }) { + ForEach(this.showArr, (item: ShowMsg) => { + ListItem() { + // 显示每个步骤的详细信息 + Text(`[${item.no}, ( ${item.row + 1}, ${item.col + 1}), ${item.selectNum}]`) + .width("100%") + .height("100%") + .fontSize(14) + .textAlign(TextAlign.Start) + .backgroundColor(Color.White) + }.width("100%").height(16) + }) + } + } + .padding({ left: 10, top: 2 }) + .border({ width: 1 }) + .width('100%') + .height('60%') + .backgroundColor(Color.White) + } + // 如果游戏已结束,则绘制结束界面 + else { + Column({ space: 5 }) { + // 游戏胜利提示 + Text('游戏结束,恭喜通过!') + .fontSize(25) + .fontWeight(FontWeight.Bold) + .fontColor(Color.Red) + .margin({ right: 5 ,bottom:10}) + + // 显示最终用时 + Text(this.timerText) + .fontSize(20) + .fontWeight(FontWeight.Bold) + .margin({ right: 5,bottom:10 }) + + // 重新开始按钮 + Button('重新开始') + .fontSize(15) + .fontWeight(FontWeight.Bold) + .backgroundColor(Color.Blue) + .onClick(() => { + globalState.reset() // 重置游戏 + }) + } + .alignItems(HorizontalAlign.Center) + .justifyContent(FlexAlign.Center) + .width('100%') + .height('100%') + .backgroundColor(Color.White) + } + } + .padding({ left: 10, top: 10, right: 10 }) + .alignItems(HorizontalAlign.Start) + .justifyContent(FlexAlign.Start) + .width('100%') + .height('100%') + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/ets/components/SolveDialog.ets b/sudukuing/sudoku/entry/src/main/ets/components/SolveDialog.ets new file mode 100644 index 0000000..86cc52f --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/components/SolveDialog.ets @@ -0,0 +1,86 @@ +// 求解对话框组件 +import { globalState } from '../common/constants/globalState' +import {autosolve,bettersolve} from './solvefunc' + +@CustomDialog +export default struct SolveDialog { + controller: CustomDialogController // 对话框控制器 + @State solveChoose:boolean=false // 求解选项状态 + + build(){ + Column(){ + Column({ space: 10 }) { + // 如果未选择求解速度设置 + if(!this.solveChoose){ + // 顺序求解按钮 + Text('自动求解-顺序求解') + .fontColor(Color.White) + .fontWeight(500) + .fontSize(20) + .onClick(() => { + globalState.iswork = true // 设置自动求解状态 + autosolve() // 调用顺序求解函数 + this.controller.close() // 关闭对话框 + }) + + // 优化求解按钮 + Text('自动求解-优化') + .fontColor(Color.White) + .fontWeight(500) + .fontSize(20) + .onClick(() => { + globalState.iswork = true // 设置自动求解状态 + bettersolve() // 调用优化求解函数 + this.controller.close() // 关闭对话框 + }) + + // 求解速度选择按钮 + Text('求解速度选择') + .fontColor(Color.White) + .fontWeight(500) + .fontSize(20) + .onClick(() => { + this.solveChoose=true // 切换到速度选择界面 + }) + }else{ + // 低速求解选项 + Text('低速') + .fontColor(Color.White) + .fontWeight(500) + .fontSize(20) + .onClick(() => { + globalState.waittime = 1000 // 设置等待时间为1000ms + !this.solveChoose // 切换回主界面 + this.controller.close() // 关闭对话框 + }) + + // 中速求解选项 + Text('中速') + .fontColor(Color.White) + .fontWeight(500) + .fontSize(20) + .onClick(() => { + globalState.waittime = 500 // 设置等待时间为500ms + !this.solveChoose // 切换回主界面 + this.controller.close() // 关闭对话框 + }) + + // 高速求解选项 + Text('高速') + .fontColor(Color.White) + .fontWeight(500) + .fontSize(20) + .onClick(() => { + globalState.waittime = 200 // 设置等待时间为200ms + !this.solveChoose // 切换回主界面 + this.controller.close() // 关闭对话框 + }) + } + } + } + .backgroundColor('#1E90FF') // 设置背景颜色 + .justifyContent(FlexAlign.Center) + .width(400) + .height(120) + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/ets/components/ToolBar.ets b/sudukuing/sudoku/entry/src/main/ets/components/ToolBar.ets new file mode 100644 index 0000000..35368b8 --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/components/ToolBar.ets @@ -0,0 +1,167 @@ +import { globalState } from '../common/constants/globalState' + +// 样式扩展,便于复用 - 工具按钮样式 +@Extend(Text) function toolStyle(){ + .fontSize(16) + .width(60) + .height(30) + .textAlign(TextAlign.Center) + .fontWeight(FontWeight.Bold) + .borderWidth(2) + .backgroundColor(Color.White) +} + +// 样式扩展,便于复用 - 数字按钮样式 +@Extend(Text) function numberStyle(){ + .fontSize(16) + .width(35) + .height(35) + .textAlign(TextAlign.Center) + .fontWeight(FontWeight.Bold) + .borderWidth(2) + .backgroundColor(Color.White) +} + +// 功能栏组件 +@Component +export default struct ToolBar { + @Link ifSave: boolean // 链接父组件的笔记模式标志 + + build() { + Column() { + // 第一行功能按钮 + Row() { + // 撤回按钮 + Text('撤回') + .toolStyle() + .onClick(() => { + // 只有步骤计数大于0时才能撤回 + if(globalState.stepCount>0){ + globalState.undoAction() // 调用撤回函数 + globalState.stepCount-- + } + }) + + // 擦除按钮 + Text('擦除') + .toolStyle() + .onClick(() => { + globalState.selectedNum = 0 // 设置选中数字为0表示擦除 + globalState.notify() // 通知各组件更新 + console.log("Erase button clicked.") + }) + + // 笔记按钮 + Text('笔记') + .toolStyle() + .onClick(() => { + this.ifSave = !this.ifSave // 切换笔记模式标志 + globalState.ifsave = !globalState.ifsave // 同步更新全局状态 + }) + + // 一键笔记按钮 + Text('一键笔记') + .toolStyle() + .fontSize(14) + .onClick(() => { + globalState.ifrecord = true // 设置一键笔记标志 + globalState.notify() // 通知各组件更新 + }) + + // 提示按钮 + Text('提示') + .toolStyle() + .onClick(() => { + globalState.selectedNum=0 // 清除选中数字 + globalState.ifnote = true // 设置提示标志 + globalState.notify() // 通知各组件更新 + }) + } + .justifyContent(FlexAlign.SpaceAround) + .width('100%') + .height('50%') + + // 第二行数字按钮 + Row() { + // 数字1按钮 + Text('1') + .numberStyle() + .onClick(() => { + globalState.selectedNum = 1 // 设置选中数字为1 + globalState.notify() // 通知各订阅组件刷新,从而实现实时更新效果 + }) + + // 数字2按钮 + Text('2') + .numberStyle() + .onClick(() => { + globalState.selectedNum = 2 // 设置选中数字为2 + globalState.notify() + }) + + // 数字3按钮 + Text('3') + .numberStyle() + .onClick(() => { + globalState.selectedNum = 3 // 设置选中数字为3 + globalState.notify() + }) + + // 数字4按钮 + Text('4') + .numberStyle() + .onClick(() => { + globalState.selectedNum = 4 // 设置选中数字为4 + globalState.notify() + }) + + // 数字5按钮 + Text('5') + .numberStyle() + .onClick(() => { + globalState.selectedNum = 5 // 设置选中数字为5 + globalState.notify() + }) + + // 数字6按钮 + Text('6') + .numberStyle() + .onClick(() => { + globalState.selectedNum = 6 // 设置选中数字为6 + globalState.notify() + }) + + // 数字7按钮 + Text('7') + .numberStyle() + .onClick(() => { + globalState.selectedNum = 7 // 设置选中数字为7 + globalState.notify() + }) + + // 数字8按钮 + Text('8') + .numberStyle() + .onClick(() => { + globalState.selectedNum = 8 // 设置选中数字为8 + globalState.notify() + }) + + // 数字9按钮 + Text('9') + .numberStyle() + .onClick(() => { + globalState.selectedNum = 9 // 设置选中数字为9 + globalState.notify() + }) + } + .justifyContent(FlexAlign.SpaceEvenly) + .width('100%') + .height('50%') + } + .justifyContent(FlexAlign.Start) + .width('100%') + .height('100%') + } + +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/ets/components/solvefunc.ets b/sudukuing/sudoku/entry/src/main/ets/components/solvefunc.ets new file mode 100644 index 0000000..65cee9b --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/components/solvefunc.ets @@ -0,0 +1,183 @@ +import {globalState} from '../common/constants/globalState' +import {sleep,drawGrayBackground} from '../utils/util' + +// 定义展示消息接口 +interface ShowMsg { + no: number // 序号 + row: number // 行索引 + col: number // 列索引 + selectNum: number // 选择的数字 +} + +// 异步函数,用于自动解决数独谜题 +export async function autosolve():Promise{ + const STEP:ShowMsg[] = [] // 用于存储解决步骤的数组 + // 重置数独为初始状态 + globalState.sudokuInstance.martix = JSON.parse(JSON.stringify(globalState.initMatrix)) + globalState.selectedNum = 0 // 重置选中数字 + globalState.notify() // 通知状态更新 + const success =await autosolveHelper(globalState.sudokuInstance.martix,STEP) + if(success){ // 如果求解成功 + globalState.showArr = STEP // 更新显示数组为求解步骤 + globalState.notify() + globalState.myTimer.stop() + } +} + +// 更优的自动求解函数 +export async function bettersolve():Promise{ + const STEP:ShowMsg[] = [] // 用于存储解决步骤的数组 + // 重置数独为初始状态 + globalState.sudokuInstance.martix = JSON.parse(JSON.stringify(globalState.initMatrix)) + globalState.selectedNum = 0 // 重置选中数字 + globalState.notify() // 通知状态更新 + globalState.notify() // 再次通知状态更新 + const success = await bettersolveHelper(globalState.sudokuInstance.martix,STEP) + if(success){ // 如果求解成功 + globalState.showArr = STEP // 更新显示数组为求解步骤 + globalState.notify() + globalState.myTimer.stop() + } + +} + +// 辅助函数,使用优化的回溯算法解决数独 +async function bettersolveHelper(martrix:number[][],STEP:ShowMsg[]):Promise{ + let flag = false // 标志位,用于标识是否有唯一解的格子 + + // 第一阶段:处理所有只有一个可能值的格子(唯一候选数法) + for(let row = 0;row < 9;row++){ + for(let col = 0;col < 9; col++){ + if(martrix[row][col] === 0){ // 如果当前格子为空 + // 获取该位置所有可能的数字 + const possible = Array.from(globalState.sudokuInstance.getPossible(row,col)) + // 如果只有一个可能值 + if(possible.length === 1){ + flag = true // 设置标志,表示找到了唯一解的格子 + const value:number = possible[0] // 获取唯一可能值 + martrix[row][col]=value // 填入唯一可能值 + const seq = STEP.length + 1 // 计算当前步骤序号 + // 创建步骤对象 + const step:ShowMsg = {no:seq,row:row,col:col,selectNum:value} + STEP.push(step) // 将步骤对象添加到步骤数组中 + globalState.notify() // 通知状态更新 + // 绘制当前步骤 + await drawstep(row,col,value,martrix,STEP) + await sleep(globalState.waittime) // 等待一段时间,实现动画效果 + // 递归调用,继续求解 + if(await bettersolveHelper(martrix,STEP)){ + return true // 如果找到解决方案,返回true + } + // 回溯,将当前格子重置为空 + martrix[row][col] = 0 + // 绘制回溯步骤 + await drawstep(row,col,0,martrix,STEP) + await sleep(globalState.waittime) // 等待一段时间,实现动画效果 + } + } + } + } + + // 第二阶段:处理所有可能值的格子(回溯法) + if(!flag){ // 如果没有唯一解的格子 + for(let row = 0;row < 9;row++){ + for(let col = 0;col < 9;col++){ + if(martrix[row][col]===0){ // 如果当前格子为空 + // 获取该位置所有可能的数字 + const possible = Array.from(globalState.sudokuInstance.getPossible(row,col)) + // 尝试每个可能值 + for(const value of possible){ + martrix[row][col] = value // 尝试填入每个可能值 + const seq = STEP.length+1 + // 创建步骤对象 + const step:ShowMsg = {no:seq,row:row,col:col,selectNum:value} + STEP.push(step) // 将步骤对象添加到步骤数组中 + // 绘制当前步骤 + await drawstep(row,col,value,martrix,STEP) + await sleep(globalState.waittime) // 等待一段时间,实现动画效果 + // 递归调用,继续求解 + if(await bettersolveHelper(martrix,STEP)){ + return true // 如果找到解决方案,返回true + } + // 回溯,将当前格子重置为空 + martrix[row][col] = 0 + // 绘制回溯步骤 + await drawstep(row,col,0,martrix,STEP) + await sleep(globalState.waittime) // 等待一段时间,实现动画效果 + } + return false // 如果没有找到合适的值,返回false + } + } + } + } + return true // 如果所有格子都填满,返回true,表示求解成功 +} + +// 辅助函数,使用回溯算法解决数独 +async function autosolveHelper(u_M:number[][],STEP:ShowMsg[]):Promise{ + // 遍历数独矩阵 + for(let row = 0;row < 9;row++){ + for(let col = 0; col < 9;col++){ + if(u_M[row][col] === 0){ // 如果当前格子为空 + // 获取该位置所有可能的数字 + const possible = globalState.sudokuInstance.getPossible(row,col) + // 尝试每个可能值 + for(const value of possible){ + u_M[row][col] = value // 尝试填入一个可能值 + const seq = STEP.length+1 // 计算当前步骤序号 + // 创建步骤对象 + const step:ShowMsg = {no:seq,row:row,col:col,selectNum:value} + STEP.push(step) // 将步骤对象添加到步骤数组中 + globalState.notify() // 通知更新状态 + drawGrayBackground(row,col,u_M) // 绘制选中格子背景 + // 绘制当前步骤 + await drawstep(row,col,value,u_M,STEP) + await sleep(globalState.waittime) // 等待一段时间,起到选择速度的效果 + // 递归调用,继续求解 + if(await autosolveHelper(u_M,STEP)){ + return true // 如果找到解决方案,返回true + } + // 回溯,将当前格子重置为空 + u_M[row][col] = 0 + drawGrayBackground(row,col,u_M) // 绘制选中格子背景 + // 绘制回溯步骤 + await drawstep(row,col,0,u_M,STEP) + await sleep(globalState.waittime) // 等待一段时间,实现动画效果 + } + return false // 如果没有找到合适的值,返回false + } + } + } + return true // 如果所有格子都填满,返回true,表示求解成功 +} + +// 绘制步骤函数 +async function drawstep(row:number,col:number,value:number,u_M:number[][],STEP:ShowMsg[]){ + globalState.sudokuInstance.martix = u_M // 更新数独矩阵 + globalState.showArr = STEP // 更新显示数组 + globalState.notify() // 通知状态更新 + drawGrayBackground(row,col,u_M) // 绘制选中格子背景 + checkWin() // 检查是否游戏胜利 +} + +// 检查游戏是否胜利 +export function checkWin(){ + let count = 0 // 计数器,用于统计空格数量 + const matrix = globalState.sudokuInstance.martix // 获取当前数独矩阵 + + // 遍历数独矩阵统计空格数量 + for(let i = 0;i < 9;i++){ + for(let j = 0;j < 9;j++){ + if(matrix[i][j]===0){ // 若存在空格 + count++ + } + } + } + + // 如果没有空格且数独符合规则,则游戏胜利 + if(count===0 && globalState.sudokuInstance.judge()){ + globalState.myTimer.stop() // 若胜利则将计时器暂停 + globalState.showArr = [] // 将展示数据赋空 + globalState.GameOver = true // Gameover标志为true,表示游戏结束 + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/ets/entryability/EntryAbility.ets b/sudukuing/sudoku/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000..1c7aa63 --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,43 @@ +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; +import hilog from '@ohos.hilog'; +import UIAbility from '@ohos.app.ability.UIAbility'; +import Want from '@ohos.app.ability.Want'; +import window from '@ohos.window'; + +export default class EntryAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); + } + + onDestroy(): void { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // Main window is created, set main page for this ability + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + + windowStage.loadContent('pages/Index', (err, data) => { + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); + }); + } + + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // Ability has brought to foreground + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground(): void { + // Ability has back to background + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground'); + } +} diff --git a/sudukuing/sudoku/entry/src/main/ets/pages/Index.ets b/sudukuing/sudoku/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000..35ac9ec --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,93 @@ +// 导入数独类和各个组件 +import SudoKu from "../classes/SudoKu" +import Header from '../components/Header' +import Main from '../components/Main' +import ToolBar from '../components/ToolBar' +import ShowContent from '../components/ShowContent' +import { globalState } from '../common/constants/globalState' + +// 定义展示消息接口 +interface ShowMsg { + no: number // 序号 + row: number // 行索引 + col: number // 列索引 + selectNum: number // 选择的数字 +} + +// 应用入口组件 +@Entry +@Component +struct Index { + // 状态变量定义 + @State sudoku: SudoKu = globalState.sudokuInstance // 全局数独变量 + @State showArr: ShowMsg[] = globalState.showArr // 展示数据的数组 + @State showArrItem: ShowMsg = { no: 1, row: 0, col: 0, selectNum: 0 } // 每次展示的数据 + @State ifSave: boolean = false // 判断是否是笔记模式 + + // 组件即将出现时的生命周期回调 + aboutToAppear() { + this.initState() + // 订阅全局状态变化事件,当状态更新时同步数据 + globalState.subscribe(() => { + this.updateStateFromGlobal() + }) + } + + // 初始化状态 + initState() { + globalState.reset(); // 重置全局状态 + this.updateStateFromGlobal() // 从全局状态更新本地状态 + } + + // 从全局状态更新本地状态 + updateStateFromGlobal() { + this.sudoku = globalState.sudokuInstance + this.showArr = globalState.showArr.slice() // 复制数组避免引用问题 + + // 手动复制对象属性确保数据同步 + this.showArrItem = { + no: globalState.showArrItem.no, + row: globalState.showArrItem.row, + col: globalState.showArrItem.col, + selectNum: globalState.showArrItem.selectNum + }; + } + + // 构建UI界面 + build() { + Column() { + // 头部菜单栏 + Header() + .width('100%') + .height('5%') + .backgroundColor(Color.White) + + Row() { + // 左侧画布区域 + Main({ showArr: this.showArr, showArrItem: this.showArrItem }) + .width('50%') + .height('100%') + + // 右侧内容展示与功能栏区域 + Column() { + // 右侧内容展示区域 + ShowContent({ ifSave: this.ifSave, showArr: this.showArr, showArrItem: this.showArrItem }) + .width('100%') + .height('75%') + + // 功能工具栏 + ToolBar({ ifSave: this.ifSave }) + .width('100%') + .height('25%') + } + .width('50%') + .height('100%') + } + .width('100%') + .height('95%') + } + .backgroundColor("#f5970d") // 设置背景颜色 + .width('100%') + .height('100%') + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/ets/utils/util.ets b/sudukuing/sudoku/entry/src/main/ets/utils/util.ets new file mode 100644 index 0000000..da3072f --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/ets/utils/util.ets @@ -0,0 +1,221 @@ +import * as CommonConstants from '../common/constants/CommonConstants' +import { context2 } from '../common/constants/CommonConstants'; +import { globalState } from '../common/constants/globalState'; + +// 绘制棋盘线条 +export function drawLine() { + // 设置线条样式 + CommonConstants.context2.strokeStyle = "#f5970d"; //线条颜色 + CommonConstants.context2.lineWidth = 1; //线条宽 + + // 绘制横线和竖线 + for (let i = 0; i <= 9; i++) { + // 绘制横线 + CommonConstants.context2.beginPath(); + CommonConstants.context2.moveTo(CommonConstants.miniGridSize / 2, i * CommonConstants.gridSize + CommonConstants.miniGridSize / 2); + CommonConstants.context2.lineTo(CommonConstants.gridSize * 9 + CommonConstants.miniGridSize / 2, i * CommonConstants.gridSize + CommonConstants.miniGridSize / 2); + CommonConstants.context2.stroke(); + + // 绘制竖线 + CommonConstants.context2.beginPath(); + CommonConstants.context2.moveTo(i * CommonConstants.gridSize + CommonConstants.miniGridSize / 2, CommonConstants.miniGridSize / 2); + CommonConstants.context2.lineTo(i * CommonConstants.gridSize + CommonConstants.miniGridSize / 2, CommonConstants.gridSize * 9 + CommonConstants.miniGridSize / 2); + CommonConstants.context2.stroke(); + } +} + +// 绘制白色方块 +export function drawWhiteBlock(row:number, col:number){ + const X = col * CommonConstants.gridSize+CommonConstants.miniGridSize/2 + const Y = row * CommonConstants.gridSize+CommonConstants.miniGridSize/2 + CommonConstants.context.drawImage(CommonConstants.imgWhiteBlock, X, Y,CommonConstants.gridSize,CommonConstants.gridSize); +} + +// 绘制橙色方块 +export function drawOrangeBlock(row:number, col:number){ + const X = col * CommonConstants.gridSize+CommonConstants.miniGridSize/2 + const Y = row * CommonConstants.gridSize+CommonConstants.miniGridSize/2 + CommonConstants.context.drawImage(CommonConstants.imgWhiteBlock, X, Y,CommonConstants.gridSize,CommonConstants.gridSize); +} + +// 绘制区块棋盘背景 +export function drawBlockBoard() { + // 设置线条样式 + CommonConstants.context.strokeStyle = '#00aacc'; //线条颜色 + CommonConstants.context.lineWidth = 1; //线条宽 + + // 绘制9x9的棋盘格子,不同区域使用不同颜色背景 + for (let i = 0; i <9; i++) { + for(let j=0;j<9;j++){ + let X = i * CommonConstants.gridSize+CommonConstants.miniGridSize/2 + let Y = j * CommonConstants.gridSize+CommonConstants.miniGridSize/2 + + // 为特定区域设置黄色背景 + if(i>=0&&i<=2&&j>=3&&j<=5){ + CommonConstants.context.drawImage(CommonConstants.imgYellowBlock, X, Y,CommonConstants.gridSize,CommonConstants.gridSize); + }else if(i>=3&&i<=5&&j>=0&&j<=2){ + CommonConstants.context.drawImage(CommonConstants.imgYellowBlock, X, Y,CommonConstants.gridSize,CommonConstants.gridSize); + }else if(i>=3&&i<=5&&j>=6&&j<=8){ + CommonConstants.context.drawImage(CommonConstants.imgYellowBlock, X, Y,CommonConstants.gridSize,CommonConstants.gridSize); + }else if(i>=6&&i<=8&&j>=3&&j<=5){ + CommonConstants.context.drawImage(CommonConstants.imgYellowBlock, X, Y,CommonConstants.gridSize,CommonConstants.gridSize); + }else{ + // 其他区域使用白色背景 + CommonConstants.context.drawImage(CommonConstants.imgWhiteBlock, X, Y,CommonConstants.gridSize,CommonConstants.gridSize); + } + + } + } +} + +// 记录上一次点击的行列位置 +let previousRow: number | null = null +let previousCol: number | null = null + +// 绘制选中格子所在行列的灰色背景 +export function drawGrayBackground(row: number, col: number, matrix: number[][]) { + const context = CommonConstants.context2 + const gridSize = CommonConstants.gridSize + + // 恢复前一个点击的格子所在行列的背景颜色 + if (previousRow !== null && previousCol !== null) { + // 清除之前选中行和列的背景 + for (let i = 0; i < 9; i++) { + context.clearRect(i * gridSize + CommonConstants.miniGridSize / 2, + previousRow * gridSize + CommonConstants.miniGridSize / 2, gridSize, gridSize) + context.clearRect(previousCol * gridSize + CommonConstants.miniGridSize / 2, + i * gridSize + CommonConstants.miniGridSize / 2, gridSize, gridSize) + } + } + + // 绘制当前点击的格子所在行和列的背景颜色为灰色 + context.fillStyle = '#ffd4cfcf' + for (let i = 0; i < 9; i++) { + // 绘制选中行的背景 + context.fillRect(i * gridSize + CommonConstants.miniGridSize / 2, row * gridSize + CommonConstants.miniGridSize / 2, + gridSize, gridSize) + // 绘制选中列的背景 + context.fillRect(col * gridSize + CommonConstants.miniGridSize / 2, i * gridSize + CommonConstants.miniGridSize / 2, + gridSize, gridSize) + } + + // 重新绘制数字 + drawSudoku(matrix,context2) // 确保调用 `drawSudoku` 函数来重新绘制数字 + drawLine() // 绘制线条 + previousRow = row + previousCol = col +} + +// 绘制数独数字 +export function drawSudoku(matrix: number[][], context: CanvasRenderingContext2D) { + const gridSize = CommonConstants.gridSize + const fontSize = gridSize * 1.5 + context.font = `${fontSize}px Arial` + context.textAlign = 'center' + context.textBaseline = 'middle' + + // 遍历数独矩阵绘制数字 + for (let i = 0; i < 9; i++) { + for (let j = 0; j < 9; j++) { + const number = matrix[i][j] + if (number !== 0) { + let color = 'black' // 默认黑色填入(初始数字) + // 如果是玩家填入的数字 + if (globalState.initMatrix[i][j] === 0) { + // 检查数字是否符合数独规则 + const isValid = checkValidity(matrix, i, j, number) + // 根据是否符合规则选择颜色:蓝色表示有效,红色表示无效 + color = isValid ? 'blue' : 'red' + } + context.fillStyle = color + // 在格子中心绘制数字 + context.fillText(number.toString(), j * gridSize + gridSize / 2, i * gridSize + gridSize / 2) + } + } + } +} + +// 检查数字在数独中的有效性 +function checkValidity(matrix: number[][], row: number, col: number, num: number): boolean { + // 检查行是否有重复数字 + for (let j = 0; j < 9; j++) { + if (j !== col && matrix[row][j] === num) { + return false + } + } + // 检查列是否有重复数字 + for (let i = 0; i < 9; i++) { + if (i !== row && matrix[i][col] === num) { + return false + } + } + // 检查3x3宫格是否有重复数字 + const startRow = Math.floor(row / 3) * 3 + const startCol = Math.floor(col / 3) * 3 + for (let i = startRow; i < startRow + 3; i++) { + for (let j = startCol; j < startCol + 3; j++) { + if ((i !== row || j !== col) && matrix[i][j] === num) { + return false + } + } + } + return true // 所有检查通过,数字有效 +} + +// 绘制笔记数字 +export function drawNote(row: number, col: number, note:Set) { + // 计算小格子的大小和位置偏移 + const miniCellSize = CommonConstants.gridSize / 3; + const xOffset = col * CommonConstants.gridSize + CommonConstants.miniGridSize / 2; + const yOffset = row * CommonConstants.gridSize + CommonConstants.miniGridSize / 2; + + // 设置笔记数字的字体样式 + context2.font = '35px Arial'; + context2.textAlign = 'center'; + context2.textBaseline = 'middle'; + context2.fillStyle = 'blue'; + + // 遍历笔记数字集合,绘制每个数字 + for(const num of note){ + // 计算数字在格子中的位置 + const x = xOffset + ((num - 1) % 3) * miniCellSize + const y = yOffset + Math.floor((num - 1) / 3) * miniCellSize + // 在指定位置绘制数字 + context2.fillText(num.toString(), x + miniCellSize / 2, y + miniCellSize / 2) + } +} + +// 填充笔记矩阵 +export function fillnoteMatrix(){ + // 遍历所有格子 + for(let row=0;row<9;row++){ + for(let col=0;col<9;col++){ + // 如果格子为空 + if(globalState.sudokuInstance.martix[row][col]===0){ + // 获取该位置所有可能的数字并添加到笔记中 + for(const num of globalState.sudokuInstance.getPossible(row,col)){ + globalState.addNoteNumber(row,col,num) + } + } + } + } +} + +// 清除笔记矩阵 +export function clearnoteMatrix(){ + // 遍历所有格子 + for(let row=0;row<9;row++){ + for(let col=0;col<9;col++){ + // 如果是初始为空的格子 + if(globalState.initMatrix[row][col]===0){ + // 清除该位置的笔记 + globalState.clearNotes(row,col) + } + } + } +} + +// 异步函数sleep,用于休眠 +export async function sleep(ms:number):Promise { + return new Promise(resolve => setTimeout(resolve,ms)) +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/module.json5 b/sudukuing/sudoku/entry/src/main/module.json5 new file mode 100644 index 0000000..f2ff2c7 --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/module.json5 @@ -0,0 +1,37 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "default", + "tablet" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:icon", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/resources/base/element/color.json b/sudukuing/sudoku/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000..3c71296 --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/resources/base/element/string.json b/sudukuing/sudoku/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000..f945955 --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "label" + } + ] +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/resources/base/media/icon.png b/sudukuing/sudoku/entry/src/main/resources/base/media/icon.png new file mode 100644 index 0000000..cd45acc Binary files /dev/null and b/sudukuing/sudoku/entry/src/main/resources/base/media/icon.png differ diff --git a/sudukuing/sudoku/entry/src/main/resources/base/media/icon_logo.png b/sudukuing/sudoku/entry/src/main/resources/base/media/icon_logo.png new file mode 100644 index 0000000..468ee55 Binary files /dev/null and b/sudukuing/sudoku/entry/src/main/resources/base/media/icon_logo.png differ diff --git a/sudukuing/sudoku/entry/src/main/resources/base/media/icon_logo2.png b/sudukuing/sudoku/entry/src/main/resources/base/media/icon_logo2.png new file mode 100644 index 0000000..28a3d49 Binary files /dev/null and b/sudukuing/sudoku/entry/src/main/resources/base/media/icon_logo2.png differ diff --git a/sudukuing/sudoku/entry/src/main/resources/base/media/icon_logo3.png b/sudukuing/sudoku/entry/src/main/resources/base/media/icon_logo3.png new file mode 100644 index 0000000..fe3db55 Binary files /dev/null and b/sudukuing/sudoku/entry/src/main/resources/base/media/icon_logo3.png differ diff --git a/sudukuing/sudoku/entry/src/main/resources/base/media/startIcon.png b/sudukuing/sudoku/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000..366f764 Binary files /dev/null and b/sudukuing/sudoku/entry/src/main/resources/base/media/startIcon.png differ diff --git a/sudukuing/sudoku/entry/src/main/resources/base/profile/main_pages.json b/sudukuing/sudoku/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000..1898d94 --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/sudukuing/sudoku/entry/src/main/resources/en_US/element/string.json b/sudukuing/sudoku/entry/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000..f945955 --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "label" + } + ] +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/main/resources/zh_CN/element/string.json b/sudukuing/sudoku/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000..597ecf9 --- /dev/null +++ b/sudukuing/sudoku/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "模块描述" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "label" + } + ] +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/ohosTest/ets/test/Ability.test.ets b/sudukuing/sudoku/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000..8aa3749 --- /dev/null +++ b/sudukuing/sudoku/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,35 @@ +import hilog from '@ohos.hilog'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }) + }) +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/ohosTest/ets/test/List.test.ets b/sudukuing/sudoku/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000..794c7dc --- /dev/null +++ b/sudukuing/sudoku/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,5 @@ +import abilityTest from './Ability.test'; + +export default function testsuite() { + abilityTest(); +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/ohosTest/ets/testability/TestAbility.ets b/sudukuing/sudoku/entry/src/ohosTest/ets/testability/TestAbility.ets new file mode 100644 index 0000000..9484761 --- /dev/null +++ b/sudukuing/sudoku/entry/src/ohosTest/ets/testability/TestAbility.ets @@ -0,0 +1,50 @@ +import UIAbility from '@ohos.app.ability.UIAbility'; +import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; +import hilog from '@ohos.hilog'; +import { Hypium } from '@ohos/hypium'; +import testsuite from '../test/List.test'; +import window from '@ohos.window'; +import Want from '@ohos.app.ability.Want'; +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; + +export default class TestAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onCreate'); + hilog.info(0x0000, 'testTag', '%{public}s', 'want param:' + JSON.stringify(want) ?? ''); + hilog.info(0x0000, 'testTag', '%{public}s', 'launchParam:' + JSON.stringify(launchParam) ?? ''); + let abilityDelegator: AbilityDelegatorRegistry.AbilityDelegator; + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + let abilityDelegatorArguments: AbilityDelegatorRegistry.AbilityDelegatorArgs; + abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments(); + hilog.info(0x0000, 'testTag', '%{public}s', 'start run testcase!!!'); + Hypium.hypiumTest(abilityDelegator, abilityDelegatorArguments, testsuite); + } + + onDestroy() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage) { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageCreate'); + windowStage.loadContent('testability/pages/Index', (err, data) => { + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', + JSON.stringify(data) ?? ''); + }); + } + + onWindowStageDestroy() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageDestroy'); + } + + onForeground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onForeground'); + } + + onBackground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onBackground'); + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/ohosTest/ets/testability/pages/Index.ets b/sudukuing/sudoku/entry/src/ohosTest/ets/testability/pages/Index.ets new file mode 100644 index 0000000..423b427 --- /dev/null +++ b/sudukuing/sudoku/entry/src/ohosTest/ets/testability/pages/Index.ets @@ -0,0 +1,17 @@ +@Entry +@Component +struct Index { + @State message: string = 'Hello World'; + + build() { + Row() { + Column() { + Text(this.message) + .fontSize(50) + .fontWeight(FontWeight.Bold) + } + .width('100%') + } + .height('100%') + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ets b/sudukuing/sudoku/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ets new file mode 100644 index 0000000..917d27a --- /dev/null +++ b/sudukuing/sudoku/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ets @@ -0,0 +1,47 @@ +import hilog from '@ohos.hilog'; +import TestRunner from '@ohos.application.testRunner'; +import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; +import Want from '@ohos.app.ability.Want'; + +let abilityDelegator: AbilityDelegatorRegistry.AbilityDelegator | undefined = undefined +let abilityDelegatorArguments: AbilityDelegatorRegistry.AbilityDelegatorArgs | undefined = undefined + +async function onAbilityCreateCallback() { + hilog.info(0x0000, 'testTag', '%{public}s', 'onAbilityCreateCallback'); +} + +async function addAbilityMonitorCallback(err : Error) { + hilog.info(0x0000, 'testTag', 'addAbilityMonitorCallback : %{public}s', JSON.stringify(err) ?? ''); +} + +export default class OpenHarmonyTestRunner implements TestRunner { + constructor() { + } + + onPrepare() { + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner OnPrepare '); + } + + async onRun() { + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun run'); + abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() + const bundleName = abilityDelegatorArguments.bundleName; + const testAbilityName = 'TestAbility'; + let lMonitor: AbilityDelegatorRegistry.AbilityMonitor = { + abilityName: testAbilityName, + onAbilityCreate: onAbilityCreateCallback, + }; + abilityDelegator.addAbilityMonitor(lMonitor, addAbilityMonitorCallback) + const want: Want = { + bundleName: bundleName, + abilityName: testAbilityName + }; + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + abilityDelegator.startAbility(want, (err, data) => { + hilog.info(0x0000, 'testTag', 'startAbility : err : %{public}s', JSON.stringify(err) ?? ''); + hilog.info(0x0000, 'testTag', 'startAbility : data : %{public}s',JSON.stringify(data) ?? ''); + }) + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun end'); + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/ohosTest/module.json5 b/sudukuing/sudoku/entry/src/ohosTest/module.json5 new file mode 100644 index 0000000..4fc9701 --- /dev/null +++ b/sudukuing/sudoku/entry/src/ohosTest/module.json5 @@ -0,0 +1,37 @@ +{ + "module": { + "name": "entry_test", + "type": "feature", + "description": "$string:module_test_desc", + "mainElement": "TestAbility", + "deviceTypes": [ + "default", + "tablet" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:test_pages", + "abilities": [ + { + "name": "TestAbility", + "srcEntry": "./ets/testability/TestAbility.ets", + "description": "$string:TestAbility_desc", + "icon": "$media:icon", + "label": "$string:TestAbility_label", + "exported": true, + "startWindowIcon": "$media:icon", + "startWindowBackground": "$color:start_window_background", + "skills": [ + { + "actions": [ + "action.system.home" + ], + "entities": [ + "entity.system.home" + ] + } + ] + } + ] + } +} diff --git a/sudukuing/sudoku/entry/src/ohosTest/resources/base/element/color.json b/sudukuing/sudoku/entry/src/ohosTest/resources/base/element/color.json new file mode 100644 index 0000000..3c71296 --- /dev/null +++ b/sudukuing/sudoku/entry/src/ohosTest/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/ohosTest/resources/base/element/string.json b/sudukuing/sudoku/entry/src/ohosTest/resources/base/element/string.json new file mode 100644 index 0000000..65d8fa5 --- /dev/null +++ b/sudukuing/sudoku/entry/src/ohosTest/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_test_desc", + "value": "test ability description" + }, + { + "name": "TestAbility_desc", + "value": "the test ability" + }, + { + "name": "TestAbility_label", + "value": "test label" + } + ] +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/ohosTest/resources/base/media/icon.png b/sudukuing/sudoku/entry/src/ohosTest/resources/base/media/icon.png new file mode 100644 index 0000000..ce307a8 Binary files /dev/null and b/sudukuing/sudoku/entry/src/ohosTest/resources/base/media/icon.png differ diff --git a/sudukuing/sudoku/entry/src/ohosTest/resources/base/profile/test_pages.json b/sudukuing/sudoku/entry/src/ohosTest/resources/base/profile/test_pages.json new file mode 100644 index 0000000..b7e7343 --- /dev/null +++ b/sudukuing/sudoku/entry/src/ohosTest/resources/base/profile/test_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "testability/pages/Index" + ] +} diff --git a/sudukuing/sudoku/entry/src/test/List.test.ets b/sudukuing/sudoku/entry/src/test/List.test.ets new file mode 100644 index 0000000..bb5b5c3 --- /dev/null +++ b/sudukuing/sudoku/entry/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/sudukuing/sudoku/entry/src/test/LocalUnit.test.ets b/sudukuing/sudoku/entry/src/test/LocalUnit.test.ets new file mode 100644 index 0000000..ed22d4d --- /dev/null +++ b/sudukuing/sudoku/entry/src/test/LocalUnit.test.ets @@ -0,0 +1,33 @@ +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function localUnitTest() { + describe('localUnitTest',() => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }); + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }); + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }); + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }); + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }); + }); +} \ No newline at end of file diff --git a/sudukuing/sudoku/hvigor/hvigor-config.json5 b/sudukuing/sudoku/hvigor/hvigor-config.json5 new file mode 100644 index 0000000..61c774a --- /dev/null +++ b/sudukuing/sudoku/hvigor/hvigor-config.json5 @@ -0,0 +1,17 @@ +{ + "modelVersion": "5.1.1", + "dependencies": { + }, + "execution": { + // "daemon": true, /* Enable daemon compilation. Default: true */ + // "incremental": true, /* Enable incremental compilation. Default: true */ + // "parallel": true, /* Enable parallel compilation. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Default: false */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Default: false */ + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/hvigorfile.ts b/sudukuing/sudoku/hvigorfile.ts new file mode 100644 index 0000000..f3cb9f1 --- /dev/null +++ b/sudukuing/sudoku/hvigorfile.ts @@ -0,0 +1,6 @@ +import { appTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/sudukuing/sudoku/oh-package-lock.json5 b/sudukuing/sudoku/oh-package-lock.json5 new file mode 100644 index 0000000..7487ffe --- /dev/null +++ b/sudukuing/sudoku/oh-package-lock.json5 @@ -0,0 +1,21 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos/hypium@1.0.11": "@ohos/hypium@1.0.11" + }, + "packages": { + "@ohos/hypium@1.0.11": { + "name": "", + "version": "1.0.11", + "integrity": "sha512-KawcLnv43C3QIYv1UbDnKCFX3MohtDxGuFvzlUxT/qf2DBilR56Ws6zrj90LdH6PjloJQwOPESuBQIHBACAK7w==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.11.tgz", + "shasum": "fa799d273fa7d921701578c5e7084849354a4af0", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/sudukuing/sudoku/oh-package.json5 b/sudukuing/sudoku/oh-package.json5 new file mode 100644 index 0000000..8fae540 --- /dev/null +++ b/sudukuing/sudoku/oh-package.json5 @@ -0,0 +1,13 @@ +{ + "modelVersion": "5.1.1", + "license": "", + "devDependencies": { + "@ohos/hypium": "1.0.11" + }, + "author": "", + "name": "sudoku", + "description": "Please describe the basic information.", + "main": "", + "version": "1.0.0", + "dependencies": {} +} \ No newline at end of file