ぷよぷよプログラミングAI学習システムは、中高生の自己調整学習に向けて開発されました。
どうやって、Chromebookで、10分で、素敵な人工知能を作れるのでしょうか?
秘密は、3つ。
1 jupyter liteで、webクライアントがpythonコードを実行
2 超軽量な機械学習フレームワークdezeroを採用
3 ぷよぷよのstageとactionがコンパクト
Github&X
ぷよぷよプログラミングAI学習用まとめ
チュートリアルまとめ
Step6 Animation
6 どういう風に動いているか、アニメーションで見たいよね
step5 でランダムに選択するAgentで何かが動いています。どうなっているのかみたいですね。
公式を改造して、見れるようにします。
6.1 puyopuyo-master
puyopuyo-aiフォルダーと同じレベルにpuyopuyo-masterフォルダーがあります。puyopuyo-masterフォルダーにagent_package,agent,mod_srcフォルダーを作ります。agentフォルダーには、dezero_emb.pyをコピーします。
import os
import shutil
org_folder_name = '../puyopuyo-master'
agent_pkg_folder_name = '../puyopuyo-master/agent_package'
agent_folder_name = '../puyopuyo-master/agent_package/agent'
mod_folder_name = '../puyopuyo-master/agent_package/mod_src'
print(os.listdir(org_folder_name))
if not os.path.isdir(agent_pkg_folder_name):
os.mkdir(agent_pkg_folder_name)
if not os.path.isdir(agent_folder_name):
os.mkdir(agent_folder_name)
shutil.copy( 'dezero_emb.py',agent_folder_name)
if not os.path.isdir(mod_folder_name):
os.mkdir(mod_folder_name)
['css', 'img', 'index.html', 'README.me', 'src']
6.2 index_html
大幅に書き換えます。
%%writefile $org_folder_name/index_mod.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<meta
http-equiv="Content-Security-Policy"
content="default-src * data: gap: content: https://ssl.gstatic.com; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'"
/>
<title>Sim puyo</title>
<script src="https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
<script>
async function pyodide_load(){
pyodide = await loadPyodide();
await pyodide.loadPackage("numpy");
};
pyodide_load();
</script>
<link rel="stylesheet" href="css/style.css" />
</head>
<body style="margin: 0;" >
<input type="file" id="getfile" accept=".js, .py, .zip" >
<input type="button" id="load" value="load" />
<script>
function start_loop(){
initialize();
loop();
};
async function loading(){
const button = document.querySelector('#load');
button.style.display = 'none';
const button2 = document.querySelector('#getfile');
button2.style.display = 'none';
if (typeof loop !== 'function'){
const list_packages=[
"config.js",
"stage.js",
"score.js",
"puyoimage.js",
"player.js",
"game.js"];
for(const mod_name of list_packages){
var script = document.createElement('script');
script.src = "src/" + mod_name;
document.head.appendChild(script);
}
}
setTimeout(start_loop,1000);
}
var load = document.querySelector('#load');
load.addEventListener("click", loading);
var parent_files = document.querySelector('#getfile');
parent_files.onchange = async function (){
var filelist = parent_files.files;
for(file of filelist){
var reader = new FileReader();
console.log(file.name);
let file_type = file.name.split('.').pop();
if(file_type=='js'){
reader.readAsText(file);
reader.onload = function () {
"use strict";
eval?.(reader.result);
};
}
if(file_type=='py'){
reader.readAsText(file);
reader.onload = function () {
pyodide.runPython(reader.result);
};
}
if(file_type=='zip'){
console.log(file);
var zip_buffer = await file.arrayBuffer();
let mod_name = file.name.split('.').slice(0, -1).join('.');
await pyodide.unpackArchive(zip_buffer, "zip");
var zip = JSZip();
zip.loadAsync(file).then(function(){
for (const fileName in zip.files){
console.log(fileName);
const child_file = zip.files[fileName];
let infile_type = fileName.split('.').pop();
if(!child_file.dir && infile_type=='js'){
child_file.async('text').then(function(content){
var script = document.createElement('script');
script.text=content;
document.head.appendChild(script);
});
}
};
});
}
}
};
</script>
<!---<div id="stage" style="position:absolute; left: 0; top: 0; overflow: hidden;"></div> -->
<div
id="stage"
style="
position: relative;
margin: 0 auto;
overflow: hidden;
background: url(img/puyo_1bg.png);
"
></div>
<!-- <div id="score" style="position:absolute; left: 0; top: 0; overflow: hidden; text-align: right;"></div> -->
<div
id="score"
style="margin: 0 auto; overflow: hidden; text-align: right;"
></div>
<div style="display: none;">
<img src="img/puyo_1.png" id="puyo_1" />
<img src="img/puyo_2.png" id="puyo_2" />
<img src="img/puyo_3.png" id="puyo_3" />
<img src="img/puyo_4.png" id="puyo_4" />
<img src="img/puyo_5.png" id="puyo_5" />
<img src="img/batankyu.png" id="batankyu" />
<img src="img/zenkeshi.png" id="zenkeshi" />
<img src="img/0.png" id="font0" />
<img src="img/1.png" id="font1" />
<img src="img/2.png" id="font2" />
<img src="img/3.png" id="font3" />
<img src="img/4.png" id="font4" />
<img src="img/5.png" id="font5" />
<img src="img/6.png" id="font6" />
<img src="img/7.png" id="font7" />
<img src="img/8.png" id="font8" />
<img src="img/9.png" id="font9" />
</div>
</body>
</html>
Writing ../puyopuyo-master/index_mod.html
<script src="https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
pyodideとjszipをインストールします。
- pyodide : pythonをwebで使用するツール
- jszip : zipファイルをwebで使用するツール
<script>
async function pyodide_load(){
pyodide = await loadPyodide();
await pyodide.loadPackage("numpy");
};
pyodide_load();
</script>
- pyodideをプレインストールしています。
- asyncとawaitは非同期処理のことで、外部ファイルを読み込むときには必要です。
<input type="file" id="getfile" accept=".js, .py, .zip" >
<input type="button" id="load" value="load" />
file読み込みボタンと、実行ボタンです。
function start_loop(){
initialize();
loop();
};
async function loading(){
const button = document.querySelector('#load');
button.style.display = 'none';
const button2 = document.querySelector('#getfile');
button2.style.display = 'none';
if (typeof loop !== 'function'){
const list_packages=[
"config.js",
"stage.js",
"score.js",
"puyoimage.js",
"player.js",
"game.js"];
for(const mod_name of list_packages){
var script = document.createElement('script');
script.src = "src/" + mod_name;
document.head.appendChild(script);
}
}
setTimeout(start_loop,1000);
}
var load = document.querySelector('#load');
load.addEventListener("click", loading);
- 実行ボタンのscriptです。
- 実行時に2つのボタンを非表示にします。
- もしも、loop関数がなければ、手動のソースファイルをインストールします。
- loopをスタートします。
var parent_files = document.querySelector('#getfile');
parent_files.onchange = async function (){
var filelist = parent_files.files;
for(file of filelist){
var reader = new FileReader();
console.log(file.name);
let file_type = file.name.split('.').pop();
if(file_type=='js'){
reader.readAsText(file);
reader.onload = function () {
"use strict";
eval?.(reader.result);
};
}
if(file_type=='py'){
reader.readAsText(file);
reader.onload = function () {
pyodide.runPython(reader.result);
};
}
if(file_type=='zip'){
console.log(file);
var zip_buffer = await file.arrayBuffer();
let mod_name = file.name.split('.').slice(0, -1).join('.');
await pyodide.unpackArchive(zip_buffer, "zip");
var zip = JSZip();
zip.loadAsync(file).then(function(){
for (const fileName in zip.files){
console.log(fileName);
const child_file = zip.files[fileName];
let infile_type = fileName.split('.').pop();
if(!child_file.dir && infile_type=='js'){
child_file.async('text').then(function(content){
var script = document.createElement('script');
script.text=content;
document.head.appendChild(script);
});
}
};
});
}
}
};
- ファイル選択は、複数ファイルを選択可能なので、1つづつ処理をします。
- ファイル形式によって、js,py,zipの3種類に分けて実行します。
- js : javascriptファイルは、evalで即時実行します。
- py : pythonファイルは、pyodideで即時実行します。
- zip : zipファイルは、pyodideで展開されて、中身のファイルのうち、jsファイルは、documentのscriptファイルとして追加されます。
- 実行タイミングが、複数ファイルの時には、注意が必要です。
6.3 player.js
次に、player.jsを変更します。これは、デバイス入力からagentによる自動入力に切り替えるものです。
- 可読性を向上するために、コメント行は削除しています。
- createNewPuyo : 新しいpuyoを作る段階で、人工知能にactionを作らせます。
- playing : playerがどうするか決める部分です。actionと違うならば、まず回転させて、移動させます。
%%writefile $mod_folder_name/5player.js
class Player {
static initialize() {
this.keyStatus = {
right: false,
left: false,
up: false,
down: false,
};
}
static createNewPuyo() {
if (Stage.board[0][2]) {
return false;
}
const puyoColors = Math.max(1, Math.min(5, Config.puyoColors));
this.centerPuyo = Math.floor(Math.random() * puyoColors) + 1;
this.movablePuyo = Math.floor(Math.random() * puyoColors) + 1;
this.centerPuyoElement = PuyoImage.getPuyo(this.centerPuyo);
this.movablePuyoElement = PuyoImage.getPuyo(this.movablePuyo);
Stage.stageElement.appendChild(this.centerPuyoElement);
Stage.stageElement.appendChild(this.movablePuyoElement);
this.puyoStatus = {
x: 2,
y: -1,
left: 2 * Config.puyoImgWidth,
top: -1 * Config.puyoImgHeight,
dx: 0,
dy: -1,
rotation: 90,
};
this.groundFrame = 0;
this.setPuyoPosition();
/////// add
let c1 = this.centerPuyo;
let c2 = this.movablePuyo;
this.action = create_action(Stage.board, [c1,c2]);
///////
return true;
}
static setPuyoPosition() {
this.centerPuyoElement.style.left = this.puyoStatus.left + "px";
this.centerPuyoElement.style.top = this.puyoStatus.top + "px";
const x =
this.puyoStatus.left +
Math.cos((this.puyoStatus.rotation * Math.PI) / 180) *
Config.puyoImgWidth;
const y =
this.puyoStatus.top -
Math.sin((this.puyoStatus.rotation * Math.PI) / 180) *
Config.puyoImgHeight;
this.movablePuyoElement.style.left = x + "px";
this.movablePuyoElement.style.top = y + "px";
}
static falling(isDownPressed) {
let isBlocked = false;
let x = this.puyoStatus.x;
let y = this.puyoStatus.y;
let dx = this.puyoStatus.dx;
let dy = this.puyoStatus.dy;
if (
y + 1 >= Config.stageRows ||
Stage.board[y + 1][x] ||
(y + dy + 1 >= 0 &&
(y + dy + 1 >= Config.stageRows || Stage.board[y + dy + 1][x + dx]))
) {
isBlocked = true;
}
if (!isBlocked) {
this.puyoStatus.top += Config.playerFallingSpeed;
if (isDownPressed) {
this.puyoStatus.top += Config.playerDownSpeed;
}
if (Math.floor(this.puyoStatus.top / Config.puyoImgHeight) != y) {
if (isDownPressed) {
Score.addScore(1);
}
y += 1;
this.puyoStatus.y = y;
if (
y + 1 >= Config.stageRows ||
Stage.board[y + 1][x] ||
(y + dy + 1 >= 0 &&
(y + dy + 1 >= Config.stageRows || Stage.board[y + dy + 1][x + dx]))
) {
isBlocked = true;
}
if (!isBlocked) {
this.groundFrame = 0;
return;
} else {
this.puyoStatus.top = y * Config.puyoImgHeight;
this.groundFrame = 1;
return;
}
} else {
this.groundFrame = 0;
return;
}
}
if (this.groundFrame == 0) {
this.groundFrame = 1;
return;
} else {
this.groundFrame++;
if (this.groundFrame > Config.playerGroundFrame) {
return true;
}
}
}
static playing(frame) {
//// add
this.keyStatus = {
right: false,
left: false,
up: false,
down: false,
};
if(this.puyoStatus.rotation !== this.action[1]){
this.keyStatus.up = true;
}else if(this.puyoStatus.x < this.action[0]){
this.keyStatus.right = true;
}else if(this.puyoStatus.x > this.action[0]){
this.keyStatus.left = true;
}else{
this.keyStatus.down = true;
}
////
if (this.falling(this.keyStatus.down)) {
this.setPuyoPosition();
return "fix";
}
this.setPuyoPosition();
if (this.keyStatus.right || this.keyStatus.left) {
const cx = this.keyStatus.right ? 1 : -1;
const x = this.puyoStatus.x;
const y = this.puyoStatus.y;
const mx = x + this.puyoStatus.dx;
const my = y + this.puyoStatus.dy;
let canMove = true;
if (
y < 0 ||
x + cx < 0 ||
x + cx >= Config.stageCols ||
Stage.board[y][x + cx]
) {
if (y >= 0) {
canMove = false;
}
}
if (
my < 0 ||
mx + cx < 0 ||
mx + cx >= Config.stageCols ||
Stage.board[my][mx + cx]
) {
if (my >= 0) {
canMove = false;
}
}
if (this.groundFrame === 0) {
if (
y + 1 < 0 ||
x + cx < 0 ||
x + cx >= Config.stageCols ||
Stage.board[y + 1][x + cx]
) {
if (y + 1 >= 0) {
canMove = false;
}
}
if (
my + 1 < 0 ||
mx + cx < 0 ||
mx + cx >= Config.stageCols ||
Stage.board[my + 1][mx + cx]
) {
if (my + 1 >= 0) {
canMove = false;
}
}
}
if (canMove) {
this.actionStartFrame = frame;
this.moveSource = x * Config.puyoImgWidth;
this.moveDestination = (x + cx) * Config.puyoImgWidth;
this.puyoStatus.x += cx;
return "moving";
}
} else if (this.keyStatus.up) {
const x = this.puyoStatus.x;
const y = this.puyoStatus.y;
const mx = x + this.puyoStatus.dx;
const my = y + this.puyoStatus.dy;
const rotation = this.puyoStatus.rotation;
let canRotate = true;
let cx = 0;
let cy = 0;
if (rotation === 0) {
} else if (rotation === 90) {
if (
y + 1 < 0 ||
x - 1 < 0 ||
x - 1 >= Config.stageCols ||
Stage.board[y + 1][x - 1]
) {
if (y + 1 >= 0) {
cx = 1;
}
}
if (cx === 1) {
if (
y + 1 < 0 ||
x + 1 < 0 ||
y + 1 >= Config.stageRows ||
x + 1 >= Config.stageCols ||
Stage.board[y + 1][x + 1]
) {
if (y + 1 >= 0) {
canRotate = false;
}
}
}
} else if (rotation === 180) {
if (y + 2 < 0 || y + 2 >= Config.stageRows || Stage.board[y + 2][x]) {
if (y + 2 >= 0) {
cy = -1;
}
}
if (
y + 2 < 0 ||
y + 2 >= Config.stageRows ||
x - 1 < 0 ||
Stage.board[y + 2][x - 1]
) {
if (y + 2 >= 0) {
cy = -1;
}
}
} else if (rotation === 270) {
if (
y + 1 < 0 ||
x + 1 < 0 ||
x + 1 >= Config.stageCols ||
Stage.board[y + 1][x + 1]
) {
if (y + 1 >= 0) {
cx = -1;
}
}
if (cx === -1) {
if (
y + 1 < 0 ||
x - 1 < 0 ||
x - 1 >= Config.stageCols ||
Stage.board[y + 1][x - 1]
) {
if (y + 1 >= 0) {
canRotate = false;
}
}
}
}
if (canRotate) {
if (cy === -1) {
if (this.groundFrame > 0) {
this.puyoStatus.y -= 1;
this.groundFrame = 0;
}
this.puyoStatus.top = this.puyoStatus.y * Config.puyoImgHeight;
}
this.actionStartFrame = frame;
this.rotateBeforeLeft = x * Config.puyoImgHeight;
this.rotateAfterLeft = (x + cx) * Config.puyoImgHeight;
this.rotateFromRotation = this.puyoStatus.rotation;
this.puyoStatus.x += cx;
const distRotation = (this.puyoStatus.rotation + 90) % 360;
const dCombi = [
[1, 0],
[0, -1],
[-1, 0],
[0, 1],
][distRotation / 90];
this.puyoStatus.dx = dCombi[0];
this.puyoStatus.dy = dCombi[1];
return "rotating";
}
}
return "playing";
}
static moving(frame) {
this.falling();
const ratio = Math.min(
1,
(frame - this.actionStartFrame) / Config.playerMoveFrame
);
this.puyoStatus.left =
ratio * (this.moveDestination - this.moveSource) + this.moveSource;
this.setPuyoPosition();
if (ratio === 1) {
return false;
}
return true;
}
static rotating(frame) {
this.falling();
const ratio = Math.min(
1,
(frame - this.actionStartFrame) / Config.playerRotateFrame
);
this.puyoStatus.left =
(this.rotateAfterLeft - this.rotateBeforeLeft) * ratio +
this.rotateBeforeLeft;
this.puyoStatus.rotation = this.rotateFromRotation + ratio * 90;
this.setPuyoPosition();
if (ratio === 1) {
this.puyoStatus.rotation = (this.rotateFromRotation + 90) % 360;
return false;
}
return true;
}
static fix() {
const x = this.puyoStatus.x;
const y = this.puyoStatus.y;
const dx = this.puyoStatus.dx;
const dy = this.puyoStatus.dy;
if (y >= 0) {
Stage.setPuyo(x, y, this.centerPuyo);
Stage.puyoCount++;
}
if (y + dy >= 0) {
Stage.setPuyo(x + dx, y + dy, this.movablePuyo);
Stage.puyoCount++;
}
Stage.stageElement.removeChild(this.centerPuyoElement);
Stage.stageElement.removeChild(this.movablePuyoElement);
this.centerPuyoElement = null;
this.movablePuyoElement = null;
}
static batankyu() {
if (this.keyStatus.up) {
location.reload();
}
}
}
Writing ../puyopuyo-master/agent_package/mod_src/5player.js
6.4 game.js
手動で行う部分を全面的に削除しました。
%%writefile $mod_folder_name/6game.js
// 起動されたときに呼ばれる関数を登録する
let mode; // ゲームの現在の状況
let frame; // ゲームの現在フレーム(1/60秒ごとに1追加される)
let combinationCount = 0; // 何連鎖かどうか
function initialize() {
// 画像を準備する
PuyoImage.initialize();
// ステージを準備する
Stage.initialize();
// ユーザー操作の準備をする
Player.initialize();
// シーンを初期状態にセットする
Score.initialize();
// スコア表示の準備をする
mode = "start";
// フレームを初期化する
frame = 0;
}
function loop() {
switch (mode) {
case "start":
// 最初は、もしかしたら空中にあるかもしれないぷよを自由落下させるところからスタート
mode = "checkFall";
break;
case "checkFall":
// 落ちるかどうか判定する
if (Stage.checkFall()) {
mode = "fall";
} else {
// 落ちないならば、ぷよを消せるかどうか判定する
mode = "checkErase";
}
break;
case "fall":
if (!Stage.fall()) {
// すべて落ちきったら、ぷよを消せるかどうか判定する
mode = "checkErase";
}
break;
case "checkErase":
// 消せるかどうか判定する
const eraseInfo = Stage.checkErase(frame);
if (eraseInfo) {
mode = "erasing";
combinationCount++;
// 得点を計算する
Score.calculateScore(
combinationCount,
eraseInfo.piece,
eraseInfo.color
);
Stage.hideZenkeshi();
} else {
if (Stage.puyoCount === 0 && combinationCount > 0) {
// 全消しの処理をする
Stage.showZenkeshi();
Score.addScore(3600);
}
combinationCount = 0;
// 消せなかったら、新しいぷよを登場させる
mode = "newPuyo";
}
break;
case "erasing":
if (!Stage.erasing(frame)) {
// 消し終わったら、再度落ちるかどうか判定する
mode = "checkFall";
}
break;
case "newPuyo":
if (!Player.createNewPuyo()) {
// 新しい操作用ぷよを作成出来なかったら、ゲームオーバー
mode = "gameOver";
} else {
// プレイヤーが操作可能
mode = "playing";
}
break;
case "playing":
// プレイヤーが操作する
const action = Player.playing(frame);
mode = action; // 'playing' 'moving' 'rotating' 'fix' のどれかが帰ってくる
break;
case "moving":
if (!Player.moving(frame)) {
// 移動が終わったので操作可能にする
mode = "playing";
}
break;
case "rotating":
if (!Player.rotating(frame)) {
// 回転が終わったので操作可能にする
mode = "playing";
}
break;
case "fix":
// 現在の位置でぷよを固定する
Player.fix();
// 固定したら、まず自由落下を確認する
mode = "checkFall";
break;
case "gameOver":
// ばたんきゅーの準備をする
PuyoImage.prepareBatankyu(frame);
mode = "batankyu";
break;
case "batankyu":
PuyoImage.batankyu(frame);
Player.batankyu();
break;
}
frame++;
requestAnimationFrame(loop); // 1/60秒後にもう一度呼び出す
}
Writing ../puyopuyo-master/agent_package/mod_src/6game.js
6.5 その他のソースファイル
その他の4つのソースファイルをコピーします。
名前を変更します。
shutil.copy(org_folder_name+"/src/config.js",mod_folder_name+"/1config.js")
shutil.copy(org_folder_name+"/src/stage.js",mod_folder_name+"/2stage.js")
shutil.copy(org_folder_name+"/src/score.js",mod_folder_name+"/3score.js")
shutil.copy(org_folder_name+"/src/puyoimage.js",mod_folder_name+"/4puyoimage.js")
'../puyopuyo-master/agent_package/mod_src/4puyoimage.js'
6.6 agent.py
まずは、ランダムagentです。
- 出力 : xの位置 、 回転0,90,180,270
- 入力 : list board. puyo_color
%%writefile $agent_folder_name/agent.py
import numpy as np
import random
class CFG:
Height = 12
Width = 6
n_color = 4
class Puyopuyo:
def __init__(self):
self.x = 2
self.y = 0
self.dx = [1, 0, -1, 0]
self.dy = [0, -1, 0, 1]
self.centerPuyo = random.randint(1, CFG.n_color)
self.movablePuyo = random.randint(1, CFG.n_color)
self.rotation = 1
class Agents:
def __init__(self):
self.name = 'agent'
def random_agent(self, board, puyo):
action_list = utils.create_action_list(board)
if len(action_list) == 0:
return [2,1]
random_id = random.randint(0, len(action_list)-1)
return action_list[random_id]
def __call__(self, board_list, puyo_c):
board_list = board_list.to_py()
board = np.zeros(CFG.Height * CFG.Width, dtype=np.int32).reshape(CFG.Height, CFG.Width)
for i in range(CFG.Height):
for j in range(CFG.Width):
if board_list[i][j] != None:
board[i][j] = int(board_list[i][j]['puyo'])
puyo = Puyopuyo()
puyo.centerPuyo = puyo_c[0]
puyo.movablePuyo = puyo_c[1]
action = self.random_agent(board, puyo)
action[1] = action[1] * 90
return action
class utils:
def check_collision(board, puyo):
rot = puyo.rotation
if rot == 0 and puyo.x == 5:
return True
if rot == 2 and puyo.x == 0:
return True
if puyo.y >= 12:
return True
if puyo.y == 11 and rot == 3 :
return True
if board[puyo.y, puyo.x] > 0 :
return True
if not( rot == 1) and board[puyo.y + puyo.dy[rot], puyo.x + puyo.dx[rot]] > 0:
return True
return False
def create_action_list(board):
puyo2 = Puyopuyo()
res = []
for rot in range(4):
for pos1 in range(6):
puyo2.x = pos1
puyo2.rotation = rot
if not utils.check_collision(board, puyo2):
res.append([pos1, rot])
return res
Writing ../puyopuyo-master/agent_package/agent/agent.py
6.7 init
init.pyと_init__.jsを入れます。
%%writefile $agent_folder_name/__init__.py
from agent.dezero_emb import *
from agent.agent import *
Writing ../puyopuyo-master/agent_package/agent/__init__.py
%%writefile $agent_folder_name/__init__.js
create_action = pyodide.runPython(`
from agent import *
agent = Agents()
agent`);
Writing ../puyopuyo-master/agent_package/agent/__init__.js
6.8 zipファイルを作ります。
import zipfile
shutil.make_archive('agent', format='zip', root_dir=agent_pkg_folder_name)
'c:\\Users\\user\\Documents\\puyopuyo-py\\jupyterlite\\draft\\content\\puyopuyo-ai\\agent.zip'