sudokuing

This commit is contained in:
LeeJu 2026-05-22 16:22:03 +08:00
parent 0637373af2
commit 196bb3307f
57 changed files with 2223 additions and 0 deletions

21
sudukuing/README.md Normal file
View File

@ -0,0 +1,21 @@
# pointgaming
### 1、项目说明
总体思路扫雷程序项目开发的总体思路如图1.1示意,拟实现交互界面与交互控制、数据结构和扫雷算法等。
图1.1 项目开发的总体思路图
<img src="figs/overall.png" width=80% /></center>
### 2、开发环境说明
1系统版本OpenHarmony 4.0 Release
2SDK版本API 10
3Model类型Stage
4DevEco Studio版本Deveco Studio 4.0 release
5硬件环境Unionpi Whale、RK3566开发板
6预览器参数(1)DeviceType:default;(2)Resolution:560-1280;(3)DPI:240;其余参数自定。
### 3、项目主要函数调用关系图与项目输出界面演进图
图3.1 项目主要函数调用关系图
<img src="figs/relation.png" width=80% /></center>
图3.2 项目输出界面演进图
<img src="figs/evolution.png" width=80% /></center>

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

BIN
sudukuing/figs/overall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

BIN
sudukuing/figs/relation.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

11
sudukuing/sudoku/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
/node_modules
/oh_modules
/local.properties
/.idea
**/build
/.hvigor
.cxx
/.clangd
/.clang-format
/.clang-tidy
**/.test

View File

@ -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"
}
}

View File

@ -0,0 +1,8 @@
{
"string": [
{
"name": "app_name",
"value": "Sudoku"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -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"
]
}
]
}
]
}

6
sudukuing/sudoku/entry/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/node_modules
/oh_modules
/.preview
/build
/.cxx
/.test

View File

@ -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",
}
]
}

View File

@ -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. */
}

View File

@ -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

View File

@ -0,0 +1,10 @@
{
"license": "",
"devDependencies": {},
"author": "",
"name": "entry",
"description": "Please describe the basic information.",
"main": "",
"version": "1.0.0",
"dependencies": {}
}

View File

@ -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;
}
}
}

View File

@ -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;i<m*3+3;i++){
subBlock.push(this.martix[i].slice(n*3,n*3+3)) // 切片每一行的指定列范围
}
let block_martix=subBlock.flat()// 使用flat()方法将二维数组转换为一维数组
let block_set=block_martix.filter(num=>num!=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<number>{
let Nums:Set<number>=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<number>=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<m*3+3;i++) {
for (let j = n*3; j < n*3+3; j++) {
if(row==i&&j==col){
continue // 跳过当前位置
}
set.add(this.martix[i][j])//将所在九宫格出现数字加入集合
}
}
// 返回1-9的集合与出现数字集合的差集即可填数字
return this.setDifference(Nums,set)
}
// 集合差集运算
setDifference(setA:Set<number>, setB:Set<number>) {
// 创建一个新的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
}
}

View File

@ -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");//黄色方块背景

View File

@ -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<number>[][] // 存储每个位置的笔记数字
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<number>()
}
}
}
// 初始化状态
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<number>()
}
}
}
// 数独重置函数
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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -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%')
}
}

View File

@ -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) // 绘制每个格子的笔记
}
}
}
}

View File

@ -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)
}
}

View File

@ -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%')
}
}

View File

@ -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)
}
}

View File

@ -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%')
}
}

View File

@ -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<void>{
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<void>{
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<boolean>{
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<boolean>{
// 遍历数独矩阵
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表示游戏结束
}
}

View File

@ -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');
}
}

View File

@ -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%')
}
}

View File

@ -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<number>) {
// 计算小格子的大小和位置偏移
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<void> {
return new Promise(resolve => setTimeout(resolve,ms))
}

View File

@ -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"
]
}
]
}
]
}
}

View File

@ -0,0 +1,8 @@
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
}
]
}

View File

@ -0,0 +1,16 @@
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,5 @@
{
"src": [
"pages/Index"
]
}

View File

@ -0,0 +1,16 @@
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
}
]
}

View File

@ -0,0 +1,16 @@
{
"string": [
{
"name": "module_desc",
"value": "模块描述"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
}
]
}

View File

@ -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);
})
})
}

View File

@ -0,0 +1,5 @@
import abilityTest from './Ability.test';
export default function testsuite() {
abilityTest();
}

View File

@ -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');
}
}

View File

@ -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%')
}
}

View File

@ -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');
}
}

View File

@ -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"
]
}
]
}
]
}
}

View File

@ -0,0 +1,8 @@
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
}
]
}

View File

@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -0,0 +1,5 @@
{
"src": [
"testability/pages/Index"
]
}

View File

@ -0,0 +1,5 @@
import localUnitTest from './LocalUnit.test';
export default function testsuite() {
localUnitTest();
}

View File

@ -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);
});
});
}

View File

@ -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 */
}
}

View File

@ -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. */
}

View File

@ -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"
}
}
}

View File

@ -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": {}
}