macsbug

PACMAN with M5STACK

with 4 comments

PACMAN ゲームを M5STACK FACES に移植しました。     2018.03.07


.
Pacman は Arduino Due 用に Github CrazyLabz! の DrNCXCortex氏 が作成しました。
YouTube:Pacman on Arduino Due
github:Pacman Game for Arduino Due
その ソースを基に @Tw_Mhage氏 が ESP32 + 240×320 ILI9328 TFT Display へ移植しました。
Qiita:ESP32でゲームを作ってみた:MhageGH / esp32_ILI9328_Pacman

移植方法:@Tw_Mhage氏の esp32_ILI9328_Pacman を M5STACK FACES用に変更します。
スケッチで注目する技術は 画像表示に たった1行の命令で構成されている事です。
DrNCXCortex氏 と @Tw_Mhage氏 に 感謝致します。


.
準備:
M5Stack Store
M5STACK BASIC :$35.00
M5STACK FACES Pocket Computer:$89.90 ( 9498円:03/07 1$ 105.6円)。
スケッチ:esp32_ILI9328_Pacman を ダウンロードします。


.
構成:
M5STACK FACES と GameBoy Panel を使用します。
原作の画像は 320 (縦) x 240 ( 横 ) です。FACES は 240 (縦) x 320 ( 横 ) です。
FACES を縦に使用しますと 画像の縦横の変更が必要になる為に FACES を横にします。
操作は START, SELECT , CROSS key で行います。
DEMO モードがあり ボタン操作しなくとも デモ画面を見る事ができます。

FACES でなく M5STACK Basic の A,B,C ボタンでも可能ですが 3つのボタンで
6つの操作をする為にスケッチの工夫が必要になります。
方法は esp32 snake with M5STACK を参照下さい。


.
移植:以下の8つを変更します。
_ esp32_ILI9328_Pacman.ino:変更あり。
_ DrawindexedMap.h:画像表示ルーチン:変更あり。
_ centsc.h , carpal.h, font8x8.cpp:変更なし。

変更:esp32_ILI9328_Pacman.ino:
1:追加1:24行目

#include <M5Stack.h>

2:削除1:25-27行を削除:

const char ssid[] = "ESP32";
const char password[] = "esp32pass";
const int localPort = 10000;

3:削除2:53-55行を削除:

#include <SPI.h>
#include <WiFi.h>
#include <WiFiUdp.h>

4:削除3:57-61行を削除:

#define CS 5
#define RESET 17
ili9328SPI tft(CS, RESET);
WiFiUDP udp;

5:変更1:1394-1402行:setup を M5STACK用に変更します。

void setup() {
  randomSeed(analogRead(0));
  Serial.begin(115200);
  M5.begin();
  Wire.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(TFT_BLACK);
  pinMode(5, INPUT_PULLUP);
  delay(100);
}

6:変更2:1394-1402行:KeyPadLoop を FACES GameBoy 用に 以下2-14を追加します。

void KeyPadLoop(){
  char r;
  if(digitalRead(5) == LOW) {
    Wire.requestFrom(0X88, 1);         // request 1 byte from keyboard
    while (Wire.available()) { 
      uint8_t key_val = Wire.read();   // receive a byte as character
      if ( key_val == 191) { r = 'z';} // select
      if ( key_val == 127) { r = 'x';} // start
      if ( key_val == 247) { r = '8';} // up
      if ( key_val == 251) { r = '2';} // down
      if ( key_val == 254) { r = '4';} // left
      if ( key_val == 253) { r = '6';} // right
    }
  }
  if (r == 'z') { ClearKeys(); but_A=true; delay(300); } //else but_A=false;
  if (r == 'x') { ClearKeys(); but_B=true; delay(300); } //else but_B=false;
  if (r == '8') { ClearKeys(); but_UP=true; }  //else but_UP=false;
  if (r == '2') { ClearKeys(); but_DOWN=true; }  //else but_DOWN=false;
  if (r == '4') { ClearKeys(); but_LEFT=true; }   //else but_LEFT=false;
  if (r == '6') { ClearKeys(); but_RIGHT=true; }  //else but_RIGHT=false;
}

変更:DrawindexedMap.h:
7:削除4:16行目を削除:#include “ili9328.h”

8:変更3:46-61行:
_ word を uint16_t に変更。
_ TFT 表示:tft->drawFastHLine1 を M5.Lcd.drawFastHLine に変更します。
_ 注目:画像表示処理は 9行目の「drawFastHLine」1行で行われています。

void drawIndexedmap(uint8_t* indexmap, int16_t x, uint16_t y) {
  byte i = 0;
  uint16_t color = _paletteW[indexmap[0]];
  for (byte tmpY = 0; tmpY < 8; tmpY++) {
    byte width = 1;
    for (byte tmpX = 0; tmpX < 8; tmpX++) {
      uint16_t next_color = _paletteW[indexmap[++i]];
      if ((color != next_color && width >= 1) || tmpX == 7) {
        M5.Lcd.drawFastHLine(x + tmpX - width, y + tmpY, width, color);
        color = next_color;
        width = 0;
      }
      width++;
    }
  }
}

.
Gameboy Panel:
Gameboy Panel は ATmega328P で構成され I2C で通信しています。
アドレスは 0x88 です。

参考:
M5-hardware: M5-hardware/FACES/GameBoy.ino:U,D,L,R,A,B,s,S が定義されています。
Modules/FACES/I2C_Keyboard/:
例:CROSS Key の上は BIN = 0xFE, DEC = 254, ASCii = U です。


.
価格:
2月28日から スイッチサイエンスより M5STACK が発売されました。
_ 4640円。(4490+150):スイッチサイエンス
_ 海外との価格の比較は以下で 最安値の販売店より 1252円 程高いです。
_ AliExpress 最安値:$30.50 ( 3238円 03/09 1$ 106.1円換算 )
_ $30.50 ( 3238円 03/09 1$ 106.1円換算 ):SHENGSUN SENOR Store
_ $30.50 ( 3238円 03/09 1$ 106.1円換算 ):BETTERSHENGSUN SHENGSUN Store
_ $30.88 ( 3278円 03/09 1$ 106.1円換算 ):CFSUNBIRD Store
_ $31.50 ( 3343円 03/09 1$ 106.1円換算 ):Shop3213115 Store
_ $35.00 ( 3715円 03/09 1$ 106.1円換算 ):M5Stack Store:本家

_ M5STACK FACES:M5Stack Store との価格差は 2536円 と高いです。
_  スイッチサイエンス:価格 12034円。
_   注意:SS の FACES には サンプルが書き込まれていません。
_  M5Stack Store:価格 $89.90 ( 9498円:03/07 1$ 105.6円)。
_   サンプルには マリオが 書き込まれています。
_   輸送期間:M5STACK STORE は 2月実績では 10日で届いています。

