diff --git a/boards/default_src/python_pyodide/language/en.js b/boards/default_src/python_pyodide/language/en.js index b54514c5..a8e6bb3c 100644 --- a/boards/default_src/python_pyodide/language/en.js +++ b/boards/default_src/python_pyodide/language/en.js @@ -1,6 +1,12 @@ export const EnMsg = { 'PYTHON_PYODIDE_IMAGE': 'Image', 'PYTHON_PYODIDE_TOOL': 'Teachable Machine', + 'PYTHON_PYODIDE_GAME': 'Game of Life', + 'PYTHON_PYODIDE_GAME_EPOCH': 'Iterations', + 'PYTHON_PYODIDE_GAME_START': 'Start', + 'PYTHON_PYODIDE_GAME_PAUSE': 'Pause', + 'PYTHON_PYODIDE_GAME_RANDOM': 'Random Initialization', + 'PYTHON_PYODIDE_GAME_RESET': 'Reset', 'PYTHON_PYODIDE_LOADING': 'Python3 kernel loading...', 'PYTHON_PYODIDE_FILE_SYSTEM': 'Local File System', 'PYTHON_PYODIDE_LOAD_FILE_SYSTEM': 'Load Local Folder' diff --git a/boards/default_src/python_pyodide/language/zh-hans.js b/boards/default_src/python_pyodide/language/zh-hans.js index bcb7a932..5504592f 100644 --- a/boards/default_src/python_pyodide/language/zh-hans.js +++ b/boards/default_src/python_pyodide/language/zh-hans.js @@ -1,6 +1,12 @@ export const ZhHansMsg = { 'PYTHON_PYODIDE_IMAGE': '图像', 'PYTHON_PYODIDE_TOOL': '可教机器', + 'PYTHON_PYODIDE_GAME': '生命游戏', + 'PYTHON_PYODIDE_GAME_EPOCH': '代数', + 'PYTHON_PYODIDE_GAME_START': '开始', + 'PYTHON_PYODIDE_GAME_PAUSE': '暂停', + 'PYTHON_PYODIDE_GAME_RANDOM': '随机初始化', + 'PYTHON_PYODIDE_GAME_RESET': '重置', 'PYTHON_PYODIDE_LOADING': 'Python3内核载入中...', 'PYTHON_PYODIDE_FILE_SYSTEM': '本地文件系统', 'PYTHON_PYODIDE_LOAD_FILE_SYSTEM': '载入本地文件夹' diff --git a/boards/default_src/python_pyodide/language/zh-hant.js b/boards/default_src/python_pyodide/language/zh-hant.js index 60c65ada..8d632420 100644 --- a/boards/default_src/python_pyodide/language/zh-hant.js +++ b/boards/default_src/python_pyodide/language/zh-hant.js @@ -1,6 +1,12 @@ export const ZhHantMsg = { 'PYTHON_PYODIDE_IMAGE': '影像', 'PYTHON_PYODIDE_TOOL': '可教機器', + 'PYTHON_PYODIDE_GAME': '生命遊戲', + 'PYTHON_PYODIDE_GAME_EPOCH': '代數', + 'PYTHON_PYODIDE_GAME_START': '開始', + 'PYTHON_PYODIDE_GAME_PAUSE': '暫停', + 'PYTHON_PYODIDE_GAME_RANDOM': '隨機初始化', + 'PYTHON_PYODIDE_GAME_RESET': '重置', 'PYTHON_PYODIDE_LOADING': 'Python3核心載入...', 'PYTHON_PYODIDE_FILE_SYSTEM': '本機檔案系統', 'PYTHON_PYODIDE_LOAD_FILE_SYSTEM': '載入本機資料夾' diff --git a/boards/default_src/python_pyodide/others/python-shell.js b/boards/default_src/python_pyodide/others/python-shell.js index 1fe2b2a2..8ff8a1af 100644 --- a/boards/default_src/python_pyodide/others/python-shell.js +++ b/boards/default_src/python_pyodide/others/python-shell.js @@ -15,6 +15,7 @@ import { KernelLoader } from '@basthon/kernel-loader'; import StatusBarImage from './statusbar-image'; import StatusBarFileSystem from './statusbar-filesystem'; import StatusBarTool from './statusbar-tool'; +import StatusBarGame from './statusbar-game'; import TeachableMachineApp from './teachableMachine/App.vue'; import LOADER_TEMPLATE from '../templates/html/loader.html'; @@ -63,6 +64,7 @@ export default class PythonShell { this.statusBarTool = StatusBarTool.init(); const teachableMachineApp = createApp(TeachableMachineApp); teachableMachineApp.mount(this.statusBarTool.getContent()[0]); + this.statusBarGame = StatusBarGame.init(); this.pythonShell = new PythonShell(); this.pyodide = window.pyodide; this.interruptBuffer = new Uint8Array(new ArrayBuffer(1)); diff --git a/boards/default_src/python_pyodide/others/statusbar-game.js b/boards/default_src/python_pyodide/others/statusbar-game.js new file mode 100644 index 00000000..325fea0d --- /dev/null +++ b/boards/default_src/python_pyodide/others/statusbar-game.js @@ -0,0 +1,227 @@ +import $ from 'jquery'; +import { Msg } from 'blockly/core'; +import { + PageBase, + HTMLTemplate, + StatusBarsManager, + Workspace +} from 'mixly'; +import '../language/loader'; +import STATUS_BAR_GAME_TEMPLATE from '../templates/html/statusbar-game.html'; + + +export default class StatusBarGame extends PageBase { + static { + HTMLTemplate.add( + 'html/statusbar/statusbar-game.html', + new HTMLTemplate(STATUS_BAR_GAME_TEMPLATE) + ); + + this.init = function () { + StatusBarsManager.typesRegistry.register(['game'], StatusBarGame); + const mainWorkspace = Workspace.getMain(); + const statusBarsManager = mainWorkspace.getStatusBarsManager(); + statusBarsManager.add({ + type: 'game', + id: 'game', + name: Msg.PYTHON_PYODIDE_GAME, + title: Msg.PYTHON_PYODIDE_GAME + }); + statusBarsManager.changeTo('output'); + return statusBarsManager.get('game'); + } + } + + #$startBtn_ = null; + #$pauseBtn_ = null; + #$randomBtn_ = null; + #$resetBtn_ = null; + #$generation_ = null; + #$grid_ = null; + #GRID_SIZE_ = 10; + #SPEED_ = 500; + #grid_ = []; + #isRunning_ = false; + #generation_ = 0; + #intervalId_ = null; + + constructor() { + super(); + const $content = $(HTMLTemplate.get('html/statusbar/statusbar-game.html').render({ + epoch: Msg.PYTHON_PYODIDE_GAME_EPOCH, + start: Msg.PYTHON_PYODIDE_GAME_START, + pause: Msg.PYTHON_PYODIDE_GAME_PAUSE, + random: Msg.PYTHON_PYODIDE_GAME_RANDOM, + reset: Msg.PYTHON_PYODIDE_GAME_RESET + })); + this.setContent($content); + this.#$startBtn_ = $content.find('.start-btn'); + this.#$pauseBtn_ = $content.find('.pause-btn'); + this.#$randomBtn_ = $content.find('.random-btn'); + this.#$resetBtn_ = $content.find('.reset-btn'); + this.#$generation_ = $content.find('.generation'); + this.#$grid_ = $content.find('.grid'); + this.#addEventListeners_(); + } + + #addEventListeners_() { + this.#$startBtn_.click(() => this.startGame()); + this.#$pauseBtn_.click(() => this.pauseGame()); + this.#$randomBtn_.click(() => this.randomInitialize()); + this.#$resetBtn_.click(() => this.resetGame()); + } + + // 初始化网格 + initializeGrid() { + this.#$grid_.empty(); + this.#grid_ = []; + + for (let i = 0; i < this.#GRID_SIZE_; i++) { + this.#grid_[i] = []; + for (let j = 0; j < this.#GRID_SIZE_; j++) { + this.#grid_[i][j] = 0; // 0表示死亡,1表示存活 + + const cell = document.createElement('div'); + cell.className = 'cell'; + cell.dataset.row = i; + cell.dataset.col = j; + + cell.addEventListener('click', () => this.toggleCell(i, j)); + + this.#$grid_.append(cell); + } + } + this.updateGridDisplay(); + } + + // 切换细胞状态 + toggleCell(row, col) { + if (!this.#isRunning_) { + this.#grid_[row][col] = this.#grid_[row][col] === 0 ? 1 : 0; + this.updateGridDisplay(); + } + } + + // 更新网格显示 + updateGridDisplay() { + const $cells = this.#$grid_.children('.cell'); + for (let i = 0; i < $cells.length; i++) { + const cell = $cells[i]; + const row = parseInt(cell.dataset.row); + const col = parseInt(cell.dataset.col); + if (this.#grid_[row][col] === 1) { + cell.classList.add('alive'); + } else { + cell.classList.remove('alive'); + } + } + } + + // 计算下一代 + nextGeneration() { + const newGrid = []; + + for (let i = 0; i < this.#GRID_SIZE_; i++) { + newGrid[i] = []; + for (let j = 0; j < this.#GRID_SIZE_; j++) { + const neighbors = this.countNeighbors(i, j); + + if (this.#grid_[i][j] === 1) { + // 存活细胞:周围有2-3个存活细胞则继续存活 + newGrid[i][j] = (neighbors === 2 || neighbors === 3) ? 1 : 0; + } else { + // 死亡细胞:周围有3个存活细胞则复活 + newGrid[i][j] = neighbors === 3 ? 1 : 0; + } + } + } + + this.#grid_ = newGrid; + this.#generation_++; + this.#$generation_.text(this.#generation_); + this.updateGridDisplay(); + } + + // 计算周围存活细胞数量 + countNeighbors(row, col) { + let count = 0; + for (let i = -1; i <= 1; i++) { + for (let j = -1; j <= 1; j++) { + if (i === 0 && j === 0) continue; // 跳过自身 + + const newRow = row + i; + const newCol = col + j; + + // 检查边界 + if (newRow >= 0 && newRow < this.#GRID_SIZE_ && newCol >= 0 && newCol < this.#GRID_SIZE_) { + count += this.#grid_[newRow][newCol]; + } + } + } + return count; + } + + // 开始游戏 + startGame() { + if (!this.#isRunning_) { + this.#isRunning_ = true; + this.#generation_ = 0; + this.#$generation_.text(this.#generation_); + this.#intervalId_ = setInterval(() => this.nextGeneration(), this.#SPEED_); + this.updateButtons(); + } + } + + // 暂停游戏 + pauseGame() { + if (this.#isRunning_) { + this.#isRunning_ = false; + clearInterval(this.#intervalId_); + this.updateButtons(); + } + } + + // 随机初始化网格 + randomInitialize() { + if (!this.#isRunning_) { + for (let i = 0; i < this.#GRID_SIZE_; i++) { + for (let j = 0; j < this.#GRID_SIZE_; j++) { + // 25%的概率生成存活细胞 + this.#grid_[i][j] = Math.random() < 0.25 ? 1 : 0; + } + } + this.updateGridDisplay(); + } + } + + // 重置游戏 + resetGame() { + this.#isRunning_ = false; + clearInterval(this.#intervalId_); + this.#generation_ = 0; + this.#$generation_.text(this.#generation_); + this.initializeGrid(); + this.updateButtons(); + } + + // 更新按钮状态 + updateButtons() { + this.#$startBtn_.attr('disabled', this.#isRunning_); + this.#$pauseBtn_.attr('disabled', !this.#isRunning_); + this.#$randomBtn_.attr('disabled', this.#isRunning_); + this.#$resetBtn_.attr('disabled', false); + } + + init() { + super.init(); + this.hideCloseBtn(); + this.initializeGrid(); + this.updateButtons(); + } + + onMounted() { } + + onUnmounted() { } + + resize() { } +} \ No newline at end of file diff --git a/boards/default_src/python_pyodide/templates/html/statusbar-game.html b/boards/default_src/python_pyodide/templates/html/statusbar-game.html new file mode 100644 index 00000000..da359cce --- /dev/null +++ b/boards/default_src/python_pyodide/templates/html/statusbar-game.html @@ -0,0 +1,107 @@ + +