はじめに
Arduino の応用機器である Gamebuino を入手したので、以前から興味があった Scratch との連携をやってみました。
Gamebuino は、言ってしまえば、Arduino UNO にLCDやプッシュボタン、バッテリー等を付加して、コンパクトにしたものです。
一方、Scratch は、ブロックを組み合わせてインタラクティブにプログラミングを体験できる環境です。
題材を考えた結果、今回は、簡単な一人ポーカーを作ってみました。
具体的に
Scratch でランダムにカードの山札を作り、その中から5枚のカードをGamubuinoに配布します。Gamebuinoには、配布されたカードが表示されます。
ユーザは、その中の不要なカードをカーソルで選択し、Bボタンをクリックして Scratch へ伝えます。Scratchは、指定されたカードを破棄し、山札から次のカードをGamebuinoへ渡します。
ユーザは、役がそろったと思えば、Aボタンをクリックして、Scratchへ役の判定を依頼します。Scratchは、判定結果をGamebuinoへ通知しますが、その時、役ができていなければ、プレイを継続します。
また、より高い役を目指す場合は、Aボタンをクリックせずに、Bボタンでカードを破棄することができます。ただし、5回を超えてカードを変えた時点で、役を自動判定し、ゲームは終了します。
環境
環境としては、下記を使用します。
1.PC
・Windows 10
・Scratch オフラインエディタ 2.0
・Python 3.6 (Anaconda を利用すればより簡単だと思います)
・Arduino IDE 1.8.5 (ビルドができれば、他のバージョンでもOKだと思います)
なお、事前に Gamebuino ライブラリをインストールしておいてください。
2.Gamebuino
PCとUSBケーブルで接続したまま、シリアル通信でやり取りをします。もし、PCに他のシリアル通信機器が接続してある場合には、あらかじめ外しておいてください。
Gamebuino
Gamebuino に、PCとのシリアル通信、カード表示、メッセージ表示、サウンド鳴、ボタンクリックなどの機能を提供するスケッチを格納します。
任意のフォルダに、次のソースを格納してください。
//================================================
// Carduino
//
// Gamebuino: Arduino UNO compatible
//------------------------------------------------
#include <SPI.h>
#include <Gamebuino.h>
#include "GBtones.h"
#include "Cardset.h"
//------------------------------------------------
// card info
//------------------------------------------------
struct CardInfo {
int suit;
int number;
};
//------------------------------------------------
// define
//------------------------------------------------
#define GAME_TITLE "Carduino"
#define VERSION 1
//------------------------------------------------
// button
//------------------------------------------------
#define PRC_UP 0
#define PRC_RIGHT 1
#define PRC_DOWN 2
#define PRC_LEFT 3
#define PRC_A 4
#define PRC_B 5
//------------------------------------------------
#define CNT_MESS 200
#define LEN_MESS 12
#define SX_MESS 10
#define SY_MESS 10
//------------------------------------------------
#define ID_CHANNEL 0
#define SND_SLOW 8
#define SND_FAST 2
#define DLY_SLOW 12
#define DLY_FAST 3
//------------------------------------------------
//#define RAD_BASE 7
#define NUM_CARD 5
#define WID_CARD 16
#define HGH_CARD 24
#define WID_SUIT 10
#define HGH_SUIT 9
#define WID_NUMBER 10
#define HGH_NUMBER 9
#define WID_CURSOR 10
#define HGH_CURSOR 4
#define SX_SUIT 3
#define SY_SUIT 2
#define SX_NUMBER 3
#define SY_NUMBER 13
#define SX_CURSOR 4
#define SY_CURSOR 25
#define CNT_CURSOR 20
//------------------------------------------------
#define COM_SPEED 57600
#define CMD_LENGTH 3
//------------------------------------------------
// music
//------------------------------------------------
const int p00[] = { NOTE_C4, 2, NOTE_E4, 2, NOTE_G4, 2 ,NOTE_C4, 2, NOTE_E4, 2, NOTE_G4, 2 ,0 };
const int p01[] = { NOTE_G4, 2, NOTE_E4, 2, NOTE_C4, 2 ,NOTE_G4, 2, NOTE_E4, 2, NOTE_C4, 2 ,0 };
const int p02[] = { NOTE_C4, 2, NOTE_D4, 2, NOTE_E4, 2 ,NOTE_F4, 2, NOTE_G4, 2, NOTE_A4, 2 ,0 };
const int p03[] = { NOTE_C5, 2, NOTE_G5, 2, NOTE_C5, 2 ,NOTE_G5, 2, NOTE_C5, 2, NOTE_G5, 2 ,0 };
const int p04[] = { NOTE_C5, 4, NOTE_E5, 2, NOTE_G5, 2 ,NOTE_C5, 4, NOTE_E5, 2, NOTE_G5, 2 ,0 };
//------------------------------------------------
// font
//------------------------------------------------
extern const byte font5x7[]; // get the default large font
//------------------------------------------------
// field
//------------------------------------------------
Gamebuino pb; // Gamebuino instance
boolean paused = false;
boolean flgCursor = false;
boolean flgMusic = false;
int cntProc = 0;
int numProc = 0;
int inxProc = 0;
int inxBttn = 0;
int inxCursor = 0;
int cntCursor = 0;
int cntMess = 0;
int* pntMusic = 0;
int cntMusic = 0;
//------------------------------------------------
// Gamebuino: 84 x 48
//------------------------------------------------
int crdX[] = { 0, 17, 34, 51, 68 };
int crdY[] = { 19, 19, 19, 19, 19 };
int* musicSet[] = { p00, p01, p02, p03, p04 };
bool btnPressed = false;
String ms; // message
CardInfo cf[NUM_CARD]; // cards
//------------------------------------------------
// setup
//------------------------------------------------
void setup() {
// serial communication
Serial.begin(COM_SPEED);
// initialize Gamebuino object
pb.begin();
//change the font to the large one
pb.display.setFont (font5x7);
// display title and main menu
initialize();
//hide the battery indicator
pb.battery.show = false;
// frame count
pb.frameCount = LCDWIDTH * 8;
pb.setFrameRate (30);
// sound command
pb.sound.command(CMD_INSTRUMENT, 1, 0, 3);
pb.sound.setVolume(7);
}
//------------------------------------------------
// loop
//------------------------------------------------
void loop() {
// from scratch
procCommand();
// game board procedure
if(pb.update()){
// sound proc
procButton();
// show header
showHeader();
// scan check
cntProc++;
if (cntProc > numProc - 1) {
cntProc = 0;
inxProc = -1;
}
// sound
procSound();
// show message
showMess();
// show card
showCard();
// blink cursor
blinkCursor();
// end of game ?
if(pb.buttons.pressed(BTN_C)) {
pb.sound.stopNote();
showTitle();
initialize();
}
}
}
//************************************************
// functions
//------------------------------------------------
// read command
//------------------------------------------------
String readCommand() {
String s;
// read string from serial port
if (Serial.available()) {
s = Serial.readString();
s.replace("\n", " ");
s.replace("\r", " ");
s.trim();
}
return s;
}
//------------------------------------------------
// write status
//------------------------------------------------
void writeStatus(String str) {
Serial.println(str);
}
//------------------------------------------------
// command process
//------------------------------------------------
void procCommand() {
// read command from PC
String s = readCommand();
if (s.length() > 0) {
String c = s.substring(0, CMD_LENGTH); // get command
String t = s.substring(CMD_LENGTH); // get option
// parse command
do {
if (c.equals("INQ")) {
cmdINQ(t);
break;
}
if (c.equals("CRD")) {
cmdCards(t);
break;
}
if (c.equals("MES")) {
cmdMess(t);
break;
}
if (c.equals("SND")) {
cmdSound(t);
break;
}
} while (false);
}
}
//---------------------------------------------------
// INQ
//---------------------------------------------------
void cmdINQ(String str) {
String s = String("REP");
s.concat(String(VERSION));
writeStatus(s);
}
//------------------------------------------------
// messages
//------------------------------------------------
void cmdMess(String str) {
ms = str;
cntMess = CNT_MESS;
}
//------------------------------------------------
// show message
//------------------------------------------------
void showMess() {
if (cntMess > 0) {
cntMess--;
pb.display.cursorX = SX_MESS;
pb.display.cursorY = SY_MESS;
pb.display.print (ms);
}
}
//------------------------------------------------
// distribute cards
//
// s1nn1s2nn2s3nn3s4nn4s5nn5
// s=0: card none
//------------------------------------------------
void cmdCards(String str) {
int s, n;
int i, j;
for (i = 0; i < NUM_CARD; i++) {
s = getSuit (str);
n = getNumber(str);
cf[i].suit = s;
cf[i].number = n;
}
}
//------------------------------------------------
// get suit
//------------------------------------------------
int getSuit(String& str) {
String s = str.substring(0, 1);
int n = SUIT_NONE;
int t;
str = str.substring(1);
if (s.equals("C"))
n = SUIT_CLUB;
else if (s.equals("H"))
n = SUIT_HEART;
else if (s.equals("S"))
n = SUIT_SPADE;
else if (s.equals("D"))
n = SUIT_DIAMOND;
return n;
}
//------------------------------------------------
// get number
//------------------------------------------------
int getNumber(String& str) {
String s = str.substring(0, 1);
int n = 0;
do {
if (s.equals("1")) {
String t = str.substring(1, 2);
if (t.equals("0") || t.equals("1") || t.equals("2") || t.equals("3")) {
s = str.substring(0, 2);
str = str.substring(2);
break;
}
}
str = str.substring(1);
} while (false);
if (s.equals("A"))
n = 0;
else if (s.equals("J"))
n = NUM_J;
else if (s.equals("Q"))
n = NUM_Q;
else if (s.equals("K"))
n = NUM_K;
else
n = s.toInt() - 1;
return n;
}
//------------------------------------------------
// show card
//------------------------------------------------
void showCard() {
int i;
for (i = 0; i < NUM_CARD; i++) {
if (cf[i].suit != SUIT_NONE) {
drawCard(crdX[i], crdY[i], cf[i].suit, cf[i].number);
}
}
}
//------------------------------------------------
// draw card
// suit:
// 0:Club, 1:Heart, 2:Spade, 3:Diamond
//------------------------------------------------
void drawCard(int sx, int sy, int suit, int number) {
uint8_t* p;
// set color
pb.display.setColor(7);
// outer line
pb.display.drawRect (sx, sy, WID_CARD, HGH_CARD);
p = pgm_read_word_near (suitSet + suit);
drawImage (sx + SX_SUIT, sy + SY_SUIT, HGH_NUMBER, p);
p = pgm_read_word_near (numberSet + number);
drawImage (sx + SX_NUMBER, sy + SY_NUMBER, HGH_NUMBER, p);
}
//------------------------------------------------
// blink cursor
//------------------------------------------------
void blinkCursor() {
cntCursor++;
if (cntCursor > CNT_CURSOR - 1) {
cntCursor = 0;
flgCursor = !flgCursor;
}
if (flgCursor) {
drawCursor(crdX[inxCursor], crdY[inxCursor]);
}
}
//------------------------------------------------
// left cursor
//------------------------------------------------
void leftCursor() {
if (inxCursor > 0)
inxCursor--;
}
//------------------------------------------------
// right cursor
//------------------------------------------------
void rightCursor() {
if (inxCursor < NUM_CARD - 1)
inxCursor++;
}
//------------------------------------------------
// draw cursor
//------------------------------------------------
void drawCursor(int sx, int sy) {
drawImage (sx + SX_CURSOR, sy + SY_CURSOR, HGH_CURSOR, pCursor);
}
//------------------------------------------------
// draw image
//------------------------------------------------
void drawImage(int sx, int sy, int hy, uint16_t pt) {
uint16_t im, mk;
int x, y;
for (y = sy; y < sy + hy; y++) {
im = pgm_read_byte_near (pt++);
im <<= 8;
im += pgm_read_byte_near (pt++);
mk = 0x200;
for (x = sx; x < sx + WID_SUIT; x++) {
if (im & mk) {
pb.display.drawPixel(x, y) ;
}
mk >>= 1;
}
}
}
//------------------------------------------------
// button procedure
//------------------------------------------------
void procButton() {
scanButton();
if (inxBttn > -1) {
if (!btnPressed) {
btnPressed = true;
// set button index
inxProc = inxBttn;
cntProc = 0;
// process
btnProc();
}
}
else {
if (btnPressed) {
String s = "BTNN" + String(inxCursor + 1);
writeStatus (s);
btnPressed = false;
}
}
}
//------------------------------------------------
// scan button
//------------------------------------------------
void scanButton() {
inxBttn = -1;
if (pb.buttons.repeat(BTN_UP, 1)) {
inxBttn = PRC_UP;
}
if (pb.buttons.repeat(BTN_RIGHT, 1)) {
inxBttn = PRC_RIGHT;
}
if (pb.buttons.repeat(BTN_DOWN, 1)) {
inxBttn = PRC_DOWN;
}
if (pb.buttons.repeat(BTN_LEFT, 1)) {
inxBttn = PRC_LEFT;
}
if (pb.buttons.repeat(BTN_A, 1)) {
inxBttn = PRC_A;
}
if (pb.buttons.repeat(BTN_B, 1)) {
inxBttn = PRC_B;
}
}
//------------------------------------------------
// button process
//------------------------------------------------
void btnProc() {
String s;
switch (inxProc) {
case PRC_RIGHT:
rightCursor();
pb.sound.playNote(NOTE_C4, SND_FAST, 0);
numProc = DLY_FAST;
break;
case PRC_LEFT:
leftCursor();
pb.sound.playNote(NOTE_C4, SND_FAST, 0);
numProc = DLY_FAST;
break;
case PRC_UP:
// transmission
pb.sound.playNote(NOTE_E4, SND_FAST, 0);
s = "BTNU" + String(inxCursor + 1);
writeStatus (s);
break;
case PRC_DOWN:
// transmission
pb.sound.playNote(NOTE_E4, SND_FAST, 0);
s = "BTND" + String(inxCursor + 1);
writeStatus (s);
break;
case PRC_A:
// transmission
pb.sound.playNote(NOTE_G4, SND_FAST, 0);
s = "BTNA" + String(inxCursor + 1);
writeStatus (s);
break;
case PRC_B:
// transmission
pb.sound.playNote(NOTE_G4, SND_FAST, 0);
s = "BTNB" + String(inxCursor + 1);
writeStatus (s);
break;
}
}
//------------------------------------------------
// sound command
//------------------------------------------------
void cmdSound(String str) {
int i = str.toInt() - 1;
pntMusic = musicSet[i];
pb.sound.playNote(*pntMusic++, *pntMusic, 0);
cntMusic = *pntMusic++ + 2;
}
//------------------------------------------------
// sound process
//------------------------------------------------
void procSound() {
if (cntMusic > 0) {
cntMusic--;
if ((cntMusic == 0) && (*pntMusic)) {
pb.sound.playNote(*pntMusic++, *pntMusic, 0);
cntMusic = *pntMusic++ + 2;
}
}
}
//------------------------------------------------
// display title and set random seed
//------------------------------------------------
void showTitle() {
// display title and main menu
pb.titleScreen (F(GAME_TITLE));
//pick a different random seed each time for games to be different
pb.pickRandomSeed();
}
//------------------------------------------------
// initialization
//------------------------------------------------
void initialize() {
for (int i = 0; i < NUM_CARD; i++) {
cf[i].suit = SUIT_NONE;
cf[i].number = 0;
}
}
//------------------------------------------------
// show header
//------------------------------------------------
void showHeader() {
pb.display.print(F(GAME_TITLE));
}
また、下記の2つのヘッダファイルを同一フォルダに格納してください。
//------------------------------------------------
// cards
//------------------------------------------------
#define SUIT_CLUB 0
#define SUIT_HEART 1
#define SUIT_SPADE 2
#define SUIT_DIAMOND 3
#define SUIT_NONE 99
#define NUM_A 0
#define NUM_2 1
#define NUM_3 2
#define NUM_4 3
#define NUM_5 4
#define NUM_6 5
#define NUM_7 6
#define NUM_8 7
#define NUM_9 8
#define NUM_10 9
#define NUM_J 10
#define NUM_Q 11
#define NUM_K 12
//------------------------------------------------
// card images
//------------------------------------------------
// Club
const uint8_t pClub[] PROGMEM = {
B00000000, B01111000,
B00000000, B11111100,
B00000000, B11111100,
B00000001, B01111010,
B00000011, B11111111,
B00000011, B11111111,
B00000001, B10110110,
B00000000, B00110000,
B00000000, B11111100
};
// Heart
const uint8_t pHeart[] PROGMEM = {
B00000001, B10000110,
B00000011, B11001111,
B00000011, B11111111,
B00000011, B11111111,
B00000011, B11111111,
B00000000, B11111100,
B00000000, B01111000,
B00000000, B00110000,
B00000000, B00110000
};
// Spade
const uint8_t pSpade[] PROGMEM = {
B00000000, B00110000,
B00000000, B00110000,
B00000000, B01111000,
B00000001, B11111110,
B00000011, B11111111,
B00000011, B11111111,
B00000001, B10110110,
B00000000, B00110000,
B00000000, B11111100
};
// Diamond
const uint8_t pDiamond[] PROGMEM = {
B00000000, B00110000,
B00000000, B00110000,
B00000000, B01111000,
B00000000, B11111100,
B00000011, B11111111,
B00000000, B11111100,
B00000000, B01111000,
B00000000, B00110000,
B00000000, B00110000
};
// A
const uint8_t pCA[] PROGMEM = {
B00000000, B01111000,
B00000000, B01001000,
B00000000, B10000100,
B00000000, B10000100,
B00000001, B00000010,
B00000001, B11111110,
B00000010, B00000001,
B00000010, B00000001,
B00000010, B00000001
};
// 2
const uint8_t pC2[] PROGMEM = {
B00000000, B11111100,
B00000001, B00000010,
B00000010, B00000001,
B00000000, B00000001,
B00000000, B00111110,
B00000001, B11000000,
B00000010, B00000000,
B00000010, B00000000,
B00000011, B11111111
};
// 3
const uint8_t pC3[] PROGMEM = {
B00000000, B11111100,
B00000001, B00000010,
B00000010, B00000001,
B00000000, B00000010,
B00000000, B00111100,
B00000000, B00000010,
B00000010, B00000001,
B00000001, B00000010,
B00000000, B11111100
};
// 4
const uint8_t pC4[] PROGMEM = {
B00000000, B00001100,
B00000000, B00010100,
B00000000, B00100100,
B00000000, B01000100,
B00000000, B10000100,
B00000001, B00000100,
B00000011, B11111111,
B00000000, B00000100,
B00000000, B00000100
};
// 5
const uint8_t pC5[] PROGMEM = {
B00000011, B11111111,
B00000010, B00000000,
B00000010, B00000000,
B00000010, B01111100,
B00000011, B10000010,
B00000000, B00000001,
B00000000, B00000001,
B00000010, B00000010,
B00000001, B11111100
};
// 6
const uint8_t pC6[] PROGMEM = {
B00000000, B11111110,
B00000001, B00000001,
B00000010, B00000000,
B00000010, B01111100,
B00000011, B10000010,
B00000010, B00000001,
B00000010, B00000001,
B00000001, B00000010,
B00000000, B11111100
};
// 7
const uint8_t pC7[] PROGMEM = {
B00000011, B11111111,
B00000010, B00000001,
B00000010, B00000010,
B00000000, B00000100,
B00000000, B00001000,
B00000000, B00010000,
B00000000, B00100000,
B00000000, B00100000,
B00000000, B00100000
};
// 8
const uint8_t pC8[] PROGMEM = {
B00000000, B11111100,
B00000001, B00000010,
B00000010, B00000001,
B00000001, B00000010,
B00000000, B11111100,
B00000001, B00000010,
B00000010, B00000001,
B00000001, B00000010,
B00000000, B11111100
};
// 9
const uint8_t pC9[] PROGMEM = {
B00000000, B11111100,
B00000001, B00000010,
B00000010, B00000001,
B00000010, B00000001,
B00000001, B00000011,
B00000000, B11111101,
B00000000, B00000001,
B00000010, B00000010,
B00000001, B11111100
};
// 10
const uint8_t pC10[] PROGMEM = {
B00000001, B00011110,
B00000011, B00100001,
B00000001, B00100001,
B00000001, B00100001,
B00000001, B00100001,
B00000001, B00100001,
B00000001, B00100001,
B00000001, B00100001,
B00000011, B10011110
};
// J
const uint8_t pCJ[] PROGMEM = {
B00000000, B00011111,
B00000000, B00000100,
B00000000, B00000100,
B00000000, B00000100,
B00000000, B00000100,
B00000011, B10000100,
B00000001, B00000100,
B00000001, B00000100,
B00000000, B11111000
};
// Q
const uint8_t pCQ[] PROGMEM = {
B00000000, B11111100,
B00000001, B00000010,
B00000010, B00000001,
B00000010, B00000001,
B00000010, B00000001,
B00000010, B01111001,
B00000010, B10000110,
B00000001, B00000100,
B00000000, B11111011
};
// K
const uint8_t pCK[] PROGMEM = {
B00000011, B10000011,
B00000001, B00000010,
B00000001, B00000100,
B00000001, B00001000,
B00000001, B11110000,
B00000001, B00001000,
B00000001, B00000100,
B00000001, B00000010,
B00000011, B10000011
};
// cursor
const uint8_t pCursor[] PROGMEM = {
B00000000, B00110000,
B00000000, B00110000,
B00000000, B11001100,
B00000001, B10000110
};
//------------------------------------------------
// card set
//------------------------------------------------
const uint8_t* const suitSet[] PROGMEM = { pClub, pHeart, pSpade, pDiamond };
const uint8_t* const numberSet[] PROGMEM = { pCA, pC2, pC3, pC4, pC5, pC6, pC7, pC8, pC9, pC10, pCJ, pCQ, pCK };
#ifndef GAMEBUINO_TONES
#define GAMEBUINO_TONES
#define NOTE_AS2 0
#define NOTE_B2 1
#define NOTE_C3 2
#define NOTE_CS3 3
#define NOTE_D3 4
#define NOTE_DS3 5
#define NOTE_E3 6
#define NOTE_F3 7
#define NOTE_FS3 8
#define NOTE_G3 9
#define NOTE_GS3 10
#define NOTE_A3 11
#define NOTE_AS3 12
#define NOTE_B3 13
#define NOTE_C4 14
#define NOTE_CS4 15
#define NOTE_D4 16
#define NOTE_DS4 17
#define NOTE_E4 18
#define NOTE_F4 19
#define NOTE_FS4 20
#define NOTE_G4 21
#define NOTE_GS4 22
#define NOTE_A4 23
#define NOTE_AS4 24
#define NOTE_B4 25
#define NOTE_C5 26
#define NOTE_CS5 27
#define NOTE_D5 28
#define NOTE_DS5 29
#define NOTE_E5 30
#define NOTE_F5 31
#define NOTE_FS5 32
#define NOTE_G5 33
#define NOTE_GS5 34
#define NOTE_A5 35
#define NOTE_AS5 36
#define NOTE_B5 37
#define NOTE_C6 38
#define NOTE_CS6 39
#define NOTE_D6 40
#define NOTE_DS6 41
#define NOTE_E6 42
#define NOTE_F6 43
#define NOTE_FS6 44
#define NOTE_G6 45
#define NOTE_GS6 46
#define NOTE_A6 47
#define NOTE_AS6 48
#define NOTE_B6 49
#define NOTE_C7 50
#define NOTE_CS7 51
#define NOTE_D7 52
#define NOTE_DS7 53
#define NOTE_E7 54
#define NOTE_F7 55
#define NOTE_FS7 56
#define NOTE_G7 57
#define NOTE_GS7 58
#define NOTE_NONE 63
#endif
これらのソースを格納したら、ArduinoIDEでビルドし、Gamebuinoへ書き込んでください。
なお、 PCとGamebuino 間のコマンドシーケンスは、次の通りです。
1.PCからGamebuinoへ
- INQ
バージョンを確認する。
- CRDsnnsnnsnnsnnsnn (1枚目から5枚目までのスートと数字)
カードを配布する。
s:スート
C:クラブ
H:ハート
S:スペード
D:ダイアモンド
N:カードなし(ダミーの数字が必要)
n:数字 A、2、 ・・・、10、J、Q、K
- MESmessage
メッセージを5秒程度表示する
- SNDn
1秒程度音を鳴らす。 n:パターン1 ~ 5
2.GamebuinoからPCへ
- REP1
INQ に対する回答
- BTNbn
ボタンが押された
b:ボタン種別
U:上
D:下
A:A
B:B
n:ボタンが押された時のカーソルの位置 1 ~ 5
Scratch
Scratch とGamebuino は、Scratch の拡張機能で接続します。
流れとしては、Scratch に追加した拡張ブロックから Pythonで作成したサーバを経由して、Gamebuinoと連携します。
拡張ブロックとサーバ間のコマンドシーケンスは、次の通りです。
- dist_card
カードを配布する。
- show_message
メッセージを表示する。
- play_beep
ビープを鳴らす。
- read_button
押されたボタンを取得する。
- read_position
ボタンが押された時のカーソルの位置を取得する。
Scratchからのコマンドシーケンスを受け取ったサーバは、Gamebuinoのコマンドシーケンスに変換して送信、または受信を行います。
1.フォルダ構成
サーバを実行するためには、任意のフォルダに、以下のフォルダ構成を作成し、それぞれのファイルを格納します。(- で始まるものがフォルダです)
なお、__init__.py は空のファイルです。
-carduino
-s2carduino
-configuration
__init__.py
carduino.cfg
-poker4alone
poker4alone.sb2
-ScratchFiles
-ExtensionDescriptors
__init__.py
s2carduino_JA.s2e
__init__.py
__main__.py
s2carduino_serial.py
2.Config
Python で作成したサーバの定義情報を格納します。
scratch_project を指定すると、Scratchを起動する際に自動起動します。なお、現状では日本語のみに対応しています。
[scratch_info]
scratch_language = 2
windows_wait_time = 3
scratch_executable = default
# scratch_project =
3.拡張ブロック
Scratchのプロジェクトとサーバ間の橋渡しをするために追加したブロックです。
現状は、日本語のみに対応しています。
また、先頭のボード番号は、複数のGamebuinoが接続された場合に、区別するためのものです。
今回のプロジェクトでは、複数のGamebuinoには対応していません。
{
"extensionName": "s2carduino",
"extensionPort": 50209,
"url": "http://localhost",
"blockSpecs": [
[
"",
"ボード%m.board のカードを %s %n %s %n %s %n %s %n %s %n にする",
"dist_card",
"0",
"N",
"0",
"N",
"0",
"N",
"0",
"N",
"0",
"N",
"0"
],
[
"",
"ボード%m.board に %s を表示",
"show_message",
"0",
"message"
],
[
"",
"ボード%m.board のビープ %m.pattern を鳴らす",
"play_beep",
"0",
"1"
],
[
"r",
"ボード%m.board のボタンを読む",
"read_button",
"0"
],
[
"r",
"ボード%m.board の位置を読む",
"read_position",
"0"
],
],
"menus": {
"board": [
"0",
"1",
"2",
"3"
],
"suit": [
"C",
"H",
"S",
"D",
"N"
],
"number": [
"A",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"J",
"Q",
"K"
],
"pattern": [
"1",
"2",
"3",
"4",
"5"
]
}
}
4.Scratch プロジェクト
Scratch プロジェクトについては、画像を参照してください。
各ブロックは、次の機能を持ちます。
0)リスト
リストは、固定長で使用しているので、作成時に長さを確保しておく必要があります。
- SUIT_BASE:サーバへのコマンドシーケンスで使用するスート文字へ変換するためのリストです。
値は、あらかじめ格納しておく必要があります。
- SUIT:Gamebuinoへ配布したカードのスートを格納します。
- NUMBER:Gamebuinoへ配布したカードの番号を格納します。
- STOCK_SUIT:山札のスートを格納します。
- STOCK_NUMBER:山札の番号を格納します。
- TEMP:役の検査をする際などに一時的に使用します。
リストの6番目には、14を事前に格納しておく必要があります。(ストレートの検査用です)
- USED:山を作る際に、山に積み上げ済みか否かをチェックするために使用します。
- COUNTER:役を検査する際に、同一番号のカードの枚数を格納します。
1)開始ブロック
緑色の旗がクリックされると処理を開始します。
- カードの積み上げで、山を作ります。
- カードの配布で、山の上から5枚をGamubuinoへ配布します。
- カードをGamebuinoに表示します。
- ボタン検査を使って、Gamebuinoのボタンがクリックされたか否かを検査します。もし、クリックされた場合は、下記の処理を行います。
Aボタンがクリックされると、役の検査を行います。
もし役ができていると、役を表示して処理を終了します。
Bボタンがクリックされると、カーソルの位置にあるカードを破棄して、山の次の1枚を渡します。
- カード交換(破棄と配布)が5回を超えた場合、役の判定を行い処理を終了します。
2)カード積上げ
52枚のカードをランダムに山に積みあげます。
ただし、すべてのカードを積みあげると時間がかかりすぎるため、使用する。11枚のみを積みあげます。
3)カード配布
カード分配を5回呼び出すことにより、山からカードを5枚配布します。
4)カード分配
山から1枚カードを取り出し、Gamebuinoのリストへ格納します。
5)カード表示
Gamebuinoのカードリスト(SUIT、NUMBER)に格納されているカードを拡張ブロックを使用してGamebuinoへ渡します。
6)ボタン検査
拡張ブロックからクリックされたボタンとクリックされた際の位置を読み取ります。
ここでは、ボタンが押されたままの場合も1回と判定するための処理をしています。
7)役検査
Gamebuino のカード(SUIT、NUMBERリスト)の役を検査します。
役は、ストレート、フラッシュ。4カード、フルハウス、3カード、2ペア、1ペアです。
(ロイヤルストレートフラッシュは、未確認です。)
8)整列
NUMBERリストの内容をTEMPリストへ複製したのち、昇順に整列します。
9)ストレート
TEMPリストの値が順番に並んでいるかどうかを検査します。
もし、1番目がAで、2番目が10の場合は、10、J、Q、K、A の順番になっているかどうかを検査します。
10)フラッシュ
SUITリストがすべて同じであるかどうかを検査します。
11)カウント
TEMPリストの値ごとの個数をカウントしてCOUNTリストへ格納します。
12)ペア
COUNTリストの内容から、4カード、フルハウス、3カード、2ペア、1ペアを判定します。
13)役決定
役を検査した結果から、最終的な役を決定します。
5.サーバ
Python で作成したサーバは、本体とシリアル通信用のプログラムの2本があります。
1)サーバ機能
コンフィグ情報を読込み、接続されているGamebuinoを有効なシリアルポートから検索します。
もし、コンフィグファイルにScratch のプロジェクトが指定されていた場合には、起動します。
サーバは、Scratchからのリクエストを受けるスレッドとScratchからの応答を受け取るスレッドからなります。
Scratchの拡張ブロックからのリクエストは、Webサーバにて受け取り、リクエストを受けると、Gamebuino へのコマンドシーケンスを組み立てて、シリアル通信で送信します。
別のスレッドでは、Gamebuinoからの応答があるとScratchのコマンドシーケンスをキューイングし、Scratchからのポーリング時に、拡張ブロックへ渡します。
2)通信機能
通信機能は、サーバ機能からの要求によりGamebuinoとシリアル通信を行います。
初期処理用の同期処理とScratchとの通信時に使用する非同期処理があります。
#!/usr/bin/env python3
#=============================================================================
# Carduino for scrach & arduino game board
#-----------------------------------------------------------------------------
import argparse
import asyncio
import configparser
import os
import os.path
import re
import signal
import subprocess
import sys
import time
import webbrowser
from aiohttp import web
import s2carduino_serial as ss
from socket import socket, AF_INET, SOCK_STREAM
HOST = 'localhost'
PORTWEB = 50209
PORTSCK = 51000
MAX_MESSAGE = 200
OK = "ok".encode('utf-8')
the_loop = None
# import pdb; pdb.set_trace() ### break point
#*****************************************************************************
# Class
#-----------------------------------------------------------------------------
class S2CARDUINO:
#-----------------------------------------------------------------------------
# initialize
#-----------------------------------------------------------------------------
def __init__(self):
print('s2carduino version 1.0 - 2019/1/1')
# variables
self.poll_reply = ''
self.poll_time_stamp = 0.0
self.loop = None
self.carduino = []
try:
# set carduino board com port
self.com_port = ss.CarduinoSerial.get_ports()
if len(self.com_port) == 0:
print ('no available communication port')
sys.exit(0)
for p in self.com_port:
b = ss.CarduinoSerial(com_port = p)
# 1'st communication for carduino
b.writeString('INQ')
rep = b.readString()
self.carduino.append(b)
print('carduino %s version:%s\n' % (p, rep[3:]))
# set base path
self.base_path = os.getcwd()
# get configuration
config = configparser.ConfigParser()
config_file_path = str(self.base_path + '\\configuration\\carduino.cfg')
config.read(config_file_path, encoding = 'utf8')
# get language
scratch_block_language_dict = { '1':'s2carduino.s2e', '2':'s2carduino_JA.s2e'}
self.scratch_language = config.get('scratch_info', 'scratch_language')
# get wait time
self.windows_wait_time = int(config.get('scratch_info', 'windows_wait_time'))
# get scratch path
self.scratch_executable = config.get('scratch_info', 'scratch_executable')
if self.scratch_executable == 'default':
self.scratch_executable = '"C:\\Program Files (x86)\\Scratch 2\\Scratch 2.exe"'
# get scratch project path
s = config.get('scratch_info', 'scratch_project', fallback=None)
if s == None:
self.scratch_project = ''
else:
self.scratch_project = self.base_path + '\\' + s
except Exception as ex:
print (ex)
sys.exit(0)
#-----------------------------------------------------------------------------
# serial procedure
#-----------------------------------------------------------------------------
async def proc_serial(self, my_loop, inx):
try:
while True:
data = await self.carduino[inx].readline()
self.serial_input(inx, data)
except Exception as ex:
print (ex)
print ('end of procedure\n')
print ('Please, press Control-C')
#-----------------------------------------------------------------------------
# serial input
#-----------------------------------------------------------------------------
def serial_input(self, n, m):
if m[0:3] == 'BTN':
self.poll_reply += 'read_button/' + str(n) + ' ' + str(m[3:4]) + '\n'
self.poll_reply += 'read_position/' + str(n) + ' ' + str(m[4:5]) + '\n'
#-----------------------------------------------------------------------------
# kick off
#-----------------------------------------------------------------------------
async def kick_off(self, my_loop):
global START
self.loop = my_loop
try:
# start up the HTTP server
app = web.Application(loop = my_loop)
#-----------------------------------------------------------------------------------------
# command handlers
#-----------------------------------------------------------------------------------------
app.router.add_route('GET', '/dist_card/{board}/{s1}/{n1}/{s2}/{n2}/{s3}/{n3}/{s4}/{n4}/{s5}/{n5}', self.dist_cards)
app.router.add_route('GET', '/show_message/{board}/{value}', self.show_message)
app.router.add_route('GET', '/play_beep/{board}/{value}', self.play_beep)
app.router.add_route('GET', '/poll', self.poll)
# wake up web server
srv = await my_loop.create_server(app.make_handler(), HOST, PORTWEB)
# start scratch
if self.scratch_executable:
if self.scratch_project:
os_string = self.scratch_executable + ' ' + self.scratch_project
else:
os_string = self.scratch_executable
await asyncio.sleep(self.windows_wait_time)
subprocess.Popen(os_string)
print('=== s2carduino started (port: %s) ===' % PORTWEB)
await self.keep_alive()
return srv
except Exception as ex:
print ('** An error occurred: ')
print (ex)
self.loop.stop()
the_loop.stop()
pass
#-----------------------------------------------------------------------------
# distribute cards
#-----------------------------------------------------------------------------
async def dist_cards(self, request):
board = self.get_board(request)
s1 = request.match_info['s1']
n1 = request.match_info['n1']
s2 = request.match_info['s2']
n2 = request.match_info['n2']
s3 = request.match_info['s3']
n3 = request.match_info['n3']
s4 = request.match_info['s4']
n4 = request.match_info['n4']
s5 = request.match_info['s5']
n5 = request.match_info['n5']
try:
str = 'CRD' + s1 + n1 + s2 + n2 + s3 + n3 + s4 + n4 + s5 + n5
await self.carduino[board].writeline(str)
except ValueError:
print ('problem dist_cards\n')
return web.Response(body = OK)
return web.Response(body = OK)
#-----------------------------------------------------------------------------
# show message
#-----------------------------------------------------------------------------
async def show_message(self, request):
board = self.get_board(request)
value = request.match_info['value']
try:
str = 'MES' + value
await self.carduino[board].writeline(str)
except ValueError:
print ('show_message\n')
return web.Response(body = OK)
return web.Response(body = OK)
#-----------------------------------------------------------------------------
# play beep
#-----------------------------------------------------------------------------
async def play_beep(self, request):
board = self.get_board(request)
value = request.match_info['value']
try:
str = 'SND' + value
await self.carduino[board].writeline(str)
except ValueError:
print ('play_beep\n')
return web.Response(body = OK)
return web.Response(body = OK)
#-----------------------------------------------------------------------------
# get board number
#-----------------------------------------------------------------------------
def get_board(self, req):
n = int(req.match_info['board'])
if n >= len(self.carduino):
n = 0
return n
#-----------------------------------------------------------------------------
# poll
#-----------------------------------------------------------------------------
async def poll(self, request):
# refresh the poll watch dog timer
self.poll_time_stamp = self.loop.time()
# save the reply to a temporary variable
total_reply = self.poll_reply
# clear the poll reply string for the next reply set
self.poll_reply = ''
# send the HTTP response
return web.Response(headers = {"Access-Control-Allow-Origin": "*"}, content_type = "text/html", charset = "ISO-8859-1", text = total_reply)
#-----------------------------------------------------------------------------
# noinspection
#-----------------------------------------------------------------------------
async def keep_alive(self):
while True:
await asyncio.sleep(1)
#-----------------------------------------------------------------------------
# Control-C handling
def signal_handler(signal, frame):
print('Control-C detected.')
for t in asyncio.Task.all_tasks(the_loop):
t.cancel()
the_loop.run_until_complete(asyncio.sleep(.1))
the_loop.stop()
the_loop.close()
sys.exit(0)
#*****************************************************************************
# main procedure
#-----------------------------------------------------------------------------
def main():
# listen for SIGINT
signal.signal(signal.SIGINT, signal_handler)
# create instance
s2carduino = S2CARDUINO()
# event loop
global the_loop
the_loop = asyncio.get_event_loop()
try:
asyncio.ensure_future(s2carduino.kick_off (the_loop))
for i in range(len(s2carduino.com_port)):
asyncio.ensure_future(s2carduino.proc_serial (the_loop, i))
the_loop.run_forever()
except:
sys.exit(0)
asyncio.sleep(2)
#*****************************************************************************
# default procedure
#-----------------------------------------------------------------------------
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
sys.exit(0)
# noinspection PyBroadException
loop = asyncio.get_event_loop()
try:
loop.run_forever()
loop.stop()
loop.close()
except:
pass
# -*- coding: utf-8 -*-
#=================================================================================
# Scratch to Carduino interface
#=================================================================================
import asyncio
import sys
import serial
from serial.tools import list_ports
from time import sleep
#*****************************************************************************
# class
#-----------------------------------------------------------------------------
class CarduinoSerial:
#-----------------------------------------------------------------------------
# define
#-----------------------------------------------------------------------------
SER_SPEED = 57600
SER_SLEEP = 0.001
#-----------------------------------------------------------------------------
# get serial port
#-----------------------------------------------------------------------------
@staticmethod
def get_ports():
ports = list_ports.comports() # get ports
l = []
for i in ports:
l.append(i[0]) # get port name
if len(l) == 0:
print ("no comport")
sys.exit(1)
return l
#-----------------------------------------------------------------------------
# constructor
#-----------------------------------------------------------------------------
def __init__(self, com_port = 'COM12'):
print('Initializing now\n')
self.my_serial = serial.Serial(com_port, self.SER_SPEED, timeout = 2, writeTimeout = 2)
self.com_port = com_port
self.sleep_tune = self.SER_SLEEP
sleep(3)
#-----------------------------------------------------------------------------
# sync write to serial
#-----------------------------------------------------------------------------
def writeString(self, str):
self.my_serial.write(bytes(str, 'UTF-8'))
#-----------------------------------------------------------------------------
# sync read from serial
#-----------------------------------------------------------------------------
def readString(self):
s = self.my_serial.readline()
if len(s) > 0:
rslt = str(s, 'UTF-8')
rslt = rslt.rstrip('\n')
rslt = rslt.rstrip('\r')
else:
rslt = ""
return rslt
#-----------------------------------------------------------------------------
# arync write to serial
#-----------------------------------------------------------------------------
async def writeline(self, data):
rslt = self.my_serial.write(bytes(data, 'UTF-8'))
return rslt
#-----------------------------------------------------------------------------
# async read from serial
#-----------------------------------------------------------------------------
async def readline(self):
future = asyncio.Future()
data_available = False
while True:
if not data_available:
if not self.my_serial.inWaiting():
await asyncio.sleep(self.sleep_tune)
else:
data_available = True
data = self.my_serial.readline()
rslt = str(data, 'UTF-8')
rslt = rslt.rstrip('\n')
rslt = rslt.rstrip('\r')
future.set_result(rslt)
else:
if not future.done():
await asyncio.sleep(self.sleep_tune)
else:
return future.result()
プロジェクトの実行
Scratchプロジェクトを実行するには、以下の手順で実施してください。
1)PCとGamebuino をUSBケーブルで接続する。
2)Gamebuino の電源を入れる。
3)PC のPython で、main.py を実行する。
例えば、Anaconda のコンソールを起動して、s2carduinoフォルダへ移動して、下記コマンドを実行する。
>Python __main__.py
4)Scratch からScratch プロジェクトを読込実行する。
緑色の旗をクリックする。
5)Gamebuinoを操作する。
4つある青いボタンの中段の2つのボタンをクリックして、カーソルを左右に動かします。
赤いBボタンをクリックするとカーソル位置のカードが破棄され、新たなカードが配布されます。
緑のAボタンをクリックすると役ができているかどうかを判定します。もし、役ができていれば、役を表示しゲーム終了です。
役ができていなければ、ゲームは続行されます。(このとき、特に何も表示しません)
5回を超えて、カードを交換すると役を判定して、ゲーム終了です。この時、役ができていなければ Game Over を表示します。
再度、実行する際には、Scratch の緑の旗をクリックしてください。
さいごに
ひととり、動作はしましたが、処理速度に難があることがわかりした。(想定はしていましたが)
特に、リスト処理において、途中で条件が確定した場合でも最後まで回さざるを得ないため、無駄な時間がかかっていると思います。
BREAKブロックが欲しいところです。また、変数やリストの内容をリアルタイムに表示させているのも処理速度低下の原因だと思います。
(ここには隠すことができますが、まとめてはできないようなので、あえて試しませんでした)
なお、今回の実験において、下記および関連するサイトを参考にさせていただきました。
https://pypi.org/project/s2aio/
https://memakura.github.io/dialogsystem/