Maker Faire Shenzhen(深セン) 2017 での価格:M5STACK BASIC 2700円
_ Maker Faire Shenzhen(深セン) 2017に行ってきました その2
_  記事中の 3/4 あたりに記載されています。
_  COREのディベロッパーキットを買いました(158元、約2,700円)。


.
M5STACK COLLECTION:Github:Parts and Circuits
_ GitHub に M5STACK のスケッチを収集した場所があります。
_ 私が移植した M5Stack-SpaceShooter, M5Stack-Tetris が掲載されています。
_  gojimmypi, Kongduino, tomsuch へのリンクも掲載されており便利です。


.
感想:
M5STACK の ディスプレーとボタンの標準装備は 工作不要で大変便利です。
特に ゲーム用の GameBoy アダプターは 作るとなると手間と多大な時間を使用します
ので最高です。

原作:DrNCXCortex氏による PACMAN のリストから学ぶ物は多く 感嘆致します。
_ メインの esp32_ILI9328_Pacman.ino は 変数の処理に多大な内容を使用し、
_ グラフィック表示の為の DrawindexedMap.h は たった1行の 「drawFastHLine」
_ で行っています。つまりスムーズに動いている画像は 横書きの線で行われています。
_ 素晴らしいコードにより 移植を容易に行なう事ができました。

_ Pacman-Arduino-Due-master は 2014年に作成され 冒頭に以下の様に書かれています。
_ /* Copyright (c) 2014 Dr. NCX (mirracle.mxx@gmail.com) */
_ /* THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL */


.

スケッチ:PACMAN with M5STACK : 2018.03.08 Transplant by macsbug

//========================================================================
// PACMAN with M5STACK : 2018.03.08 Transplant by macsbug
// Controller : FACES / GameBoy
// Github     : https://macsbug.wordpress.com/2018/03/07/pacman-with-m5stack/
/******************************************************************************/
/*  PACMAN GAME FOR ARDUINO DUE                                               */
/******************************************************************************/
/*  Copyright (c) 2014  Dr. NCX (mirracle.mxx@gmail.com)                      */
/*                                                                            */
/* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL              */
/* WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED              */
/* WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR    */
/* BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES      */
/* OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,     */
/* WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,     */
/* ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS        */
/* SOFTWARE.                                                                  */
/*                                                                            */
/*  MIT license, all text above must be included in any redistribution.       */
/* 
 *  Controller configuration:
 *  Buttons UP, RIGHT, DOWN, LEFT, PAUSE and RESTART are each assigned 
 *   on characters '8', '6', '2', '4', 'x', 'z' 
 *   in the both case of SerialPort and WiFi UDP.
 */
// Pacman Game for Arduino Due
// https://github.com/DrNCXCortex/Pacman-Arduino-Due
// https://qiita.com/Tw_Mhage/items/ca9c52c52b4258ff27f1

#include <M5Stack.h>
#include "M5StackUpdater.h"
byte SPEED = 1; // 1=SLOW 2=NORMAL 4=FAST //do not try  other values!!!
/******************************************************************************/
/*   MAIN GAME VARIABLES                                                      */
/******************************************************************************/
#define BONUS_INACTIVE_TIME 600
#define BONUS_ACTIVE_TIME   300
#define START_LIFES 2
#define START_LEVEL 1
byte MAXLIFES = 5;
byte LIFES = START_LIFES;
byte GAMEWIN = 0;
byte GAMEOVER = 0;
byte DEMO = 1;
byte LEVEL = START_LEVEL;
byte ACTUALBONUS = 0;  //actual bonus icon
byte ACTIVEBONUS = 0;  //status of bonus
byte GAMEPAUSED = 0;
byte PACMANFALLBACK = 0;
#include "DrawindexedMap.h"
/******************************************************************************/
/*   Controll KEYPAD LOOP                                                     */
/******************************************************************************/
boolean but_A     = false;  //38
boolean but_B     = false;  //40
boolean but_UP    = false;  //52
boolean but_DOWN  = false;  //50
boolean but_LEFT  = false;  //48
boolean but_RIGHT = false;  //46
/******************************************************************************/
/*   GAME VARIABLES AND DEFINITIONS                                           */
/******************************************************************************/
#include "PacmanTiles.h"
//------------------------------------------------------------------------------/
enum GameState {
  ReadyState,
  PlayState,
  DeadGhostState, // Player got a ghost, show score sprite and only move eyes
  DeadPlayerState,
  EndLevelState
};
//------------------------------------------------------------------------------/
enum SpriteState{
  PenState,
  RunState,
  FrightenedState,
  DeadNumberState,
  DeadEyesState,
  AteDotState,    // pacman
  DeadPacmanState
};
//------------------------------------------------------------------------------/
enum {
  MStopped = 0,
  MRight   = 1,
  MDown    = 2,
  MLeft    = 3,
  MUp      = 4,
};
//------------------------------------------------------------------------------/
#define ushort uint16_t
#define BINKY  0
#define PINKY  1
#define INKY   2
#define CLYDE  3
#define PACMAN 4
#define BONUS  5
//------------------------------------------------------------------------------/
const byte _initSprites[] ={
  BINKY,  14,     17 - 3,   31, MLeft,
  PINKY,  14 - 2, 17,       79, MLeft,
  INKY,   14,     17,      137, MLeft,
  CLYDE,  14 + 2, 17,      203, MRight,
  PACMAN, 14,     17 + 9,    0, MLeft,
  BONUS,  14,     17 + 3,    0, MLeft,
};
//------------------------------------------------------------------------------/
//  Ghost colors
const byte _palette2[] ={
  0, 11, 1, 15, // BINKY red
  0, 11, 3, 15, // PINKY pink
  0, 11, 5, 15, // INKY cyan
  0, 11, 7, 15, // CLYDE brown
  0, 11, 9,  9, // PACMAN yellow
  0, 11,15, 15, // FRIGHTENED
  0, 11, 0, 15, // DEADEYES

  0, 1, 15,  2, // cherry
  0, 1, 15, 12, // strawberry
  0, 7,  2, 12, // peach
  0, 9, 15,  0, // bell

  0, 15,  1, 2, // apple
  0, 12, 15, 5, // grape
  0, 11,  9, 1, // galaxian
  0, 5, 15, 15, // key
};
//------------------------------------------------------------------------------/
const byte _paletteIcon2[] ={
  0, 9,  9, 9, // PACMAN
  0, 2, 15, 1, // cherry
  0, 12,15, 1, // strawberry
  0, 12, 2, 7, // peach
  0, 0, 15, 9, // bell
  0, 2, 15, 1, // apple
  0, 12,15, 5, // grape
  0, 1, 9, 11, // galaxian
  0, 5,15, 15, // key
};
//------------------------------------------------------------------------------/
#define PACMANICON 1
#define BONUSICON 2
#define FRIGHTENEDPALETTE 5
#define DEADEYESPALETTE 6
#define BONUSPALETTE 7
#define FPS 60
#define CHASE 0
#define SCATTER 1
#define DOT 7
#define PILL 14
#define PENGATE 0x1B
const byte _opposite[] = { MStopped, MLeft, MUp, MRight, MDown };
#define OppositeDirection(_x) pgm_read_byte(_opposite + _x)
const byte _scatterChase[]      = { 7, 20, 7, 20, 5, 20, 5, 0 };
const byte _scatterTargets[]    = { 2, 0, 25, 0, 0, 35, 27, 35 }; // inky/clyde scatter targets are backwards
//const char _pinkyTargetOffset[] = { 4, 0, 0, 4, -4, 0, -4, 4 }; // Includes pinky target bug
const char _pinkyTargetOffset[] = { 4, 0, 0, 4, 4, 0, 4, 4 };     // Includes pinky target bug
#define FRIGHTENEDGHOSTSPRITE 0
#define GHOSTSPRITE 2
#define NUMBERSPRITE 10
#define PACMANSPRITE 14
const byte _pacLeftAnim[]  = { 5, 6, 5, 4 };
const byte _pacRightAnim[] = { 2, 0, 2, 4 };
const byte _pacVAnim[]     = { 4, 3, 1, 3 };
word _BonusInactiveTimmer = BONUS_INACTIVE_TIME;
word _BonusActiveTimmer = 0;
/******************************************************************************/
/*   GAME - Sprite Class                                                      */
/******************************************************************************/
class Sprite{
  public:
    int16_t _x, _y;
    int16_t lastx, lasty;
    byte cx, cy;        // cell x and y
    byte tx, ty;        // target x and y
    SpriteState state;
    byte  pentimer;     // could be the same
    byte who;
    byte _speed;
    byte dir;
    byte phase;
    // Sprite bits
    byte palette2;  // 4->16 color map index
    byte bits;      // index of sprite bits
    signed char sy;
    //-------------------------------------------------------------------------/
    void Init(const byte* s){
      who = pgm_read_byte(s++);
      cx =  pgm_read_byte(s++);
      cy =  pgm_read_byte(s++);
      pentimer = pgm_read_byte(s++);
      dir = pgm_read_byte(s);
      _x = lastx = (int16_t)cx * 8 - 4;
      _y = lasty = (int16_t)cy * 8;
      state = PenState;
      _speed = 0;
      Target(random(20), random(20));
    }
    //-------------------------------------------------------------------------/
    void Target(byte x, byte y){
      tx = x;
      ty = y;
    }
    
