This repository has been archived by the owner on Jun 23, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
17a2703
commit 4d7c265
Showing
8 changed files
with
358 additions
and
278 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,69 +1,9 @@ | ||
from tkinter.filedialog import askopenfilename | ||
from tkinter.messagebox import askyesno, showerror, showinfo | ||
from tkinter.simpledialog import askinteger | ||
|
||
from numpy import array | ||
from numpy.dtypes import Int8DType | ||
from numpy.random import randint | ||
|
||
from .entities import Nonogram # type: ignore[import-untyped] | ||
from .utils import Puzzle, generate_clues # type: ignore[import-untyped] | ||
from .interface.utils import GameUI | ||
|
||
__version__ = "0.3.0" | ||
__all__ = ["main"] | ||
|
||
|
||
def generate_puzzle() -> Puzzle: | ||
""" | ||
随机生成数织题解 | ||
【注意】此种随机生成无法保证题目存在唯一逻辑解,甚至可能不存在逻辑解(逻辑求解算法无解) | ||
Returns: | ||
Puzzle: 随机 0-1 二维 NumPy 矩阵 | ||
""" | ||
if (level := askinteger("Input", "请输入难度级别:")) and level > 0: | ||
return randint(0, 2, (level, level)) | ||
showinfo("Tips", "已默认设置为 10×10 难度!") | ||
return randint(0, 2, (10, 10)) | ||
|
||
|
||
def load_file() -> Puzzle: | ||
""" | ||
加载题解,根据题解还原谜题 | ||
Returns: | ||
Puzzle: 题解二维 NumPy 矩阵 | ||
""" | ||
if filename := askopenfilename(filetypes=[("Text files", "*.txt")]): | ||
with open(filename, encoding="UTF-8") as file: | ||
data = file.readlines() | ||
return array([[int(col) for col in row.strip()] for row in data], dtype=Int8DType) | ||
return generate_puzzle() | ||
|
||
|
||
def pick_puzzle_type() -> Puzzle: | ||
""" | ||
选择数织谜题来源,根据来源获取题解矩阵 | ||
Returns: | ||
Puzzle: 二维 NumPy 矩阵 | ||
""" | ||
return load_file() if askyesno("题目模式", "是否从题解文件加载生成题目?") else generate_puzzle() | ||
|
||
|
||
def ask_cell_size() -> int: | ||
size = askinteger("Input", "请输入单个方块的边长(px):") | ||
return size if (size and size > 0) else 25 | ||
|
||
|
||
def main() -> None: | ||
"""应用程序入口""" | ||
answer = pick_puzzle_type() | ||
if answer.shape[0] != answer.shape[1]: | ||
showerror("Error", "题解数据不合理或不是方阵!") | ||
showerror("Exit", "因意外情况而退出!") | ||
exit(-1) | ||
clues = generate_clues(answer) | ||
cell_size = ask_cell_size() | ||
Nonogram(clues, answer, cell_size) | ||
GameUI() |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
from collections import deque | ||
from typing import Sequence | ||
|
||
from numpy import array, ndarray, zeros | ||
from numpy.dtypes import Int8DType | ||
from numpy.random import randint | ||
|
||
from ..utils import Activity | ||
|
||
MatrixLike = Sequence[Sequence[int]] | ||
Puzzle = ndarray[tuple[int, int], Int8DType] | ||
Row = ndarray[tuple[int], Int8DType] | ||
LogStack = deque[Activity] | ||
|
||
|
||
class GameCore: | ||
""" | ||
游戏内核 | ||
Properties: | ||
answer (Puzzle): 题解 | ||
clues (Clues): 线索 | ||
grid (Puzzle): 当前游戏面板 | ||
undo_log (LogStack): 撤回操作栈 | ||
redo_log (LogStack): 重做操作栈 | ||
""" | ||
|
||
class Clues: | ||
"""线索""" | ||
|
||
def __init__( | ||
self, rows: Sequence[tuple[int]] | None = None, columns: Sequence[tuple[int]] | None = None | ||
) -> None: | ||
self.rows: Sequence[tuple[int]] = rows or [] | ||
self.columns: Sequence[tuple[int]] = columns or [] | ||
|
||
def __eq__(self, value: object) -> bool: | ||
return ( | ||
(self.rows == value.rows and self.columns == value.columns) | ||
if isinstance(value, GameCore.Clues) | ||
else False | ||
) | ||
|
||
def __repr__(self) -> str: | ||
return f"Clues(rows={self.rows}, columns={self.columns})" | ||
|
||
def __init__(self, level: int = 10) -> None: | ||
self.grid: Puzzle = zeros((level, level), dtype=Int8DType) | ||
self.undo_log: deque[Activity] = deque() | ||
self.redo_log: deque[Activity] = deque() | ||
|
||
def count_continues(self, _row: Row) -> tuple[int]: | ||
""" | ||
统计 NumPy 矩阵单行/列中连续 1 的数量 | ||
Args: | ||
_array (Row): NumPy 二维矩阵 | ||
Returns: | ||
tuple[int]: 数量列表,用作解题线索 | ||
""" | ||
from numpy import append, diff, insert, where | ||
|
||
# 参考文献: [计算连续出现次数](https://geek-docs.com/numpy/numpy-ask-answer/231_numpy_count_consecutive_occurences_of_values_varying_in_length_in_a_numpy_array.html#ftoc-heading-2) ## noqa: B950 | ||
# 找到数组中所有的1 | ||
ones = where(_row == 1)[0] | ||
# 计算连续1的起始索引和结束索引 | ||
diff = diff(ones) # type: ignore[assignment] | ||
starts = insert(where(diff != 1)[0] + 1, 0, 0) | ||
ends = append(where(diff != 1)[0], len(ones) - 1) | ||
# 计算连续1的长度 | ||
lengths = ends - starts + 1 | ||
|
||
return tuple(lengths.tolist()) | ||
|
||
def generate_clues(self, _matrix: Puzzle | MatrixLike | None = None) -> Clues: | ||
""" | ||
生成线索(谜面) | ||
Args: | ||
_matrix (Puzzle | MatrixLike): 游戏板状态二维矩阵 | ||
Returns: | ||
Clues: 线索字典 | ||
""" | ||
from numpy import array | ||
|
||
if _matrix is None: | ||
_matrix = self.answer | ||
elif not isinstance(_matrix, ndarray): | ||
# 当传入参数不是 NumPy 矩阵时,转换为矩阵 | ||
_matrix = array(_matrix) | ||
# 提取矩阵规模 | ||
rows, cols = _matrix.shape | ||
if rows <= 0 or cols <= 0: | ||
# 当矩阵某一个维度为 0 ,不可能存在谜面 | ||
return self.Clues() | ||
row_clues = [self.count_continues(_matrix[_, :]) for _ in range(rows)] | ||
col_clues = [self.count_continues(_matrix[:, _]) for _ in range(cols)] | ||
self.clues = self.Clues(row_clues, col_clues) | ||
return self.clues | ||
|
||
def generate_puzzle(self, level: int = 10) -> None: | ||
""" | ||
随机生成数织题解 | ||
【注意】此种随机生成无法保证题目存在唯一逻辑解,甚至可能不存在逻辑解(逻辑求解算法无解) | ||
Returns: | ||
Puzzle: 随机 0-1 二维 NumPy 矩阵 | ||
""" | ||
self.answer = randint(0, 2, (level, level)) | ||
|
||
def load_file(self, filename: str | None) -> bool: | ||
""" | ||
加载题解,根据题解还原谜题 | ||
Returns: | ||
(bool): 文件是否解析成功? | ||
""" | ||
if filename: | ||
with open(filename, encoding="UTF-8") as file: | ||
data = file.readlines() | ||
try: | ||
self.answer = array([[int(col) for col in row.strip()] for row in data], dtype=Int8DType) | ||
return True | ||
except Exception: | ||
pass | ||
return False | ||
|
||
def toggle_cell(self, activity: Activity) -> None: | ||
""" | ||
切换单元格状态 | ||
Args: | ||
activity (Activity): 用户操作 | ||
""" | ||
if activity.is_context: | ||
self.grid[activity.y, activity.x] = 0 if self.grid[activity.y, activity.x] == 2 else 2 | ||
else: | ||
self.grid[activity.y, activity.x] = 0 if self.grid[activity.y, activity.x] == 1 else 1 | ||
|
||
def undo(self) -> Activity | None: | ||
""" | ||
撤销至上一步 | ||
Returns: | ||
(Activity | None): 能够撤销时,返回本次撤销的操作;无法撤销时返回 `None` | ||
""" | ||
if self.undo_log: | ||
# pop() 操作会在空双向队列是触发 IndexError | ||
activity = self.undo_log.pop() | ||
self.toggle_cell(activity) | ||
self.redo_log.append(activity) | ||
return activity | ||
return None | ||
|
||
def redo(self) -> Activity | None: | ||
""" | ||
重做下一步 | ||
Returns: | ||
(Activity | None): 能够重做时,返回本次重做的操作;无法重做时返回 `None` | ||
""" | ||
if self.redo_log: | ||
activity = self.redo_log.pop() | ||
self.toggle_cell(activity) | ||
self.undo_log.append(activity) | ||
return activity | ||
return None | ||
|
||
def move(self, activity: Activity) -> None: | ||
""" | ||
执行一次游戏移动 | ||
「移动」特指用户的一次交互,不管是用户左击还是右击 | ||
Args: | ||
activity (Activity): 用户操作 | ||
""" | ||
self.toggle_cell(activity) | ||
self.undo_log.append(activity) | ||
self.redo_log.clear() | ||
|
||
def is_solved(self) -> bool: | ||
"""检查谜题是否已被用户求解成功""" | ||
current = self.generate_clues(self.grid) | ||
return current == self.clues | ||
|
||
def get_answer(self) -> str: | ||
"""获取字符串形式的题解""" | ||
return "\n".join("".join(map(lambda _: "[X]" if _ else "[ ]", r)) for r in self.answer) |
Empty file.
Oops, something went wrong.