feat(board): python_pyodide板卡状态栏添加新tab 生命游戏
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
export const EnMsg = {
|
export const EnMsg = {
|
||||||
'PYTHON_PYODIDE_IMAGE': 'Image',
|
'PYTHON_PYODIDE_IMAGE': 'Image',
|
||||||
'PYTHON_PYODIDE_TOOL': 'Teachable Machine',
|
'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_LOADING': 'Python3 kernel loading...',
|
||||||
'PYTHON_PYODIDE_FILE_SYSTEM': 'Local File System',
|
'PYTHON_PYODIDE_FILE_SYSTEM': 'Local File System',
|
||||||
'PYTHON_PYODIDE_LOAD_FILE_SYSTEM': 'Load Local Folder'
|
'PYTHON_PYODIDE_LOAD_FILE_SYSTEM': 'Load Local Folder'
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
export const ZhHansMsg = {
|
export const ZhHansMsg = {
|
||||||
'PYTHON_PYODIDE_IMAGE': '图像',
|
'PYTHON_PYODIDE_IMAGE': '图像',
|
||||||
'PYTHON_PYODIDE_TOOL': '可教机器',
|
'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_LOADING': 'Python3内核载入中...',
|
||||||
'PYTHON_PYODIDE_FILE_SYSTEM': '本地文件系统',
|
'PYTHON_PYODIDE_FILE_SYSTEM': '本地文件系统',
|
||||||
'PYTHON_PYODIDE_LOAD_FILE_SYSTEM': '载入本地文件夹'
|
'PYTHON_PYODIDE_LOAD_FILE_SYSTEM': '载入本地文件夹'
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
export const ZhHantMsg = {
|
export const ZhHantMsg = {
|
||||||
'PYTHON_PYODIDE_IMAGE': '影像',
|
'PYTHON_PYODIDE_IMAGE': '影像',
|
||||||
'PYTHON_PYODIDE_TOOL': '可教機器',
|
'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_LOADING': 'Python3核心載入...',
|
||||||
'PYTHON_PYODIDE_FILE_SYSTEM': '本機檔案系統',
|
'PYTHON_PYODIDE_FILE_SYSTEM': '本機檔案系統',
|
||||||
'PYTHON_PYODIDE_LOAD_FILE_SYSTEM': '載入本機資料夾'
|
'PYTHON_PYODIDE_LOAD_FILE_SYSTEM': '載入本機資料夾'
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { KernelLoader } from '@basthon/kernel-loader';
|
|||||||
import StatusBarImage from './statusbar-image';
|
import StatusBarImage from './statusbar-image';
|
||||||
import StatusBarFileSystem from './statusbar-filesystem';
|
import StatusBarFileSystem from './statusbar-filesystem';
|
||||||
import StatusBarTool from './statusbar-tool';
|
import StatusBarTool from './statusbar-tool';
|
||||||
|
import StatusBarGame from './statusbar-game';
|
||||||
import TeachableMachineApp from './teachableMachine/App.vue';
|
import TeachableMachineApp from './teachableMachine/App.vue';
|
||||||
import LOADER_TEMPLATE from '../templates/html/loader.html';
|
import LOADER_TEMPLATE from '../templates/html/loader.html';
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ export default class PythonShell {
|
|||||||
this.statusBarTool = StatusBarTool.init();
|
this.statusBarTool = StatusBarTool.init();
|
||||||
const teachableMachineApp = createApp(TeachableMachineApp);
|
const teachableMachineApp = createApp(TeachableMachineApp);
|
||||||
teachableMachineApp.mount(this.statusBarTool.getContent()[0]);
|
teachableMachineApp.mount(this.statusBarTool.getContent()[0]);
|
||||||
|
this.statusBarGame = StatusBarGame.init();
|
||||||
this.pythonShell = new PythonShell();
|
this.pythonShell = new PythonShell();
|
||||||
this.pyodide = window.pyodide;
|
this.pyodide = window.pyodide;
|
||||||
this.interruptBuffer = new Uint8Array(new ArrayBuffer(1));
|
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