    int16_t Distance(byte x, byte y){
      int16_t dx = cx - x;
      int16_t dy = cy - y;
      return dx * dx + dy * dy; // Distance to target
    }
    //-------------------------------------------------------------------------/
    //  once per sprite, not 9 times
    void SetupDraw(GameState gameState, byte deadGhostIndex){
      sy = 1;
      palette2 = who;
      byte p = phase >> 3;
      if (who == BONUS) {
        //BONUS ICONS
        bits = 21 + ACTUALBONUS;
        palette2 = BONUSPALETTE + ACTUALBONUS;
        return;
      }
      if (who != PACMAN){
        bits = GHOSTSPRITE + ((dir - 1) << 1) + (p & 1); // Ghosts
        switch (state){
          case FrightenedState:
            bits = FRIGHTENEDGHOSTSPRITE + (p & 1); // frightened
            palette2 = FRIGHTENEDPALETTE;
            break;
          case DeadNumberState:
            palette2 = FRIGHTENEDPALETTE;
            bits = NUMBERSPRITE + deadGhostIndex;
            break;
          case DeadEyesState:
            palette2 = DEADEYESPALETTE;
            break;
          default:
            ;
        }
        return;
      }
      //  PACMAN animation
      byte f = (phase >> 1) & 3;
      if (dir == MLeft)
        f = pgm_read_byte(_pacLeftAnim + f);
      else if (dir == MRight)
        f = pgm_read_byte(_pacRightAnim + f);
      else
        f = pgm_read_byte(_pacVAnim + f);
      if (dir == MUp)
        sy = -1;
      bits = f + PACMANSPRITE;
    }
    //-------------------------------------------------------------------------/
    //  Draw this sprite into the tile at x,y
    void Draw8(int16_t x, int16_t y, byte* tile){
      int16_t px = x - (_x - 4);
      if (px <= -8 || px >= 16) return;
      int16_t py = y - (_y - 4);
      if (py <= -8 || py >= 16) return;
      // Clip y
      int16_t lines = py + 8;
      if (lines > 16)
        lines = 16;
      if (py < 0){
        tile -= py * 8;
        py = 0;
      }
      lines -= py;
      //  Clip in X
      byte right = 16 - px;
      if (right > 8)
        right = 8;
      byte left = 0;
      if (px < 0){
        left = -px;
        px = 0;
      }
      //  Get bitmap
      signed char dy = sy;
      if (dy < 0)
        py = 15 - py;  // VFlip
      byte* data = (byte*)(pacman16x16 + bits * 64);
      data += py << 2;
      dy <<= 2;
      data += px >> 2;
      px &= 3;
      const byte* palette = _palette2 + (palette2 << 2);
      while (lines){
        const byte *src = data;
        byte d = pgm_read_byte(src++);
        d >>= px << 1;
        byte sx = 4 - px;
        byte x = left;
        do{
          byte p = d & 3;
          if (p){
            p = pgm_read_byte(palette + p);
            if (p)
              tile[x] = p;
          }
          d >>= 2;    // Next pixel
          if (!--sx){
            d = pgm_read_byte(src++);
            sx = 4;
          }
        } while (++x < right);
        tile += 8;
        data += dy;
        lines--;
      }
    }
};

