feat(board): python_pyodide板卡状态栏添加新tab 生命游戏

This commit is contained in:
王立帮
2025-10-14 10:58:32 +08:00
parent b7d76c763e
commit be97f0d712
6 changed files with 354 additions and 0 deletions

View File

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

View File

@@ -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': '载入本地文件夹'

View File

@@ -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': '載入本機資料夾'

View File

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

View File

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

View File

@@ -0,0 +1,107 @@
<style>
div[m-id="{{d.mId}}"] {
display: flex;
flex-direction: column;
align-items: center;
flex-wrap: nowrap;
overflow: auto;
}
div[m-id="{{d.mId}}"] .game-info {
margin-bottom: 20px;
color: #666;
}
div[m-id="{{d.mId}}"] .grid {
display: grid;
grid-template-columns: repeat(10, 30px);
grid-template-rows: repeat(10, 30px);
gap: 1px;
margin: 20px auto;
width: fit-content;
background-color: #ddd;
border: 2px solid #333;
}
div[m-id="{{d.mId}}"] .cell {
width: 30px;
height: 30px;
background-color: white;
border: 1px solid #ccc;
cursor: pointer;
transition: background-color 0.2s;
}
div[m-id="{{d.mId}}"] .cell.alive {
background-color: #4CAF50;
}
div[m-id="{{d.mId}}"] .cell:hover {
background-color: #e0e0e0;
}
div[m-id="{{d.mId}}"] .cell.alive:hover {
background-color: #45a049;
}
div[m-id="{{d.mId}}"] .controls {
margin: 20px 0;
}
div[m-id="{{d.mId}}"] button {
padding: 10px 20px;
margin: 0 5px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
div[m-id="{{d.mId}}"] .start-btn {
background-color: #4CAF50;
color: white;
}
div[m-id="{{d.mId}}"] .pause-btn {
background-color: #ff9800;
color: white;
}
div[m-id="{{d.mId}}"] .reset-btn {
background-color: #f44336;
color: white;
}
div[m-id="{{d.mId}}"] .random-btn {
background-color: #9C27B0;
color: white;
}
div[m-id="{{d.mId}}"] button:hover {
opacity: 0.8;
}
div[m-id="{{d.mId}}"] button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
div[m-id="{{d.mId}}"] .generation-counter {
font-size: 18px;
font-weight: bold;
margin: 10px 0;
padding-top: 10px;
color: #333;
}
</style>
<div m-id="{{d.mId}}" class="page-item">
<div class="generation-counter">{{d.epoch}}: <span class="generation">0</span></div>
<div class="grid"></div>
<div class="controls">
<button class="start-btn">{{d.start}}</button>
<button class="pause-btn" disabled>{{d.pause}}</button>
<button class="random-btn">{{d.random}}</button>
<button class="reset-btn">{{d.reset}}</button>
</div>
</div>