After the previous project, I distributed the LED
strip into strips of 11 LEDs and made a square with a diffuser. I wanted to continue with a series of simple LED control projects using an EMG signal
. The idea of these projects is to expand the range of applications of EMG sensors
for art, games and control of things. But this time it's uMyo EMG device using only its IMU sensors part!
Dmitry Dziuba undertook the implementation of Tetris
on this LED field. First he wrote the firmware for the game itself - which seems simple, but actually is not so much. When the Tetris engine was completed, we tried several control approaches - in particular we tried to rotate pieces with muscle command (works great, but the problem is that muscle activation changes arm shape, gyro picks it up and often shifts piece left or right, completely unexpectedly), use muscle activation to drop a piece (works nice but too often the next piece is immediately dropped afterwards), use up/down motion to drop it (completely non-intuitive!) - and ended up with what's on the video: we used only IMU part of the sensor, where arm rotation produces piece rotation, arm motion produces piece motion :)
The control process itself is interesting here - to feel the boundaries of the field, turn the pieces over, set them in the right place. It's like magic! The keyboard, mouse, joystick make control a routine unconscious process, and when you use your arm position, it’s as if there are no intermediaries between you and the object, and control comes from your body.
#include "tetris_eng.h"
sTet cur_tet;
int tet_rotate(sTet *tet, sTField *field, uint8_t cw_rot)
{
uint8_t nrot;
if(cw_rot) nrot = (tet->rot + 1)%4;
else
{
if(tet->rot == 0) nrot = 3;
else nrot = tet->rot - 1;
}
int nx[4], ny[4];
if(tet->type == tet_sq)
{
for(int n = 0; n < 4; n++) nx[n] = n%2, ny[n] = n/2;
}
if(tet->type == tet_line)
{
if(nrot == 0 || nrot == 2) for(int n = 0; n < 4; n++) nx[n] = -1 + n, ny[n] = 0;
if(nrot == 1 || nrot == 3) for(int n = 0; n < 4; n++) nx[n] = 0, ny[n] = -1 + n;
}
if(tet->type == tet_L || tet->type == tet_L2 || tet->type == tet_T)
{
if(nrot == 0 || nrot == 2)
{
for(int n = 0; n < 3; n++) nx[n] = -1 + n, ny[n] = 0;
if(tet->type == tet_L) nx[3] = 1 - nrot, ny[3] = 1 - nrot;
if(tet->type == tet_L2) nx[3] = nrot - 1, ny[3] = 1 - nrot;
if(tet->type == tet_T) nx[3] = 0, ny[3] = 1 - nrot;
}
if(nrot == 1 || nrot == 3)
{
for(int n = 0; n < 3; n++) nx[n] = 0, ny[n] = -1 + n;
if(tet->type == tet_L2) ny[3] = 2 - nrot, nx[3] = 2 - nrot;
if(tet->type == tet_L) ny[3] = nrot - 2, nx[3] = 2 - nrot;
if(tet->type == tet_T) ny[3] = 0, nx[3] = 2 - nrot;
}
}
if(tet->type == tet_z || tet->type == tet_z2)
{
if(nrot == 0 || nrot == 2)
{
nx[0] = 0, ny[0] = 0;
nx[1] = 1, ny[1] = 0;
nx[2] = 1, nx[3] = 0;
if(tet->type == tet_z) ny[2] = 1, ny[3] = -1;
else ny[2] = -1, ny[3] = 1;
}
if(nrot == 1 || nrot == 3)
{
nx[0] = 0, ny[0] = 0;
nx[1] = 0, ny[1] = 1;
ny[2] = 1, ny[3] = 0;
if(tet->type == tet_z) nx[2] = -1, nx[3] = 1;
else nx[2] = 1, nx[3] = -1;
}
}
int done = 0;
int dx_try = 0;
int dy_try = 0;
int attempt = 0;
int place_ok = 0;
while(1)
{
if(attempt == 1) dx_try = 1;
if(attempt == 2) dx_try = -1;
if(attempt == 3) dx_try = 2;
if(attempt == 4) dx_try = -2;
if(attempt > 4)
{
break;
// if(dy_try == 0) dy_try = 1, dx_try = 0, attempt = 0;
// else break;
}
attempt++;
place_ok = 1;
for(int n = 0; n < 4; n++)
{
int bx = tet->cx + dx_try;
int by = tet->cy + dy_try;
bx += nx[n];
by += ny[n];
if(by >= field->SY) continue; //higher than top, no problem
if(bx < 0 || bx >= field->SX || by < 0)
{
place_ok = 0;
break;
}
if(field->field[by * field->SX + bx])
{
place_ok = 0;
break;
}
}
if(place_ok) break;
}
if(!place_ok) return 0;
tet->rot = nrot;
tet->cx += dx_try;
tet->cy += dy_try;
for(int n = 0; n < 4; n++) tet->bx[n] = nx[n], tet->by[n] = ny[n];
return 1;
}
int tet_move(sTet *tet, sTField *field, int dir)
{
int dx, dy;
if(dir == move_down) dx = 0, dy = -1;
else if(dir == move_left) dx = -1, dy = 0;
else if(dir == move_right) dx = 1, dy = 0;
else if(dir == move_up) dx = 0, dy = 1;
else return 0; //unknown move -> failure
int place_ok = 1;
for(int n = 0; n < 4; n++)
{
int bx = tet->cx + tet->bx[n] + dx;
int by = tet->cy + tet->by[n] + dy;
if(by >= field->SY)
{
if(bx < 0 || bx >= field->SX)
{
place_ok = 0;
break;
}
continue; //higher than top, no problem
}
if(bx < 0 || bx >= field->SX || by < 0)
{
place_ok = 0;
break;
}
if(field->field[by * field->SX + bx])
{
place_ok = 0;
break;
}
}
if(place_ok)
{
tet->cx += dx;
tet->cy += dy;
return 1;
}
return 0;
}
int tet_place(sTet *tet, sTField *field)
{
for(int n = 0; n < 4; n++)
{
int bx = tet->cx + tet->bx[n];
int by = tet->cy + tet->by[n];
if(by >= field->SY) continue; //higher than top - nothing to do
if(bx < 0 || bx >= field->SX || by < 0) return 0; //error, shouldn't happen, random behavior is ok
field->field[by * field->SX + bx] = tet->type;
}
return 1;
}
uint32_t tet_check_field(sTField *field)
{
uint32_t res = 0;
for(int y = 0; y < field->SY; y++)
{
int full_line = 1;
for(int x = 0; x < field->SX; x++)
if(!field->field[y * field->SX + x]) {full_line = 0; break;}
if(full_line) res |= 1<<y;
}
return res;
}
void tet_remove_full(sTField *field)
{
for(int y = 0; y < field->SY; y++)
{
int full_line = 1;
for(int x = 0; x < field->SX; x++)
if(!field->field[y * field->SX + x]) {full_line = 0; break;}
if(full_line)
{
for(int y2 = y; y2 < field->SY-1; y2++)
{
for(int x = 0; x < field->SX; x++)
field->field[y2*field->SX + x] = field->field[(y2+1)*field->SX + x];
}
for(int x = 0; x < field->SX; x++)
field->field[(field->SY-1) * field->SX + x] = 0;
y--;
}
}
}
void tet_init(sTField *field)
{
for(int x = 0; x < field->SX*field->SY; x++)
field->field[x] = 0;
}
int tet_add_shape(sTField *field, uint8_t type)
{
if(type == 0) //random
type = 200 + rand()%7;
cur_tet.type = type;
cur_tet.cx = field->SX/2;
cur_tet.cy = field->SY-1;
cur_tet.rot = 3;
return tet_rotate(&cur_tet, field, 1);
}
uint32_t prev_game_ms = 0;
uint8_t game_state = game_waitstart;
uint32_t game_prev_act = 0;
void tet_game_moveto(sTField *field, int tgt_x, int tgt_y, int tgt_rot)
{
if(game_state != game_waitstep) return;
int need_move_x = 1;
int need_move_y = 1;
int need_rot = 1;
if(tgt_x == -1234) need_move_x = 0;
if(tgt_y == -1234) need_move_y = 0;
if(tgt_rot == -1234) need_rot = 0;
tgt_x += field->SX/2;
if(tgt_x < 0) tgt_x = 0;
if(tgt_x >= field->SX) tgt_x = field->SX-1;
int attempts = 0;
if(need_rot)
while(cur_tet.rot != tgt_rot && attempts++ < 5) tet_rotate(&cur_tet, field, 1);
attempts = 0;
if(need_move_x)
{
while(cur_tet.cx != tgt_x && attempts++ < field->SX)
{
if(cur_tet.cx > tgt_x)
tet_move(&cur_tet, field, move_left);
else
tet_move(&cur_tet, field, move_right);
}
}
}
void tet_game_step(sTField *field, int req_action)
{
uint32_t ms = millis();
if(ms - prev_game_ms < 10) return;
prev_game_ms = ms;
if(game_state == game_waitstart)
{
game_prev_act = ms;
// if(req_action != action_start) return;
tet_init(field);
game_state = game_needfigure;
return;
}
if(game_state == game_needfigure)
{
int res = tet_add_shape(field, 0);
if(res) game_state = game_waitstep;
else game_state = game_over;
game_prev_act = ms;
return;
}
if(game_state == game_over)
{
if(ms < game_prev_act + field->over_time) return;
tet_init(field);
game_state = game_waitstart;
game_prev_act = ms;
return;
}
if(game_state == game_waitblow)
{
if(ms < game_prev_act + field->blow_time) return;
tet_remove_full(field);
game_state = game_needfigure;
game_prev_act = ms;
return;
}
if(game_state == game_dropend)
{
if(ms < game_prev_act + field->blow_time) return;
game_state = game_figend;
game_prev_act = ms;
return;
}
if(game_state == game_figend)
{
if(tet_check_field(field)) game_state = game_waitblow;
else game_state = game_needfigure;
game_prev_act = ms;
return;
}
if(game_state == game_waitstep)
{
if(req_action != action_none)
{
if(req_action == action_drop)
{
while(1)
{
int moved_free = tet_move(&cur_tet, field, move_down);
if(!moved_free)
{
tet_place(&cur_tet, field);
game_state = game_dropend;
game_prev_act = ms;
break;
}
}
}
if(req_action == action_left) tet_move(&cur_tet, field, move_left);
if(req_action == action_right) tet_move(&cur_tet, field, move_right);
if(req_action == action_cw) tet_rotate(&cur_tet, field, 1);
if(req_action == action_ccw) tet_rotate(&cur_tet, field, 0);
}
if(ms < game_prev_act + field->step_time) return;
int moved_free = tet_move(&cur_tet, field, move_down);
if(!moved_free && ms > game_prev_act + field->place_time)
{
tet_place(&cur_tet, field);
game_state = game_figend;
}
if(moved_free)
game_prev_act = ms;
return;
}
}
void tet_draw_array(sTField *field, uint8_t *rgb_out)
{
uint32_t ms = millis();
if(game_state == game_over)
{
int phase = (ms - game_prev_act) * 1000 / field->over_time;
phase = 500 - phase;
if(phase < 0) phase = -phase;
phase = 500 - phase;
int full_lines = field->SY * phase / 500;
for(int x = 0; x < field->SX; x++)
for(int y = 0; y < field->SY; y++)
{
int r = 2, g = 0, b = 0;
if(y <= full_lines) r = 30, g = 30, b = 30;
int idx = 3*(y*field->SX + x);
rgb_out[idx] = r;
rgb_out[idx+1] = g;
rgb_out[idx+2] = b;
}
return;
}
for(int x = 0; x < field->SX; x++)
for(int y = 0; y < field->SY; y++)
{
int r = 2, g = 0, b = 0;
int type = field->field[y*field->SX + x];
if(type != 0) r = 15, g = 5, b = 20;
int idx = 3*(y*field->SX + x);
rgb_out[idx] = r;
rgb_out[idx+1] = g;
rgb_out[idx+2] = b;
}
for(int n = 0; n < 4; n++)
{
int xx = cur_tet.cx + cur_tet.bx[n];
int yy = cur_tet.cy + cur_tet.by[n];
if(xx >= 0 && xx < field->SX && yy >= 0 && yy < field->SY)
{
int r = 10, g = 20, b = 5;
int idx = 3*(yy*field->SX + xx);
rgb_out[idx] = r;
rgb_out[idx+1] = g;
rgb_out[idx+2] = b;
}
}
if(game_state == game_waitblow)
{
uint32_t mask = tet_check_field(field);
int phase = (ms - game_prev_act) * 1000 / field->blow_time;
for(int x = 0; x < field->SX; x++)
for(int y = 0; y < field->SY; y++)
{
if(mask & 1<<y)
{
int r = 2, g = 0, b = 0;
if(phase < 700)
r = 5 + phase/10, g = 5 + phase/10, b = 5 + phase/10;
else
r = g = b = 76 - (phase-700)/4;
int idx = 3*(y*field->SX + x);
rgb_out[idx] = r;
rgb_out[idx+1] = g;
rgb_out[idx+2] = b;
}
}
}
}
#include <Arduino.h>
enum
{
tet_line = 200,
tet_L,
tet_L2,
tet_T,
tet_sq,
tet_z,
tet_z2,
tet_none
};
enum
{
move_down = 0,
move_left,
move_right,
move_up
};
enum
{
game_waitstart = 0,
game_needfigure,
game_waitstep,
game_figdrop,
game_figend,
game_dropend,
game_waitblow,
game_over
};
enum
{
action_none = 0,
action_start,
action_left,
action_right,
action_cw,
action_ccw,
action_drop
};
typedef struct sTField
{
uint8_t *field;
int SX;
int SY;
int step_time;
int drop_time;
int place_time;
int blow_time;
int over_time;
};
typedef struct sTet
{
uint8_t type;
uint8_t rot;
int bx[4];
int by[4];
uint8_t cx;
uint8_t cy;
};
int tet_rotate(sTet *tet, sTField *field, uint8_t cw_rot);
int tet_move(sTet *tet, sTField *field, int dir);
int tet_place(sTet *tet, sTField *field);
uint32_t tet_check_field(sTField *field);
void tet_remove_full(sTField *field);
void tet_game_moveto(sTField *field, int tgt_x, int tgt_y, int tgt_rot);
void tet_game_step(sTField *field, int req_action);
void tet_draw_array(sTField *field, uint8_t *rgb_out);
#include <Arduino.h>
#include <uMyo_BLE.h>
#include <FastLED.h>
#include "tetris_eng.h"
#define NUM_LEDS 122
#define DATA_PIN 13
#define TETR_SX 11
#define TETR_SY 11
CRGB leds[NUM_LEDS];
uint8_t tetr_field[TETR_SX*TETR_SY];
uint8_t rgb_states[TETR_SX*TETR_SY*3];
sTField field;
void setup() {
field.field = tetr_field;
field.SX = TETR_SX;
field.SY = TETR_SY;
field.step_time = 250;
field.place_time = 800;
field.drop_time = 200;
field.blow_time = 600;
field.over_time = 3000;
Serial.begin(115200);
uMyo.begin();
FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS); // GRB ordering is assumed
for(int x = 0; x < NUM_LEDS; x++)
{
if(x%6 == 0) leds[x] = CRGB(5, 0, 0);
if(x%6 == 1) leds[x] = CRGB(0, 5, 0);
if(x%6 == 2) leds[x] = CRGB(0, 0, 5);
if(x%6 == 3) leds[x] = CRGB(5, 5, 0);
if(x%6 == 4) leds[x] = CRGB(0, 5, 5);
if(x%6 == 5) leds[x] = CRGB(5, 0, 5);
}
FastLED.show();
delay(1000);
tet_game_step(&field, action_start);
}
float angle_diff(float a1, float a2)
{
float diff = a1 - a2;
if(diff > 3.1415926) diff = -2*3.1415926 + diff;
if(diff < -3.1415926) diff = 2*3.1415926 + diff;
return diff;
}
uint32_t prev_ms = 0;
float avg_roll1 = 0;
float proc_roll1 = 0;
int rot_act_state = 0;
float loc_max = 100;
float zero_roll = 0;
float zero_yaw = 0;
int zero_inited = 0;
void loop() {
float pitch1 = 0, pitch2 = 0;
float level1 = 0, level2 = 0;
float roll1 = 0, roll2 = 0;
float yaw1 = 0, yaw2 = 0;
int dev_cnt = uMyo.getDeviceCount();
if(dev_cnt > 0)
{
level1 = uMyo.getAverageMuscleLevel(0);
roll1 = uMyo.getRoll(0);
pitch1 = uMyo.getPitch(0);
yaw1 = uMyo.getYaw(0);
if(!zero_inited)
{
zero_roll = roll1;
zero_yaw = yaw1;
zero_inited = 1;
}
zero_yaw *= 0.9995;
zero_yaw += 0.0005*yaw1;
}
Serial.printf("Y %g P %g R %g\n", yaw1, pitch1, roll1);
uint32_t ms = millis();
static float last_roll_sw = 0;
float roll_scale = 0.15;
float droll = roll1 - last_roll_sw;
static int roll_state = 0;
int cur_action = action_none;
static uint32_t prev_roll_tm = 0;
if(droll > roll_scale && cur_action == action_none)
{
cur_action = action_ccw;
roll_state--;
last_roll_sw = roll1;
}
if(droll < -roll_scale && cur_action == action_none)
{
cur_action = action_cw;
roll_state++;
last_roll_sw = roll1;
}
static float last_yaw_sw = 0;
float dyaw = angle_diff(yaw1, last_yaw_sw);
float yaw_scale = 0.03;
static int yaw_state = 0;
if(dyaw > yaw_scale && cur_action == action_none)
{
cur_action = action_right;
if(yaw_state < 7)
yaw_state++;
last_yaw_sw += yaw_scale;// yaw1;
}
if(dyaw < -yaw_scale && cur_action == action_none)
{
cur_action = action_left;
if(yaw_state > -7)
yaw_state--;
last_yaw_sw -= yaw_scale;//yaw1;
}
int tgt_x = yaw_state;
int tgt_y = -1234;
int tgt_rot = roll_state;
while(tgt_rot < 0) tgt_rot += 4;
tgt_rot %= 4;
tet_game_step(&field, cur_action);
tet_draw_array(&field, rgb_states);
leds[0] = 0;
for(int l = 0; l < field.SX*field.SY; l++)
{
int x = 10 - l/11;
int y = l%11;
if(x%2) y = 10 - y;
int r, g, b;
int idx = y*11 + x;
r = rgb_states[idx*3];
g = rgb_states[idx*3+1];
b = rgb_states[idx*3+2];
leds[1+l] = CRGB(r, g, b);
}
FastLED.show();
delay(5);
}
Comments