/******************************************************************************/
/*   GAME - Playfield Class                                                   */
/******************************************************************************/
class Playfield{
    Sprite _sprites[5];
    Sprite _BonusSprite; //Bonus
    byte _dotMap[(32 / 4) * (36 - 6)];
    GameState _state;
    long    _score;             // 7 digits of score
    long    _hiscore;             // 7 digits of score
    long    _lifescore;
    signed char    _scoreStr[8];
    signed char    _hiscoreStr[8];
    byte    _icons[14];         // Along bottom of screen
    ushort  _stateTimer;
    ushort  _frightenedTimer;
    byte    _frightenedCount;
    byte    _scIndex;           //
    ushort  _scTimer;           // next change of sc status
    bool _inited;
    byte* _dirty;
  public:
    Playfield() : _inited(false)
    {
      //  Swizzle palette TODO just fix in place
//      byte * p = (byte*)_paletteW;
//      for (int16_t i = 0; i < 16; i++)
//      {
//        ushort w = _paletteW[i];    // Swizzle
//        *p++ = w >> 8;
//        *p++ = w;
//      }
    }
    //-------------------------------------------------------------------------/
    // Draw 2 bit BG into 8 bit icon tiles at bottom
    void DrawBG2(byte cx, byte cy, byte* tile){
      byte index = 0;
      signed char b = 0;
      index = _icons[cx >> 1];   // 13 icons across bottom
      if (index == 0){ memset(tile, 0, 64);return;}
      index--;
      index <<= 2;                        // 4 tiles per icon
      b = (1 - (cx & 1)) + ((cy & 1) << 1); // Index of tile
      const byte* bg = pacman8x8x2 + ((b + index) << 4);
      const byte* palette = _paletteIcon2 + index;
      byte x = 16;
      while (x--){
        byte bits = (signed char)pgm_read_byte(bg++);
        byte i = 4;
        while (i--){
          tile[i] = pgm_read_byte(palette + (bits & 3));
          bits >>= 2;
        }
        tile += 4;
      }
    }
    //-------------------------------------------------------------------------/
    byte GetTile(int16_t cx, int16_t ty){
      if (_state != ReadyState && ty == 20 && cx > 10 && cx < 17) {return (0);} //READY TEXT ZONE
      if (LEVEL % 5 == 1) return pgm_read_byte(playMap1 + ty * 28 + cx);
      if (LEVEL % 5 == 2) return pgm_read_byte(playMap2 + ty * 28 + cx);
      if (LEVEL % 5 == 3) return pgm_read_byte(playMap3 + ty * 28 + cx);
      if (LEVEL % 5 == 4) return pgm_read_byte(playMap4 + ty * 28 + cx);
      if (LEVEL % 5 == 0) return pgm_read_byte(playMap5 + ty * 28 + cx);
    }
    //-------------------------------------------------------------------------/
    // Draw 1 bit BG into 8 bit tile
    void DrawBG(byte cx, byte cy, byte* tile){
      if (cy >= 34) { //DRAW ICONS BELLOW MAZE
        DrawBG2(cx, cy, tile);
        return;
      }
      byte c = 11;
      if (LEVEL % 8 == 1) c = 11; // Blue
      if (LEVEL % 8 == 2) c = 12; // Green
      if (LEVEL % 8 == 3) c = 1;  // Red
      if (LEVEL % 8 == 4) c = 9;  // Yellow
      if (LEVEL % 8 == 5) c = 2;  // Brown
      if (LEVEL % 8 == 6) c = 5;  // Cyan
      if (LEVEL % 8 == 7) c = 3;  // Pink
      if (LEVEL % 8 == 0) c = 15; // White
      byte b = GetTile(cx, cy);
      const byte* bg;
      //  This is a little messy
      memset(tile, 0, 64);
      if (cy == 20 && cx >= 11 && cx < 17){
        if (DEMO == 1 && ACTIVEBONUS == 1) return;
        if ((_state != ReadyState && GAMEPAUSED != 1 && DEMO != 1) || ACTIVEBONUS == 1) b = 0; // hide 'READY!'
        else if (DEMO == 1 && cx == 11) b = 0;
        else if (DEMO == 1 && cx == 12) b = 'D';
        else if (DEMO == 1 && cx == 13) b = 'E';
        else if (DEMO == 1 && cx == 14) b = 'M';
        else if (DEMO == 1 && cx == 15) b = 'O';
        else if (DEMO == 1 && cx == 16) b = 0;
        else if (GAMEPAUSED == 1 && cx == 11) b = 'P';
        else if (GAMEPAUSED == 1 && cx == 12) b = 'A';
        else if (GAMEPAUSED == 1 && cx == 13) b = 'U';
        else if (GAMEPAUSED == 1 && cx == 14) b = 'S';
        else if (GAMEPAUSED == 1 && cx == 15) b = 'E';
        else if (GAMEPAUSED == 1 && cx == 16) b = 'D';
      }
      else if (cy == 1){
        if (cx < 7)
          b = _scoreStr[cx];
        else if (cx >= 10 && cx < 17)
          b = _hiscoreStr[cx - 10]; // HiScore
      } else {
        if (b == DOT || b == PILL){  // DOT==7 or PILL==16
          if (!GetDot(cx, cy))
            return;
          c = 14;
        }
        if (b == PENGATE)
          c = 14;
      }
      bg = playTiles + (b << 3);
      if (b >= '0')
        c = 15; // text is white
      for (byte y = 0; y < 8; y++){
        signed char bits = (signed char)pgm_read_byte(bg++);  ///WARNING CHAR MUST BE signed !!!
        byte x = 0;
        while (bits){
          if (bits < 0)
            tile[x] = c;
          bits <<= 1;
          x++;
        }
        tile += 8;
      }
    }
    //-------------------------------------------------------------------------/
    // Draw BG then all sprites in this cell
    void Draw(uint16_t x, uint16_t y, bool sprites){
      byte tile[8 * 8];
      //      Fill with BG
      if (y == 20 && x >= 11 && x < 17 && DEMO == 1 && ACTIVEBONUS == 1)  return;
      DrawBG(x, y, tile);
      //      Overlay sprites
      x <<= 3;
      y <<= 3;
      if (sprites){
        for (byte i = 0; i < 5; i++)
          _sprites[i].Draw8(x, y, tile);
        //AND BONUS
        if (ACTIVEBONUS) _BonusSprite.Draw8(x, y, tile);
      }
      //      Show sprite block
#if 0
      for (byte i = 0; i < 5; i++)
      {
        Sprite* s = _sprites + i;
        if (s->cx == (x >> 3) && s->cy == (y >> 3)){
          memset(tile, 0, 8);
          for (byte j = 1; j < 7; j++)
            tile[j * 8] = tile[j * 8 + 7] = 0;
          memset(tile + 56, 0, 8);
        }
      }
#endif
      x += (240 - 224) / 2;//x += -0;
      y += (320 - 288) / 2;//y += -26;
      //      Should be a direct Graphics call
      byte n = tile[0];
      byte i = 0;
      word color = (word)_paletteW[n];
      drawIndexedmap(tile, x, y); //******** drawIndexedmap(&tft, tile, x, y);
    }
    boolean updateMap [36][28];
    //  Mark tile as dirty (should not need range checking here)
    void Mark(int16_t x, int16_t y, byte* m){
      x -= 4;
      y -= 4;
      updateMap[(y >> 3)][(x >> 3)] = true;
      updateMap[(y >> 3)][(x >> 3) + 1] = true;
      updateMap[(y >> 3)][(x >> 3) + 2] = true;
      updateMap[(y >> 3) + 1][(x >> 3)] = true;
      updateMap[(y >> 3) + 1][(x >> 3) + 1] = true;
      updateMap[(y >> 3) + 1][(x >> 3) + 2] = true;
      updateMap[(y >> 3) + 2][(x >> 3)] = true;
      updateMap[(y >> 3) + 2][(x >> 3) + 1] = true;
      updateMap[(y >> 3) + 2][(x >> 3) + 2] = true;
    }
    //-------------------------------------------------------------------------/
    void DrawAllBG(){
      for (byte y = 0; y < 36; y++)
        for (byte x = 0; x < 28; x++) {
          Draw(x, y, false);
        }
    }
    //  Draw sprites overlayed on cells
    void DrawAll(){
      byte* m = _dirty;
      //  Mark sprite old/new positions as dirty
      for (byte i = 0; i < 5; i++){
        Sprite* s = _sprites + i;
        Mark(s->lastx, s->lasty, m);
        Mark(s->_x, s->_y, m);
      }
      // Mark BONUS sprite old/new positions as dirty
      Sprite* _s = &_BonusSprite;
      Mark(_s->lastx, _s->lasty, m);
      Mark(_s->_x, _s->_y, m);
      //  Animation
      for (byte i = 0; i < 5; i++)
        _sprites[i].SetupDraw(_state, _frightenedCount - 1);
       _BonusSprite.SetupDraw(_state, _frightenedCount - 1);
      for (byte tmpY = 0; tmpY < 36; tmpY++) {
        for (byte tmpX = 0; tmpX < 28; tmpX++) {
          if (updateMap[tmpY][tmpX] == true) Draw(tmpX, tmpY, true);
          updateMap[tmpY][tmpX] = false;
        }
      }
    }

