feat(board): python_pyodide板卡状态栏添加新tab 生命游戏
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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': '载入本地文件夹'
|
||||
|
||||
@@ -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': '載入本機資料夾'
|
||||
|
||||
@@ -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));
|
||||
|
||||
227
boards/default_src/python_pyodide/others/statusbar-game.js
Normal file
227
boards/default_src/python_pyodide/others/statusbar-game.js
Normal 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() { }
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user