    int16_t Chase(Sprite* s, int16_t cx, int16_t cy){
      while (cx < 0)      //  Tunneling
        cx += 28;
      while (cx >= 28)
        cx -= 28;
      byte t = GetTile(cx, cy);
      if (!(t == 0 || t == DOT || t == PILL || t == PENGATE))
        return 0x7FFF;

      if (t == PENGATE){
        if (s->who == PACMAN)
          return 0x7FFF;  // Pacman can't cross this to enter pen
        if (!(InPen(s->cx, s->cy) || s->state == DeadEyesState))
          return 0x7FFF;  // Can cross if dead or in pen trying to get out
      }   
      int16_t dx = s->tx - cx;
      int16_t dy = s->ty - cy;
      return (dx * dx + dy * dy); // Distance to target
    }
    //-------------------------------------------------------------------------/
    void UpdateTimers(){
      // Update scatter/chase selector, low bit of index indicates scatter
      if (_scIndex < 8){
        if (_scTimer-- == 0){
          byte duration = pgm_read_byte(_scatterChase + _scIndex++);
          _scTimer = duration * FPS;
        }
      }
      // BONUS timmer
      if (ACTIVEBONUS == 0 && _BonusInactiveTimmer-- == 0) {
        _BonusActiveTimmer = BONUS_ACTIVE_TIME; //5*FPS;
        ACTIVEBONUS = 1;
      }
      if (ACTIVEBONUS == 1 && _BonusActiveTimmer-- == 0) {
        _BonusInactiveTimmer = BONUS_INACTIVE_TIME; //10*FPS;
        ACTIVEBONUS = 0;
      }
      //  Release frightened ghosts
      if (_frightenedTimer && !--_frightenedTimer){
        for (byte i = 0; i < 4; i++){
          Sprite* s = _sprites + i;
          if (s->state == FrightenedState){
            s->state = RunState;
            s->dir = OppositeDirection(s->dir);
          }
        }
      }
    }
    //  Target closes pill, run from ghosts?
    
void PacmanAI(){
  Sprite* pacman;
  pacman = _sprites + PACMAN;
  //  Chase frightened ghosts
  Sprite* closestGhost = NULL;
  Sprite* frightenedGhost = NULL;
  Sprite* closestAttackingGhost = NULL;
  Sprite* DeadEyesStateGhost = NULL;
  int16_t dist = 0x7FFF;
  int16_t closestfrightenedDist = 0x7FFF;
  int16_t closestAttackingDist = 0x7FFF;
  for (byte i = 0; i < 4; i++){
        Sprite* s = _sprites + i;
        int16_t d = s->Distance(pacman->cx, pacman->cy);
        if (d < dist){
          dist = d;
          if (s->state == FrightenedState ) {
            frightenedGhost = s;
            closestfrightenedDist = d;
          }
          else {
            closestAttackingGhost = s;
            closestAttackingDist = d;
          }
          closestGhost = s;
          if ( s->state == DeadEyesState ) DeadEyesStateGhost = s;
        }
      }
      PACMANFALLBACK = 0;
      if (DEMO == 1 && !DeadEyesStateGhost && frightenedGhost ){
        pacman->Target(frightenedGhost->cx, frightenedGhost->cy);
        return;
      }

      // Under threat; just avoid closest ghost
      if (DEMO == 1 && !DeadEyesStateGhost && dist <= 32  && closestAttackingDist < closestfrightenedDist )
      {
        if (dist <= 16) {
          pacman->Target( pacman->cx * 2 - closestAttackingGhost->cx, pacman->cy * 2 - closestAttackingGhost->cy);
          PACMANFALLBACK = 1;
        } else {
          pacman->Target( pacman->cx * 2 - closestAttackingGhost->cx, pacman->cy * 2 - closestAttackingGhost->cy);
        }
        return;
      }
      if (ACTIVEBONUS == 1) {
        pacman->Target(13, 20);
        return;
      }
      //  Go for the pill
      if (GetDot(1, 6))
        pacman->Target(1, 6);
      else if (GetDot(26, 6))
        pacman->Target(26, 6);
      else if (GetDot(1, 26))
        pacman->Target(1, 26);
      else if (GetDot(26, 26))
        pacman->Target(26, 26);
      else{
        // closest dot
        int16_t dist = 0x7FFF;
        for (byte y = 4; y < 32; y++){
          for (byte x = 1; x < 26; x++){
            if (GetDot(x, y)){
              int16_t d = pacman->Distance(x, y);
              if (d < dist){
                dist = d;
                pacman->Target(x, y);
              }
            }
          }
        }
        if (dist == 0x7FFF) {
          GAMEWIN = 1; // No dots, GAME WIN!
        }
      }
    }
    //-------------------------------------------------------------------------/
    void Scatter(Sprite* s){
      const byte* st = _scatterTargets + (s->who << 1);
      s->Target(pgm_read_byte(st), pgm_read_byte(st + 1));
    }
    //-------------------------------------------------------------------------/
    void UpdateTargets(){
      if (_state == ReadyState)
        return;
      PacmanAI();
      Sprite* pacman = _sprites + PACMAN;
      //  Ghost AI
      bool scatter = _scIndex & 1;
      for (byte i = 0; i < 4; i++){
        Sprite* s = _sprites + i;
        //  Deal with returning ghost to pen
        if (s->state == DeadEyesState){
          if (s->cx == 14 && s->cy == 17){ // returned to pen
            s->state = PenState;        // Revived in pen
            s->pentimer = 80;
          }
          else
            s->Target(14, 17);          // target pen
          continue;           //
        }
        //  Release ghost from pen when timer expires
        if (s->pentimer){
          if (--s->pentimer)  // stay in pen for awhile
            continue;
          s->state = RunState;
        }
        if (InPen(s->cx, s->cy)){
          s->Target(14, 14 - 2); // Get out of pen first
        } else {
          if (scatter || s->state == FrightenedState)
            Scatter(s);
          else
          {
            // Chase mode targeting
            signed char tx = pacman->cx;
            signed char ty = pacman->cy;
            switch (s->who){
              case PINKY:{
                  const char* pto = _pinkyTargetOffset + ((pacman->dir - 1) << 1);
                  tx += pgm_read_byte(pto);
                  ty += pgm_read_byte(pto + 1);
                }
                break;
              case INKY:{
                  const char* pto = _pinkyTargetOffset + ((pacman->dir - 1) << 1);
                  Sprite* binky = _sprites + BINKY;
                  tx += pgm_read_byte(pto) >> 1;
                  ty += pgm_read_byte(pto + 1) >> 1;
                  tx += tx - binky->cx;
                  ty += ty - binky->cy;
                }
                break;
              case CLYDE:{
                  if (s->Distance(pacman->cx, pacman->cy) < 64){
                    const byte* st = _scatterTargets + CLYDE * 2;
                    tx = pgm_read_byte(st);
                    ty = pgm_read_byte(st + 1);
                  }
                }
                break;
            }
            s->Target(tx, ty);
          }
        }
      }
    }

    //  Default to current direction
    byte ChooseDir(int16_t dir, Sprite* s){
      int16_t choice[4];
      choice[0] = Chase(s, s->cx, s->cy - 1); // Up
      choice[1] = Chase(s, s->cx - 1, s->cy); // Left
      choice[2] = Chase(s, s->cx, s->cy + 1); // Down
      choice[3] = Chase(s, s->cx + 1, s->cy); // Right
      if (DEMO == 0 && s->who == PACMAN && choice[0] < 0x7FFF && but_UP) dir = MUp;
      else if (DEMO == 0 && s->who == PACMAN && choice[1] < 0x7FFF && but_LEFT) dir = MLeft;
      else if (DEMO == 0 && s->who == PACMAN && choice[2] < 0x7FFF && but_DOWN) dir = MDown;
      else if (DEMO == 0 && s->who == PACMAN && choice[3] < 0x7FFF && but_RIGHT) dir = MRight;
      else if (DEMO == 0 && choice[0] < 0x7FFF && s->who == PACMAN && dir == MUp) dir = MUp;
      else if (DEMO == 0 && choice[1] < 0x7FFF && s->who == PACMAN && dir == MLeft) dir = MLeft;
      else if (DEMO == 0 && choice[2] < 0x7FFF && s->who == PACMAN && dir == MDown) dir = MDown;
      else if (DEMO == 0 && choice[3] < 0x7FFF && s->who == PACMAN && dir == MRight) dir = MRight;
      else if ((DEMO == 0 && s->who != PACMAN) || DEMO == 1 ) {
        // Don't choose opposite of current direction?
        int16_t dist = choice[4 - dir]; // favor current direction
        byte opposite = OppositeDirection(dir);
        for (byte i = 0; i < 4; i++){
          byte d = 4 - i;
          if ((d != opposite && choice[i] < dist) || (s->who == PACMAN && PACMANFALLBACK &&  choice[i] < dist))
          {
            if (s->who == PACMAN && PACMANFALLBACK) PACMANFALLBACK = 0;
            dist = choice[i];
            dir = d;
          }
        }
      } else  {
        dir = MStopped;
      }
      return dir;
    }
    bool InPen(byte cx, byte cy){
      if (cx <= 10 || cx >= 18) return false;
      if (cy <= 14 || cy >= 18) return false;
      return true;
    }
    byte GetSpeed(Sprite* s){
      if (s->who == PACMAN)
        return _frightenedTimer ? 90 : 80;
      if (s->state == FrightenedState)
        return 40;
      if (s->state == DeadEyesState)
        return 100;
      if (s->cy == 17 && (s->cx <= 5 || s->cx > 20))
        return 40;  // tunnel
      return 75;
    }
    //-------------------------------------------------------------------------/
    void PackmanDied() {  // Noooo... PACMAN DIED 😦
      if (LIFES <= 0) {
        GAMEOVER = 1;
        LEVEL = START_LEVEL;
        LIFES = START_LIFES;
        DEMO = 1;
        Init();
      } else {
        LIFES--;
        _inited = true;
        _state = ReadyState;
        _stateTimer = FPS / 2;
        _frightenedCount = 0;
        _frightenedTimer = 0;
        const byte* s = _initSprites;
        for (int16_t i = 0; i < 5; i++)
          _sprites[i].Init(s + i * 5);
        _scIndex = 0;
        _scTimer = 1;
        memset(_icons, 0, sizeof(_icons));
        //AND BONUS
        _BonusSprite.Init(s + 5 * 5);
        _BonusInactiveTimmer = BONUS_INACTIVE_TIME;
        _BonusActiveTimmer = 0;
        for (byte i = 0; i < ACTUALBONUS; i++) {
          _icons[13 - i] = BONUSICON + i;
        }
        for (byte i = 0; i < LIFES; i++) {
          _icons[0 + i] = PACMANICON;
        }
        //Draw LIFE and BONUS Icons
        for (byte y = 34; y < 36; y++)
          for (byte x = 0; x < 28; x++) {
            Draw(x, y, false);
          }
        DrawAllBG();
      }
    }
    //-------------------------------------------------------------------------/
    void MoveAll(){
      UpdateTimers();
      UpdateTargets();
      //  Update game state
      if (_stateTimer){
        if (--_stateTimer <= 0){
          switch (_state){
            case ReadyState:
              _state = PlayState;
              _dirty[20 * 4 + 1] |= 0x1F; // Clear 'READY!'
              _dirty[20 * 4 + 2] |= 0x80;
              for (byte tmpX = 11; tmpX < 17; tmpX++) Draw(tmpX, 20, false); // ReDraw (clear) 'READY' position
              break;
            case DeadGhostState:
              _state = PlayState;
              for (byte i = 0; i < 4; i++){
                Sprite* s = _sprites + i;
                if (s->state == DeadNumberState)
                  s->state = DeadEyesState;
              }
              break;
            default:
              ;
          }
        } else {
          if (_state == ReadyState)
            return;
        }
      }
      for (byte i = 0; i < 5; i++){
        Sprite* s = _sprites + i;
        //  In DeadGhostState, only eyes move
        if (_state == DeadGhostState && s->state != DeadEyesState)
          continue;
        //  Calculate speed
        s->_speed += GetSpeed(s);
        if (s->_speed < 100)
          continue;
        s->_speed -= 100;
        s->lastx = s->_x;
        s->lasty = s->_y;
        s->phase++;
        int16_t x = s->_x;
        int16_t y = s->_y;
        if ((x & 0x7) == 0 && (y & 0x7) == 0)   // cell aligned
          s->dir = ChooseDir(s->dir, s);      // time to choose another direction
        switch (s->dir){
          case MLeft:     x -= SPEED; break;
          case MRight:    x += SPEED; break;
          case MUp:       y -= SPEED; break;
          case MDown:     y += SPEED; break;
          case MStopped:  break;
        }
        //  Wrap x because of tunnels
        while (x < 0)
          x += 224;
        while (x >= 224)
          x -= 224;
        s->_x = x;
        s->_y = y;
        s->cx = (x + 4) >> 3;
        s->cy = (y + 4) >> 3;
        if (s->who == PACMAN)
          EatDot(s->cx, s->cy);
      }
      //  Collide
      Sprite* pacman = _sprites + PACMAN;

      //  Collide with BONUS
      Sprite* _s = &_BonusSprite;
      if (ACTIVEBONUS == 1 && _s->cx == pacman->cx && _s->cy == pacman->cy)
      {
        Score(ACTUALBONUS * 50);
        ACTUALBONUS++;
        if (ACTUALBONUS > 7) {
          ACTUALBONUS = 0;
          if (LIFES < MAXLIFES) LIFES++;
          //reset all icons
          memset(_icons, 0, sizeof(_icons));
          for (byte i = 0; i < LIFES; i++) {
            _icons[0 + i] = PACMANICON;
          }
        }
        for (byte i = 0; i < ACTUALBONUS; i++) {
          _icons[13 - i] = BONUSICON + i;
        }
        //REDRAW LIFE and BONUS icons
        for (byte y = 34; y < 36; y++)
          for (byte x = 0; x < 28; x++) {
            Draw(x, y, false);
          }
        ACTIVEBONUS = 0;
        _BonusInactiveTimmer = BONUS_INACTIVE_TIME;
      }
      for (byte i = 0; i < 4; i++){
        Sprite* s = _sprites + i;
        //if (s->cx == pacman->cx && s->cy == pacman->cy)
        if (s->_x + SPEED >= pacman->_x && s->_x - SPEED <= pacman->_x && s->_y + SPEED >= pacman->_y && s->_y - SPEED <= pacman->_y)
        {
          if (s->state == FrightenedState)
          {
            s->state = DeadNumberState;     // Killed a ghost
            _frightenedCount++;
            _state = DeadGhostState;
            _stateTimer = 10;
            Score((1 << _frightenedCount) * 100);
          }
          else {               // pacman died
            if (s->state == DeadNumberState || s->state == FrightenedState || s->state == DeadEyesState) {
            } else {
              PackmanDied();
            }
          }
        }
      }
    }
    //-------------------------------------------------------------------------/
    //  Mark a position dirty
    void Mark(int16_t pos){
      for (byte tmp = 0; tmp < 28; tmp++)
        updateMap[1][tmp] = true;

    }
    //-------------------------------------------------------------------------/
    void SetScoreChar(byte i, signed char c){
      if (_scoreStr[i] == c)
        return;
      _scoreStr[i] = c;
      Mark(i + 32);  //Score
      //Mark(i+32+10); //HiScore

    }
    //-------------------------------------------------------------------------/
    void SetHiScoreChar(byte i, signed char c){
      if (_hiscoreStr[i] == c)
        return;
      _hiscoreStr[i] = c;
      //Mark(i+32);    //Score
      Mark(i + 32 + 10); //HiScore
    }
    //-------------------------------------------------------------------------/
    void Score(int16_t delta){
      char str[8];
      _score += delta;
      if (DEMO == 0 && _score > _hiscore) _hiscore = _score;
      if (_score > _lifescore && _score % 10000 > 0) {
        _lifescore = (_score / 10000 + 1) * 10000;
        LIFES++; // EVERY 10000 points = 1UP
        for (byte i = 0; i < LIFES; i++) {
          _icons[0 + i] = PACMANICON;
        }
        //REDRAW LIFE and BONUS icons
        for (byte y = 34; y < 36; y++)
          for (byte x = 0; x < 28; x++) {
            Draw(x, y, false);
          }
        _score = _score + 100;
      }
      sprintf(str, "%ld", _score);
      byte i = 7 - strlen(str);
      byte j = 0;
      while (i < 7)
        SetScoreChar(i++, str[j++]);
      sprintf(str, "%ld", _hiscore);
      i = 7 - strlen(str);
      j = 0;
      while (i < 7)
        SetHiScoreChar(i++, str[j++]);
    }

    bool GetDot(byte cx, byte cy){
      return _dotMap[(cy - 3) * 4 + (cx >> 3)] & (0x80 >> (cx & 7));
    }
    
    void EatDot(byte cx, byte cy){
      if (!GetDot(cx, cy))
        return;
      byte mask = 0x80 >> (cx & 7);
      _dotMap[(cy - 3) * 4 + (cx >> 3)] &= ~mask;
      byte t = GetTile(cx, cy);
      if (t == PILL){
        _frightenedTimer = 10 * FPS;
        _frightenedCount = 0;
        for (byte i = 0; i < 4; i++){
          Sprite* s = _sprites + i;
          if (s->state == RunState){
            s->state = FrightenedState;
            s->dir = OppositeDirection(s->dir);
          }
        }
        Score(50);
      }
      else
        Score(10);
    }
    //-------------------------------------------------------------------------/
    void Init(){
      if (GAMEWIN == 1) {
        GAMEWIN = 0;
      } else {
        LEVEL = START_LEVEL;
        LIFES = START_LIFES;
        ACTUALBONUS = 0; //actual bonus icon
        ACTIVEBONUS = 0; //status of bonus
        _score = 0;
        _lifescore = 10000;
        memset(_scoreStr, 0, sizeof(_scoreStr));
        _scoreStr[5] = _scoreStr[6] = '0';
      }
      _inited = true;
      _state = ReadyState;
      _stateTimer = FPS / 2;
      _frightenedCount = 0;
      _frightenedTimer = 0;
      const byte* s = _initSprites;
      for (int16_t i = 0; i < 5; i++)
        _sprites[i].Init(s + i * 5);
      //AND BONUS
      _BonusSprite.Init(s + 5 * 5);
      _BonusInactiveTimmer = BONUS_INACTIVE_TIME;
      _BonusActiveTimmer = 0;
      _scIndex = 0;
      _scTimer = 1;
      memset(_icons, 0, sizeof(_icons));
      // SET BONUS icons
      for (byte i = 0; i < ACTUALBONUS; i++) {
        _icons[13 - i] = BONUSICON + i;
      }
      // SET Lifes icons
      for (byte i = 0; i < LIFES; i++) {
        _icons[0 + i] = PACMANICON;
      }
      //Draw LIFE and BONUS Icons
      for (byte y = 34; y < 36; y++)
        for (byte x = 0; x < 28; x++) {
          Draw(x, y, false);
        }
      //  Init dots from rom
      memset(_dotMap, 0, sizeof(_dotMap));
      byte* map = _dotMap;
      for (byte y = 3; y < 36 - 3; y++){ // 30 interior lines
        for (byte x = 0; x < 28; x++){
          byte t = GetTile(x, y);
          if (t == 7 || t == 14){
            byte s = x & 7;
            map[x >> 3] |= (0x80 >> s);
          }
        }
        map += 4;
      }
      DrawAllBG();
    }
    //-------------------------------------------------------------------------/
    void Step(){
      int16_t keys = 0;
      if (GAMEWIN == 1) {
        LEVEL++;
        Init();
      }
      // Start GAME
      if (but_A && DEMO == 1 && GAMEPAUSED == 0) {
        but_A = false;
        DEMO = 0;
        Init();
      } else if (but_A && DEMO == 0 && GAMEPAUSED == 0) { // Or PAUSE GAME
        but_A = false;
        GAMEPAUSED = 1;
      }
      if (GAMEPAUSED && but_A && DEMO == 0) {
        but_A = false;
        GAMEPAUSED = 0;
        for (byte tmpX = 11; tmpX < 17; tmpX++) Draw(tmpX, 20, false);
      }
      // Reset / Start GAME
      if (but_B) {
        DEMO = 0;
        Init();
      } else if (!_inited) {
        DEMO = 1;
        Init();
      }
      // Create a bitmap of dirty tiles
      byte m[(32 / 8) * 36]; // 144 bytes
      memset(m, 0, sizeof(m));
      _dirty = m;
      if (!GAMEPAUSED) MoveAll(); // IF GAME is PAUSED STOP ALL
      if ((ACTIVEBONUS == 0 && DEMO == 1) || GAMEPAUSED == 1 ) for (byte tmpX = 11; tmpX < 17; tmpX++) Draw(tmpX, 20, false); // Draw 'PAUSED' or 'DEMO' text
      DrawAll();
    }
};

/******************************************************************************/
/*   SETUP                                                                    */
/******************************************************************************/
#define BLACK 0x0000 // 16bit BLACK Color

void setup() {
  randomSeed(analogRead(0));
  M5.begin();
  Wire.begin();
  if(digitalRead(BUTTON_A_PIN) == 0){
    delay(100);updateFromFS(SD);ESP.restart();
  }
  M5.Lcd.setRotation(2);
  M5.Lcd.fillScreen(TFT_BLACK);
  pinMode(5, INPUT_PULLUP);
  delay(100);
}

/******************************************************************************/
/*   LOOP                                                                     */
/******************************************************************************/
void ClearKeys() {
  but_A    =false;
  but_B    =false;
  but_UP   =false;
  but_DOWN =false;
  but_LEFT =false;
  but_RIGHT=false;
}

void KeyPadLoop(){
  char r;
  if(digitalRead(5) == LOW) {
    Wire.requestFrom(0X88, 1);         // request 1 byte from keyboard
    while (Wire.available()) { 
      uint8_t key_val = Wire.read();   // receive a byte as character
      if ( key_val == 127) { r = 'x';} // start
      if ( key_val == 191) { r = 'z';} // select     
      if ( key_val == 254) { r = '4';} // left
      if ( key_val == 253) { r = '6';} // right
      if ( key_val == 251) { r = '2';} // down
      if ( key_val == 247) { r = '8';} // up
    }
  }
  if (r == 'z') { ClearKeys();  but_A=true; delay(300); } //else but_A=false;
  if (r == 'x') { ClearKeys();  but_B=true; delay(300); } //else but_B=false;
  if (r == '8') { ClearKeys();  but_UP=true; }  //else but_UP=false;
  if (r == '2') { ClearKeys();  but_DOWN=true; }  //else but_DOWN=false;
  if (r == '4') { ClearKeys();  but_LEFT=true; }   //else but_LEFT=false;
  if (r == '6') { ClearKeys();  but_RIGHT=true; }  //else but_RIGHT=false;
}

Playfield _game;

void loop() {
  _game.Step();
  KeyPadLoop();
}

Written by macsbug

3月 7, 2018 @ 1:43 pm

カテゴリー: ESP32, M5STACK

4件のフィードバック

Subscribe to comments with RSS.

  1. […] 「おー!パックマン移植しとる人おるやん!」 【PACMAN with M5Stack】 […]

    • Makershipさん、サイトの訪問をありがとうございます。
      M5Stack Mini Arcade を拝見しています。素晴らしい出来ですね。
      私も作りたかったので見て楽しんでいます。
      M5の命令は時々 変わる場合があり M5のバージョンによって再検討が必要かと思います。
      M5Stack Mini Arcadeの記事は 新規に変更方法を含み 丁寧な解説は解りやすく助かっています。

      macsbug

      10月 30, 2018 at 4:44 pm

    • しかるのち さん
      1/24ゲーム筐体コレクション(テーブル型) + 0.85インチ液晶
      https://shikarunochi.matrix.jp/?p=5137

      じっくり見させて頂きました。
      確かに 0.85インチ はあり128×128に良く入りましたね。
      この大きさに収めた技量に感服致します。
      テーブルの造り込みにも驚くばかりです。

      macsbug

      8月 14, 2023 at 3:53 pm


コメントを残す