diff --git a/quarto/MyArduboy.cpp b/quarto/MyArduboy.cpp new file mode 100644 index 0000000..cabaaf6 --- /dev/null +++ b/quarto/MyArduboy.cpp @@ -0,0 +1,323 @@ +#include "MyArduboy.h" + +PROGMEM static const uint32_t imgFont[] = { + 0x00000000, 0x00017000, 0x000C00C0, 0x0A7CA7CA, 0x0855F542, 0x19484253, 0x1251F55E, 0x00003000, + 0x00452700, 0x001C9440, 0x0519F314, 0x0411F104, 0x00000420, 0x04104104, 0x00000400, 0x01084210, + 0x0F45145E, 0x0001F040, 0x13555559, 0x0D5D5551, 0x087C928C, 0x0D555557, 0x0D55555E, 0x010C5251, + 0x0F55555E, 0x0F555556, 0x0000A000, 0x0000A400, 0x0028C200, 0x0028A280, 0x00086280, 0x000D5040, + 0x0018E300, 0x1F24929C, 0x0D5D555F, 0x1145149C, 0x0725145F, 0x1155555F, 0x0114515F, 0x1D55545E, + 0x1F10411F, 0x0045F440, 0x07210410, 0x1D18411F, 0x1041041F, 0x1F04F05E, 0x1F04109C, 0x0F45545E, + 0x0314925F, 0x1F45D45E, 0x1B34925F, 0x0D555556, 0x0105F041, 0x0721041F, 0x0108421F, 0x0F41E41F, + 0x1D184317, 0x0109C107, 0x114D5651, 0x0045F000, 0x0001F000, 0x0001F440, 0x000C1080, 0x10410410, +}; + +#ifdef USE_ARDUBOY2_LIB + +#define myAudio audio + +MyArduboy::MyArduboy(void) +{ + pTunes = new ArduboyPlaytune(myAudio.enabled); + pTunes->initChannel(PIN_SPEAKER_1); +} + +void MyArduboy::beginNoLogo(void) +{ + boot(); + blank(); + flashlight(); + systemButtons(); + myAudio.begin(); +} + +#else + +#define textSize textsize +#define textWrap wrap +#define pTunes (&tunes) + +void MyArduboyAudio::begin() +{ + if (EEPROM.read(EEPROM_AUDIO_ON_OFF)) + on(); + else + off(); +} + +void MyArduboyAudio::on() +{ + power_timer3_enable(); + audio_enabled = true; +} + +void MyArduboyAudio::off() +{ + audio_enabled = false; + power_timer3_disable(); +} + +void MyArduboyAudio::toggle() +{ + if (audio_enabled) + off(); + else + on(); +} + +void MyArduboy::beginNoLogo(void) +{ + boot(); + if (pressed(UP_BUTTON)) { + sendLCDCommand(OLED_ALL_PIXELS_ON); + setRGBled(255, 255, 255); + power_timer0_disable(); + while (true) { + idle(); // infinite loop + } + } + pTunes->initChannel(PIN_SPEAKER_1); + pinMode(PIN_SPEAKER_2, OUTPUT); // trick + myAudio.begin(); +} + +#endif + +/*----------------------------------------------------------------------------*/ + +bool MyArduboy::nextFrame(void) +{ + bool ret = ARDUBOY_LIB_CLASS::nextFrame(); + if (ret) { + lastButtonState = currentButtonState; + currentButtonState = buttonsState(); + } + return ret; +} + +bool MyArduboy::buttonDown(uint8_t buttons) +{ + return currentButtonState & ~lastButtonState & buttons; +} + +bool MyArduboy::buttonPressed(uint8_t buttons) +{ + return currentButtonState & buttons; +} + + +bool MyArduboy::buttonUp(uint8_t buttons) +{ + return ~currentButtonState & lastButtonState & buttons; +} + +/*----------------------------------------------------------------------------*/ + +void MyArduboy::setTextColor(uint8_t color) +{ + setTextColor(color, (color == BLACK) ? WHITE : BLACK); +} + +void MyArduboy::setTextColor(uint8_t color, uint8_t bg) +{ + textcolor = color; + textbg = bg; +} + +size_t MyArduboy::printEx(int16_t x, int16_t y, const char *p) +{ + setCursor(x, y); + return print(p); +} + +size_t MyArduboy::printEx(int16_t x, int16_t y, const __FlashStringHelper *p) +{ + setCursor(x, y); + return print(p); +} + +size_t MyArduboy::write(uint8_t c) +{ + if (c == '\n') { + cursor_y += textSize * 6; + cursor_x = 0; + } else if (c >= ' ' && c <= '_') { + myDrawChar(cursor_x, cursor_y, c, textcolor, textbg, textSize); + cursor_x += textSize * 6; + if (textWrap && (cursor_x > (WIDTH - textSize * 6))) write('\n'); + } + return 1; // temporary +} + +void MyArduboy::myDrawChar(int16_t x, int16_t y, unsigned char c, uint8_t color, uint8_t bg, uint8_t size) +{ + bool draw_bg = bg != color; + + if (x >= WIDTH || y >= HEIGHT || x + 5 * size < 0 || y + 6 * size < 0) return; + uint32_t ptn = pgm_read_dword(imgFont + (c - ' ')); + if (size == 1) { + for (int8_t i = 0; i < 6; i++) { + for (int8_t j = 0; j < 6; j++) { + bool draw_fg = ptn & 0x1; + if (draw_fg || draw_bg) { + drawPixel(x + i, y + j, (draw_fg) ? color : bg); + } + ptn >>= 1; + } + } + } else { + for (int8_t i = 0; i < 6; i++) { + for (int8_t j = 0; j < 6; j++) { + bool draw_fg = ptn & 0x1; + if (draw_fg || draw_bg) { + fillRect(x + i * size, y + j * size, size, size, (draw_fg) ? color : bg); + } + ptn >>= 1; + } + } + } +} + +/*----------------------------------------------------------------------------*/ + +void MyArduboy::drawRect2(int16_t x, int16_t y, uint8_t w, int8_t h, uint8_t color) +{ + drawFastHLine2(x, y, w, color); + drawFastHLine2(x, y + h - 1, w, color); + drawFastVLine2(x, y + 1, h - 2, color); + drawFastVLine2(x + w - 1, y + 1, h - 2, color); +} + +void MyArduboy::drawFastVLine2(int16_t x, int16_t y, int8_t h, uint8_t color) +{ + /* Check parameters */ + if (y < 0) { + if (h <= -y) return; + h += y; + y = 0; + } + if (h <= 0 || y >= HEIGHT || x < 0 || x >= WIDTH) return; + if (y + h > HEIGHT) h = HEIGHT - y; + + /* Draw a vertical line */ + uint8_t yOdd = y & 7; + uint8_t d = 0xFF << yOdd; + y -= yOdd; + h += yOdd; + for (buffer_t *p = getBuffer() + x + (y / 8) * WIDTH; h > 0; h -= 8, p += WIDTH) { + if (h < 8) d &= 0xFF >> (8 - h); + if (color == BLACK) { + *p &= ~d; + } else { + *p |= d; + } + d = 0xFF; + } +} + +void MyArduboy::drawFastHLine2(int16_t x, int16_t y, uint8_t w, uint8_t color) +{ + /* Check parameters */ + if (x < 0) { + if (w <= -x) return; + w += x; + x = 0; + } + if (w <= 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return; + if (x + w > WIDTH) w = WIDTH - x; + + /* Draw a horizontal line */ + uint8_t yOdd = y & 7; + uint8_t d = 1 << yOdd; + buffer_t *p = getBuffer() + x + (y / 8) * WIDTH; + if (color == BLACK) { + fillBeltBlack(p, d, w); + } else { + fillBeltWhite(p, d, w); + } +} + +void MyArduboy::fillRect2(int16_t x, int16_t y, uint8_t w, int8_t h, uint8_t color) +{ + /* Check parameters */ + if (x < 0) { + if (w <= -x) return; + w += x; + x = 0; + } + if (y < 0) { + if (h <= -y) return; + h += y; + y = 0; + } + if (w <= 0 || x >= WIDTH || h <= 0 || y >= HEIGHT) return; + if (x + w > WIDTH) w = WIDTH - x; + if (y + h > HEIGHT) h = HEIGHT - y; + + /* Draw a filled rectangle */ + uint8_t yOdd = y & 7; + uint8_t d = 0xFF << yOdd; + y -= yOdd; + h += yOdd; + for (buffer_t *p = getBuffer() + x + (y / 8) * WIDTH; h > 0; h -= 8, p += WIDTH) { + if (h < 8) d &= 0xFF >> (8 - h); + if (color == BLACK) { + fillBeltBlack(p, d, w); + } else { + fillBeltWhite(p, d, w); + } + d = 0xFF; + } +} + +void MyArduboy::fillBeltBlack(buffer_t *p, uint8_t d, uint8_t w) +{ + d = ~d; + for (; w > 0; w--) { + *p++ &= d; + } +} + +void MyArduboy::fillBeltWhite(buffer_t *p, uint8_t d, uint8_t w) +{ + for (; w > 0; w--) { + *p++ |= d; + } +} + +/*----------------------------------------------------------------------------*/ + +bool MyArduboy::isAudioEnabled(void) +{ + return myAudio.enabled(); +} + +void MyArduboy::setAudioEnabled(bool on) +{ + if (on) { + myAudio.on(); + } else { + myAudio.off(); + } + +} + +void MyArduboy::saveAudioOnOff(void) +{ + myAudio.saveOnOff(); +} + +void MyArduboy::playScore2(const byte *score, uint8_t priority) +{ + if (!isAudioEnabled()) return; + if (pTunes->playing()) { + if (priority > playScorePriority) return; + pTunes->stopScore(); + } + playScorePriority = priority; + pTunes->playScore(score); +} + +void MyArduboy::stopScore2(void) +{ + pTunes->stopScore(); +} diff --git a/quarto/MyArduboy.h b/quarto/MyArduboy.h new file mode 100644 index 0000000..b1dd8e1 --- /dev/null +++ b/quarto/MyArduboy.h @@ -0,0 +1,67 @@ +#pragma once + +//#define USE_ARDUBOY2_LIB + +#ifdef USE_ARDUBOY2_LIB +#include +#include +#define ARDUBOY_LIB_CLASS Arduboy2 +#define ARDUBOY_LIB_VER_TGT 50100 +typedef uint8_t buffer_t; +#else +#include +#define ARDUBOY_LIB_CLASS Arduboy +#define ARDUBOY_LIB_VER_TGT 10101 +typedef unsigned char buffer_t; +class MyArduboyAudio : public ArduboyAudio +{ +public: + void static begin(); + void static on(); + void static off(); + void static toggle(); +}; +#endif + + +class MyArduboy : public ARDUBOY_LIB_CLASS +{ +public: +#ifdef USE_ARDUBOY2_LIB + MyArduboy(void); +#endif + void beginNoLogo(void); + bool nextFrame(void); + bool buttonDown(uint8_t buttons); + bool buttonPressed(uint8_t buttons); + bool buttonUp(uint8_t buttons); + void setTextColor(uint8_t color); + void setTextColor(uint8_t color, uint8_t bg); + size_t printEx(int16_t x, int16_t y, const char *p); + size_t printEx(int16_t x, int16_t y, const __FlashStringHelper *p); + virtual size_t write(uint8_t); + void drawRect2(int16_t x, int16_t y, uint8_t w, int8_t h, uint8_t color); + void drawFastVLine2(int16_t x, int16_t y, int8_t h, uint8_t color); + void drawFastHLine2(int16_t x, int16_t y, uint8_t w, uint8_t color); + void fillRect2(int16_t x, int16_t y, uint8_t w, int8_t h, uint8_t color); + bool isAudioEnabled(void); + void setAudioEnabled(bool on); + void saveAudioOnOff(void); + void playScore2(const byte *score, uint8_t priority); + void stopScore2(void); + +private: + void myDrawChar(int16_t x, int16_t y, unsigned char c, uint8_t color, uint8_t bg, uint8_t size); + void fillBeltBlack(buffer_t *p, uint8_t d, uint8_t w); + void fillBeltWhite(buffer_t *p, uint8_t d, uint8_t w); + uint8_t textcolor = WHITE; + uint8_t textbg = BLACK; + uint8_t lastButtonState; + uint8_t currentButtonState; + uint8_t playScorePriority; +#ifdef USE_ARDUBOY2_LIB + ArduboyPlaytune *pTunes; +#else + MyArduboyAudio myAudio; +#endif +}; diff --git a/quarto/common.cpp b/quarto/common.cpp new file mode 100644 index 0000000..199e195 --- /dev/null +++ b/quarto/common.cpp @@ -0,0 +1,255 @@ +#include "common.h" + +/* Defines */ + +#define EEPROM_ADDR_BASE 992 +#define EEPROM_SIGNATURE 0x084E424FUL // "OBN\x08" + +#define PAD_REPEAT_DELAY (FPS / 4) +#define PAD_REPEAT_INTERVAL (FPS / 12) + +enum RECORD_STATE_T { + RECORD_NOT_READ = 0, + RECORD_INITIAL, + RECORD_STORED, +}; + +/* Global Variables */ + +MyArduboy arduboy; +RECORD_T record; +bool isRecordDirty; +int8_t padX, padY, padRepeatCount; +bool isInvalid; + +/* Local Functions */ + +static uint16_t calcCheckSum(); + +static void eepSeek(int addr); +static uint8_t eepRead8(void); +static uint16_t eepRead16(void); +static uint32_t eepRead32(void); +static void eepReadBlock(void *p, size_t n); +static void eepWrite8(uint8_t val); +static void eepWrite16(uint16_t val); +static void eepWrite32(uint32_t val); +static void eepWriteBlock(const void *p, size_t n); + +/* Local Variables */ + +PROGMEM static const byte soundTick[] = { + 0x90, 69, 0, 10, 0x80, 0xF0 // arduboy.tone2(440, 10); +}; + +PROGMEM static const byte soundClick[] = { + 0x90, 74, 0, 20, 0x80, 0xF0 // arduboy.tone2(587, 20); +}; + +static RECORD_STATE_T recordState = RECORD_NOT_READ; +static int16_t eepAddr; + +/*---------------------------------------------------------------------------*/ +/* Common Functions */ +/*---------------------------------------------------------------------------*/ + +void readRecord(void) +{ + bool isVerified = false; + eepSeek(EEPROM_ADDR_BASE); + if (eepRead32() == EEPROM_SIGNATURE) { + eepReadBlock(&record, sizeof(record)); + isVerified = (eepRead16() == calcCheckSum()); + } + + if (isVerified) { + recordState = RECORD_STORED; + isRecordDirty = false; + dprintln(F("Read record from EEPROM")); + } else { + memset(&record, 0, sizeof(record)); + record.cpuLevel = 2; + record.maxLevel = 3; + record.settings = SETTING_BIT_THINK_LED; + recordState = RECORD_INITIAL; + isRecordDirty = true; + } + setSound(arduboy.isAudioEnabled()); // Load Sound ON/OFF +} + +void writeRecord(void) +{ + if (!isRecordDirty) return; + if (recordState == RECORD_INITIAL) { + eepSeek(EEPROM_ADDR_BASE); + eepWrite32(EEPROM_SIGNATURE); + } else { + eepSeek(EEPROM_ADDR_BASE + 4); + } + eepWriteBlock(&record, sizeof(record)); + eepWrite16(calcCheckSum()); + arduboy.audio.saveOnOff(); // Save Sound ON/OFF + recordState = RECORD_STORED; + isRecordDirty = false; + dprintln(F("Write record to EEPROM")); +} + +static uint16_t calcCheckSum() +{ + uint16_t checkSum = (EEPROM_SIGNATURE & 0xFFFF) + (EEPROM_SIGNATURE >> 16) * 3; + uint16_t *p = (uint16_t *) &record; + for (int i = 0; i < sizeof(record) / 2; i++) { + checkSum += *p++ * (i * 2 + 5); + } + return checkSum; +} + +void clearRecord(void) +{ + eepSeek(EEPROM_ADDR_BASE); + for (int i = 0; i < (sizeof(record) + 6) / 4; i++) { + eepWrite32(0); + } + recordState = RECORD_INITIAL; + dprintln(F("Clear EEPROM")); +} + +void handleDPad(void) +{ + padX = padY = 0; + if (arduboy.buttonPressed(LEFT_BUTTON | RIGHT_BUTTON | UP_BUTTON | DOWN_BUTTON)) { + if (++padRepeatCount >= (PAD_REPEAT_DELAY + PAD_REPEAT_INTERVAL)) { + padRepeatCount = PAD_REPEAT_DELAY; + } + if (padRepeatCount == 1 || padRepeatCount == PAD_REPEAT_DELAY) { + if (arduboy.buttonPressed(LEFT_BUTTON)) padX--; + if (arduboy.buttonPressed(RIGHT_BUTTON)) padX++; + if (arduboy.buttonPressed(UP_BUTTON)) padY--; + if (arduboy.buttonPressed(DOWN_BUTTON)) padY++; + } + } else { + padRepeatCount = 0; + } +} + +void drawNumber(int16_t x, int16_t y, int32_t value) +{ + arduboy.setCursor(x, y); + arduboy.print(value); +} + +void drawTime(int16_t x, int16_t y, uint32_t frames) +{ + uint16_t h = frames / (FPS * 3600UL); + uint8_t m = frames / (FPS * 60) % 60; + uint8_t s = frames / FPS % 60; + arduboy.setCursor(x, y); + if (h == 0 && m == 0) { + if (s < 10) arduboy.print('0'); + arduboy.print(s); + arduboy.print('.'); + arduboy.print(frames / (FPS / 10) % 10); + } else { + if (h > 0) { + arduboy.print(h); + arduboy.print(':'); + if (m < 10) arduboy.print('0'); + } + arduboy.print(m); + arduboy.print(':'); + if (s < 10) arduboy.print('0'); + arduboy.print(s); + } +} + +void invertScreen(bool isInvert) +{ + arduboy.sendLCDCommand(isInvert ? OLED_PIXELS_INVERTED : OLED_PIXELS_NORMAL); +} + +/*---------------------------------------------------------------------------*/ +/* Sound Functions */ +/*---------------------------------------------------------------------------*/ + +void setSound(bool on) +{ + arduboy.setAudioEnabled(on); + dprint(F("audioEnabled=")); + dprintln(on); +} + +void playSoundTick(void) +{ + arduboy.playScore2(soundTick, 255); +} + +void playSoundClick(void) +{ + arduboy.playScore2(soundClick, 255); +} + +/*---------------------------------------------------------------------------*/ +/* EEPROM Functions */ +/*---------------------------------------------------------------------------*/ + +void eepSeek(int addr) +{ + eepAddr = max(addr, EEPROM_STORAGE_SPACE_START); +} + +uint8_t eepRead8(void) +{ + eeprom_busy_wait(); + return eeprom_read_byte((const uint8_t *) eepAddr++); +} + +uint16_t eepRead16(void) +{ + eeprom_busy_wait(); + uint16_t ret = eeprom_read_word((const uint16_t *)eepAddr); + eepAddr += 2; + return ret; +} + +uint32_t eepRead32(void) +{ + eeprom_busy_wait(); + uint32_t ret = eeprom_read_dword((const uint32_t *) eepAddr); + eepAddr += 4; + return ret; +} + +void eepReadBlock(void *p, size_t n) +{ + eeprom_busy_wait(); + eeprom_read_block(p, (const void *) eepAddr, n); + eepAddr += n; +} + +void eepWrite8(uint8_t val) +{ + eeprom_busy_wait(); + eeprom_write_byte((uint8_t *) eepAddr, val); + eepAddr++; +} + +void eepWrite16(uint16_t val) +{ + eeprom_busy_wait(); + eeprom_write_word((uint16_t *)eepAddr, val); + eepAddr += 2; +} + +void eepWrite32(uint32_t val) +{ + eeprom_busy_wait(); + eeprom_write_dword((uint32_t *)eepAddr, val); + eepAddr += 4; +} + +void eepWriteBlock(const void *p, size_t n) +{ + eeprom_busy_wait(); + eeprom_write_block(p, (void *) eepAddr, n); + eepAddr += n; +} diff --git a/quarto/common.h b/quarto/common.h new file mode 100644 index 0000000..2ca8a64 --- /dev/null +++ b/quarto/common.h @@ -0,0 +1,111 @@ +#pragma once + +#include "MyArduboy.h" + +/* Defines */ + +//#define DEBUG +#define FPS 60 +#define APP_TITLE "QUARTO" +#define APP_CODE "OBN-Y08" +#define APP_VERSION "0.01" +#define APP_RELEASED "OCTOBER 2019" + +enum MODE_T { + MODE_LOGO = 0, + MODE_TITLE, + MODE_GAME, +}; + +enum GAME_MODE_T { + GAME_MODE_YOU_FIRST = 0, + GAME_MODE_CPU_FIRST, + GAME_MODE_2PLAYERS, +}; + +#define SETTING_BIT_THINK_LED 0x1 +#define SETTING_BIT_SCREEN_INV 0x2 + +/* Typedefs */ + +typedef struct { + uint8_t dummy[16]; + uint8_t gameMode; // 0-2 (2 bits) + uint8_t cpuLevel; // 1-5 (3 bits) + uint8_t maxLevel; // 3-5 (3 bits) + uint8_t settings; // 2 bits + uint32_t playFrames; + uint16_t playCount; +} RECORD_T; // sizeof(RECORD_T) musb be 26 bytes + +/* Global Functions (Common) */ + +void readRecord(void); +void writeRecord(void); +void clearRecord(void); + +void handleDPad(void); +void drawNumber(int16_t x, int16_t y, int32_t value); +void drawTime(int16_t x, int16_t y, uint32_t frames); +void invertScreen(bool isInvert); + +void setSound(bool on); +void playSoundTick(void); +void playSoundClick(void); + +void eepSeek(int addr); +uint8_t eepRead8(void); +uint16_t eepRead16(void); +uint32_t eepRead32(void); +void eepReadBlock(void *p, size_t n); +void eepWrite8(uint8_t val); +void eepWrite16(uint16_t val); +void eepWrite32(uint32_t val); +void eepWriteBlock(const void *p, size_t n); + +/* Global Functions (Menu) */ + +void clearMenuItems(void); +void addMenuItem(const __FlashStringHelper *label, void (*func)(void)); +int8_t getMenuItemPos(void); +int8_t getMenuItemCount(void); +void setMenuCoords(int8_t x, int8_t y, int8_t w, int8_t h, bool f, bool s); +void setMenuItemPos(int8_t pos); +void handleMenu(void); +void drawMenuItems(bool isForced); +void drawSoundEnabled(void); + +/* Global Functions (Each Mode) */ + +void initLogo(void); +MODE_T updateLogo(void); +void drawLogo(void); + +void initTitle(void); +MODE_T updateTitle(void); +void drawTitle(void); + +void initGame(void); +MODE_T updateGame(void); +void drawGame(void); + +/* Global Variables */ + +extern MyArduboy arduboy; +extern RECORD_T record; + +extern bool isRecordDirty; +extern int8_t padX, padY, padRepeatCount; +extern bool isInvalid; + +/* For Debugging */ + +#ifdef DEBUG +extern bool dbgPrintEnabled; +extern char dbgRecvChar; +#define dprint(...) (!dbgPrintEnabled || Serial.print(__VA_ARGS__)) +#define dprintln(...) (!dbgPrintEnabled || Serial.println(__VA_ARGS__)) +#else +#define dprint(...) +#define dprintln(...) +#endif diff --git a/quarto/data.h b/quarto/data.h new file mode 100644 index 0000000..bc2f0ff --- /dev/null +++ b/quarto/data.h @@ -0,0 +1,107 @@ +#pragma once + +/*---------------------------------------------------------------------------*/ +/* Image Data */ +/*---------------------------------------------------------------------------*/ + +#define IMG_PIECE_W 15 +#define IMG_PIECE_H 15 +#define IMG_QUARTO_ID_MAX 16 + +PROGMEM static const uint8_t imgPiece[IMG_QUARTO_ID_MAX][30] = { // 15x15 x16 + { + 0x00, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x00, + 0x00, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x00 + },{ + 0x00, 0xFE, 0xFE, 0xFE, 0xFE, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0xFE, 0xFE, 0xFE, 0xFE, 0x00, + 0x00, 0x3F, 0x3F, 0x3F, 0x3F, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3F, 0x3F, 0x3F, 0x3F, 0x00 + },{ + 0x00, 0x00, 0x00, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x00, 0x00, 0x00 + },{ + 0x00, 0x00, 0x00, 0xF8, 0xF8, 0xF8, 0x38, 0x38, 0x38, 0xF8, 0xF8, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0F, 0x0F, 0x0F, 0x0E, 0x0E, 0x0E, 0x0F, 0x0F, 0x0F, 0x00, 0x00, 0x00 + },{ + 0xE0, 0xF8, 0xFC, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFC, 0xF8, 0xE0, + 0x03, 0x0F, 0x1F, 0x3F, 0x3F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x3F, 0x3F, 0x1F, 0x0F, 0x03 + },{ + 0xE0, 0xF8, 0xFC, 0xFE, 0x3E, 0x1F, 0x0F, 0x0F, 0x0F, 0x1F, 0x3E, 0xFE, 0xFC, 0xF8, 0xE0, + 0x03, 0x0F, 0x1F, 0x3F, 0x3E, 0x7C, 0x78, 0x78, 0x78, 0x7C, 0x3E, 0x3F, 0x1F, 0x0F, 0x03 + },{ + 0x00, 0x00, 0xC0, 0xF0, 0xF8, 0xF8, 0xFC, 0xFC, 0xFC, 0xF8, 0xF8, 0xF0, 0xC0, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x07, 0x0F, 0x0F, 0x1F, 0x1F, 0x1F, 0x0F, 0x0F, 0x07, 0x01, 0x00, 0x00 + },{ + 0x00, 0x00, 0xC0, 0xF0, 0xF8, 0x78, 0x3C, 0x1C, 0x3C, 0x78, 0xF8, 0xF0, 0xC0, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x07, 0x0F, 0x0F, 0x1E, 0x1C, 0x1E, 0x0F, 0x0F, 0x07, 0x01, 0x00, 0x00 + },{ + 0x00, 0xFE, 0xAA, 0x56, 0xAA, 0x56, 0xAA, 0x56, 0xAA, 0x56, 0xAA, 0x56, 0xAA, 0xFE, 0x00, + 0x00, 0x3F, 0x2A, 0x35, 0x2A, 0x35, 0x2A, 0x35, 0x2A, 0x35, 0x2A, 0x35, 0x2A, 0x3F, 0x00 + },{ + 0x00, 0xFE, 0xAA, 0x56, 0xFA, 0x16, 0x1A, 0x16, 0x1A, 0x16, 0xFA, 0x56, 0xAA, 0xFE, 0x00, + 0x00, 0x3F, 0x2A, 0x35, 0x2F, 0x34, 0x2C, 0x34, 0x2C, 0x34, 0x2F, 0x35, 0x2A, 0x3F, 0x00 + },{ + 0x00, 0x00, 0x00, 0xF8, 0xA8, 0x58, 0xA8, 0x58, 0xA8, 0x58, 0xA8, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0F, 0x0A, 0x0D, 0x0A, 0x0D, 0x0A, 0x0D, 0x0A, 0x0F, 0x00, 0x00, 0x00 + },{ + 0x00, 0x00, 0x00, 0xF8, 0xA8, 0xD8, 0x28, 0x38, 0x28, 0xD8, 0xA8, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0F, 0x0A, 0x0D, 0x0A, 0x0E, 0x0A, 0x0D, 0x0A, 0x0F, 0x00, 0x00, 0x00 + },{ + 0xE0, 0x58, 0xAC, 0x56, 0xAA, 0x55, 0xAB, 0x55, 0xAB, 0x55, 0xAA, 0x56, 0xAC, 0x58, 0xE0, + 0x03, 0x0D, 0x1A, 0x35, 0x2A, 0x55, 0x6A, 0x55, 0x6A, 0x55, 0x2A, 0x35, 0x1A, 0x0D, 0x03 + },{ + 0xE0, 0x58, 0xAC, 0xD6, 0x2A, 0x15, 0x0B, 0x0D, 0x0B, 0x15, 0x2A, 0xD6, 0xAC, 0x58, 0xE0, + 0x03, 0x0D, 0x1A, 0x35, 0x2A, 0x54, 0x68, 0x58, 0x68, 0x54, 0x2A, 0x35, 0x1A, 0x0D, 0x03 + },{ + 0x00, 0x00, 0xC0, 0x70, 0xA8, 0x58, 0xAC, 0x54, 0xAC, 0x58, 0xA8, 0x70, 0xC0, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x07, 0x0A, 0x0D, 0x1A, 0x15, 0x1A, 0x0D, 0x0A, 0x07, 0x01, 0x00, 0x00 + },{ + 0x00, 0x00, 0xC0, 0x70, 0xA8, 0x58, 0x2C, 0x14, 0x2C, 0x58, 0xA8, 0x70, 0xC0, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x07, 0x0A, 0x0D, 0x1A, 0x14, 0x1A, 0x0D, 0x0A, 0x07, 0x01, 0x00, 0x00 + } +}; + +#define IMG_PLAYER_W 11 +#define IMG_PLAYER_H 11 + +PROGMEM static const uint8_t imgPlayer[2][22] = { // 11x11 x2 + { 0x00, 0x00, 0x1C, 0xA2, 0x41, 0x01, 0x41, 0xA2, 0x1C, 0x00, 0x00, 0x00, 0x06, 0x05, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x06, 0x00 }, + { 0x54, 0xFE, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0xFE, 0x54, 0x01, 0x03, 0x06, 0x02, 0x06, 0x02, 0x06, 0x02, 0x06, 0x03, 0x01 }, +}; + +/*---------------------------------------------------------------------------*/ +/* Sound Data */ +/*---------------------------------------------------------------------------*/ + +enum { + SND_PRIO_START = 0, + SND_PRIO_RESULT, + SND_PRIO_CANCEL, + SND_PRIO_PLACE, +}; + +PROGMEM static const byte soundStart[] = { + 0x90, 72, 0, 100, 0x90, 74, 0, 100, 0x90, 76, 0, 100, + 0x90, 77, 0, 100, 0x90, 79, 0, 200, 0x80, 0xF0 +}; + +PROGMEM static const byte soundPlace[] = { + 0x90, 55, 0, 6, 0x90, 58, 0, 6, 0x90, 52, 0, 6, 0x90, 49, 0, 6, + 0x90, 55, 0, 6, 0x90, 58, 0, 6, 0x90, 52, 0, 6, 0x90, 49, 0, 6, 0x80, 0xF0 +}; + +PROGMEM static const byte soundCancel[] = { + 0x90, 72, 0, 40, 0x90, 69, 0, 40, 0x90, 65, 0, 40, 0x80, 0xF0 +}; + +PROGMEM static const byte soundWin[] = { + 0x90, 81, 0, 40, 0x80, 0, 40, 0x90, 86, 0, 40, 0x80, 0, 40, 0x90, 90, 0, 40, 0x80, 0, 40, 0xE0 +}; + +PROGMEM static const byte soundDraw[] = { + 0x90, 60, 0, 64, 0x80, 0, 64, 0xE0 +}; + +PROGMEM static const byte soundLose[] = { + 0x90, 55, 0, 120, 0x90, 54, 0, 140, 0x90, 53, 0, 160, 0x90, 52, 0, 180, + 0x90, 51, 0, 200, 0x90, 50, 0, 220, 0x90, 49, 0, 240, 0x90, 48, 0, 260, 0x80, 0xF0 +}; diff --git a/quarto/game.cpp b/quarto/game.cpp new file mode 100644 index 0000000..c48a854 --- /dev/null +++ b/quarto/game.cpp @@ -0,0 +1,612 @@ +#include "common.h" +#include "data.h" + +/* Defines */ + +#define BOARD_SIZE 4 +#define BOARD_EMPTY 0xFF +#define PIECE_ATTRS 4 +#define TURN_MAX (BOARD_SIZE * BOARD_SIZE) +#define PIECE_MAX (1 << PIECE_ATTRS) + +#define CPU_INTERVAL_FRAMES 8 +#define CPU_INTERVAL_MILLIS ((1000 * CPU_INTERVAL_FRAMES) / FPS) + +#define EVAL_INF 32767 +#define EVAL_WIN 5400 + +enum STATE_T { + STATE_INIT = 0, + STATE_PLACING, + STATE_CHOOSING, + STATE_MENU, + STATE_OVER, + STATE_LEAVE, +}; + +/* Typedefs */ + +typedef struct { + uint8_t board[BOARD_SIZE][BOARD_SIZE]; + uint8_t turn; + uint8_t currentPiece; + uint16_t restPieces; +} GAME_T; + +/* Local Functions */ + +static void newGame(void); +static void finalizeGame(void); +static bool isCpuTurn(void); +static void handlePlacing(void); +static void handleChoosing(void); +static void handleOver(void); +static void onContinue(void); +static void onCancel(void); +static void onRetry(void); +static void onQuit(void); +static void onExit(void); + +static void drawBoard(bool isAnimation); +static void drawPiece(int16_t x, int16_t y, int16_t piece); +static void drawCursor(void); +static void drawBoardEdge(int16_t x); +static void drawRestPieces(void); +static void drawPlayerIndication(bool isAnimation); +static void drawResult(void); + +static bool cpuThinking(void); +static void cpuThinkingInterval(void); +static int alphabeta(GAME_T *p, int8_t depth, int alpha, int beta); +static bool isWin(GAME_T *p, uint8_t x, uint8_t y); +static bool isLined(GAME_T *p, uint8_t x, uint8_t y, int8_t vx, int8_t vy); + +/* Local Variables */ + +static STATE_T state = STATE_INIT; +static GAME_T game, lastGame; +static bool isCancelable, isCpuInterrupted, isLastAPressed; +static uint8_t animCounter, counter; +static uint8_t cursorX, cursorY, cursorPiece, cpuX, cpuY, cpuPiece; +static unsigned long nextCpuInterval; +static const byte *resultSound; +static const __FlashStringHelper *resultLabel; + +/*---------------------------------------------------------------------------*/ +/* Main Functions */ +/*---------------------------------------------------------------------------*/ + +void initGame(void) +{ + newGame(); + isRecordDirty = true; + writeRecord(); + isCancelable = false; + arduboy.playScore2(soundStart, 0); + isInvalid = true; +} + +MODE_T updateGame(void) +{ + counter++; + if (state == STATE_PLACING || state == STATE_CHOOSING) { + record.playFrames++; + isRecordDirty = true; + } + switch (state) { + case STATE_PLACING: + handlePlacing(); + break; + case STATE_CHOOSING: + handleChoosing(); + break; + case STATE_MENU: + handleMenu(); + break; + case STATE_OVER: + handleOver(); + break; + } + return (state == STATE_LEAVE) ? MODE_TITLE : MODE_GAME; +} + +void drawGame(void) +{ + if (state == STATE_LEAVE) return; + if (state == STATE_MENU) { + drawMenuItems(isInvalid); + isInvalid = false; + return; + } + + if (true/*isInvalid*/) { + arduboy.clear(); + drawBoard(false); + drawPlayerIndication(false); + } else { + // TODO + } + + switch (state) { + case STATE_PLACING: + if (!isCpuTurn()) drawCursor(); + break; + case STATE_CHOOSING: + drawRestPieces(); + break; + case STATE_OVER: + drawResult(); + break; + } +} + +/*---------------------------------------------------------------------------*/ +/* Control Functions */ +/*---------------------------------------------------------------------------*/ + +static void newGame(void) +{ + record.playCount++; + memset(&game.board, BOARD_EMPTY, BOARD_SIZE * BOARD_SIZE); + cursorX = 0; + cursorY = 0; + cursorPiece = random(16); + game.turn = 0; + game.currentPiece = cursorPiece; + game.restPieces = ~(1 << cursorPiece); + state = STATE_PLACING; + dprintln(F("New game")); +} + +static void finalizeGame(void) +{ + if (game.turn == TURN_MAX) { + resultSound = soundDraw; + resultLabel = F("DRAW"); + } else if (record.gameMode == GAME_MODE_2PLAYERS) { + resultSound = soundWin; + resultLabel = (game.turn % 2 == 0) ? F("1P WINS") : F("2P WINS"); + } else { + bool isPlayerWin = (game.turn % 2 == record.gameMode); + resultSound = (isPlayerWin) ? soundWin : soundLose; + resultLabel = (isPlayerWin) ? F("YOU WIN") : F("YOU LOSE"); + if (isPlayerWin && record.maxLevel < 5 && record.cpuLevel == record.maxLevel) { + record.maxLevel++; + dprintln(F("Level up!!!")); + } + } + animCounter = 0; + state = STATE_OVER; + dprint(F("Game set: ")); + dprintln(resultLabel); +} + +static bool isCpuTurn(void) +{ + return (record.gameMode != GAME_MODE_2PLAYERS && game.turn % 2 != record.gameMode); +} + +static void handlePlacing(void) +{ + bool isPlaced = false; + if (isCpuTurn()) { + /* CPU thinking */ + if (!arduboy.buttonDown(A_BUTTON)) isPlaced = cpuThinking(); + if (isPlaced) { + cursorX = cpuX; + cursorY = cpuY; + } + } else { + /* Move cursor */ + handleDPad(); + if (padX != 0 || padY != 0) { + cursorX = (cursorX + padX + BOARD_SIZE) % BOARD_SIZE; + cursorY = (cursorY + padY + BOARD_SIZE) % BOARD_SIZE; + playSoundTick(); + } + /* Place a piece */ + if (arduboy.buttonDown(B_BUTTON) && game.board[cursorY][cursorX] == BOARD_EMPTY) { + isPlaced = true; + } + } + + if (isPlaced) { + game.board[cursorY][cursorX] = game.currentPiece; + arduboy.playScore2(soundPlace, SND_PRIO_PLACE); + if (isWin(&game, cursorX, cursorY)) { + finalizeGame(); + } else if (game.restPieces == 0) { + game.turn++; + finalizeGame(); + } else { + while (!(game.restPieces & 1 << cursorPiece)) { + cursorPiece = (cursorPiece + 1) % PIECE_MAX; + } + state = STATE_CHOOSING; + } + } else if (arduboy.buttonDown(A_BUTTON)) { + playSoundClick(); + clearMenuItems(); + addMenuItem(F("CONTINUE"), onContinue); + if (isCancelable) addMenuItem(F("CANCEL LAST MOVE"), onCancel); + addMenuItem(F("QUIT GAME"), onQuit); + int w = (isCancelable) ? 107 : 83; + int h = (isCancelable) ? 17 : 11; + setMenuCoords(63 - w / 2, 32 - h / 2, w, h, true, true); + setMenuItemPos(0); + isCpuInterrupted = false; + state = STATE_MENU; + isInvalid = true; + dprintln(F("Menu: playing")); + } + +#ifdef DEBUG + if (dbgRecvChar == 'q') { + finalizeGame(); + } +#endif +} + +static void handleChoosing(void) +{ + bool isChosen = false; + if (isCpuTurn()) { + cursorPiece = cpuPiece; + isChosen = true; + } else { + handleDPad(); + if (padX != 0) { + do { + cursorPiece = (cursorPiece + padX + PIECE_MAX) % PIECE_MAX; + } while (!(game.restPieces & 1 << cursorPiece)); + playSoundTick(); + } + if (arduboy.buttonDown(B_BUTTON)) { + /* Choose */ + isChosen = true; + } else if (arduboy.buttonDown(A_BUTTON)) { + /* Cancel */ + game.board[cursorY][cursorX] = BOARD_EMPTY; + state = STATE_PLACING; + } + } + + if (isChosen) { + game.turn++; + game.currentPiece = cursorPiece; + game.restPieces &= ~(1 << cursorPiece); + state = STATE_PLACING; + playSoundClick(); + } +} + +static void handleOver(void) +{ + if (animCounter <= FPS * 2) { + if (animCounter == 0) writeRecord(); + animCounter++; + if (animCounter == FPS) arduboy.playScore2(resultSound, SND_PRIO_RESULT); + if (animCounter == FPS * 2 && resultSound != soundLose) arduboy.stopScore2(); + } else { + if (arduboy.buttonDown(B_BUTTON)) isInvalid = true; + if (arduboy.buttonUp(B_BUTTON)) animCounter = FPS * 2; + if (arduboy.buttonDown(A_BUTTON)) { + playSoundClick(); + clearMenuItems(); + addMenuItem(F("RETRY GAME"), onRetry); + addMenuItem(F("BACK TO TITLE"), onExit); + setMenuCoords(19, 19, 89, 11, true, false); + setMenuItemPos(0); + state = STATE_MENU; + isInvalid = true; + dprintln(F("Menu: game over")); + } + } +} + +static void onContinue(void) +{ + playSoundClick(); + state = STATE_PLACING; + isInvalid = true; +} + +static void onCancel(void) +{ + game = lastGame; + isCancelable = false; + arduboy.playScore2(soundCancel, SND_PRIO_CANCEL); + state = STATE_PLACING; + isInvalid = true; +} + +static void onRetry(void) +{ + initGame(); +} + +static void onQuit(void) +{ + playSoundClick(); + clearMenuItems(); + addMenuItem(F("QUIT"), onExit); + addMenuItem(F("CANCEL"), onContinue); + setMenuCoords(40, 38, 47, 11, true, true); + setMenuItemPos(1); + isInvalid = true; + dprintln(F("Menu: quit")); +} + +static void onExit(void) +{ + playSoundClick(); + writeRecord(); + state = STATE_LEAVE; + dprintln(F("Back to title")); +} + +/*---------------------------------------------------------------------------*/ +/* Draw Functions */ +/*---------------------------------------------------------------------------*/ + +static void drawBoard(bool isAnimation) +{ + /* Board */ + drawBoardEdge(30); + drawBoardEdge(96); + for (uint8_t y = 0; y < BOARD_SIZE - 1; y++) { + for (uint8_t x = 0; x < BOARD_SIZE - 1; x++) { + arduboy.drawPixel(x * 16 + 47, y * 16 + 15, WHITE); + } + } + + /* Pieces */ + for (uint8_t y = 0; y < BOARD_SIZE; y++) { + for (uint8_t x = 0; x < BOARD_SIZE; x++) { + uint8_t piece = game.board[y][x]; + if (piece != BOARD_EMPTY) { + drawPiece(x * 16 + 32, y * 16, piece); + } + } + } +} + +static void drawPiece(int16_t x, int16_t y, int16_t piece) +{ + arduboy.drawBitmap(x, y, imgPiece[piece], IMG_PIECE_W, IMG_PIECE_H, WHITE); +} + +static void drawCursor(void) +{ + int8_t x = cursorX * 16 + 32; + int8_t y = cursorY * 16; + arduboy.drawRect2(x, y, IMG_PIECE_W, IMG_PIECE_H, counter % 2); +} + +static void drawBoardEdge(int16_t x) +{ + uint8_t *p = arduboy.getBuffer() + x; + for (uint8_t i = 0; i < 8; i++) { + *p = 0xAA; + p += WIDTH; + } +} + +static void drawRestPieces(void) +{ + arduboy.drawRect2(-1, 38, WIDTH + 2, 19, WHITE); + arduboy.fillRect2(0, 39, WIDTH, 17, BLACK); + + uint8_t piecesCount = TURN_MAX - 1 - game.turn; + uint8_t piecePos = 0; + for (uint8_t piece = 0; piece < cursorPiece; piece++) { + if (game.restPieces & 1 << piece) piecePos++; + } + int16_t x = 64 - piecesCount * 8; + if (piecesCount > 8) { + int16_t gap = (piecesCount - 8) * 16; + x += gap / 2 - gap * piecePos / (piecesCount - 1); + } + for (uint8_t i = 0, piece = 0; i < piecesCount; i++, piece++) { + while (!(game.restPieces & 1 << piece)) { + piece++; + } + drawPiece(x, 40, piece); + if (piece == cursorPiece) { + arduboy.drawRect2(x, 40, IMG_PIECE_W, IMG_PIECE_H, counter % 2); // TODO + } + x += 16; + } +} + +static void drawPlayerIndication(bool isAnimation) +{ + if (isAnimation) { + arduboy.fillRect2(0, 24, IMG_PLAYER_W, IMG_PLAYER_H + 8, BLACK); + arduboy.fillRect2(116, 24, IMG_PLAYER_W, IMG_PLAYER_H + 8, BLACK); + } else { + // TODO + } + + int a = counter >> 3 & 3; + for (int i = 0; i < 2; i++) { + bool isCpu = isCpuTurn(); + int x = i * 96 + 10; + int y = (i == game.turn % 2) ? 21 + abs(a - 1) : 22; + if (record.gameMode == GAME_MODE_2PLAYERS) { + arduboy.printEx(x, 14, (i == 0) ? F("1P") : F("2P")); + } else { + arduboy.printEx(x - 3, 14, isCpu ? F("CPU") : F("YOU")); + } + arduboy.drawBitmap(x, y, imgPlayer[isCpu], IMG_PLAYER_W, IMG_PLAYER_H, WHITE); + if (isCpu) drawNumber(x + 3, y + 3, record.cpuLevel); + } + + if (state == STATE_PLACING) { + uint16_t x = game.turn % 2 * 96 + 8; + drawPiece(x, 40, game.currentPiece); + arduboy.drawRect2(x - 2, 38, 19, 19, WHITE); + } +} + +static void drawResult(void) +{ + if (animCounter < FPS || animCounter > FPS * 2) return; + int h = min(animCounter - FPS + 1, 12); + arduboy.fillRect2(0, 51 - h / 2, WIDTH, h + 2, BLACK); + arduboy.fillRect2(0, 52 - h / 2, WIDTH, h, WHITE); + int a = FPS * 2 - animCounter; + int x = a * a / 18 + WIDTH / 2 - (strlen_PF((uint_farptr_t) resultLabel) * 6 - 1); + if (x < WIDTH) { + arduboy.setTextColor(BLACK, BLACK); + arduboy.setTextSize(2); + arduboy.printEx(x, 47, resultLabel); + arduboy.setTextColor(WHITE, BLACK); + arduboy.setTextSize(1); + } +} + +/*---------------------------------------------------------------------------*/ +/* Thinking Algorithm */ +/*---------------------------------------------------------------------------*/ + +static bool cpuThinking(void) +{ + GAME_T work = game; + int8_t depth = min(record.cpuLevel, game.turn / 2 + 1); + nextCpuInterval = millis() + CPU_INTERVAL_MILLIS; + isCpuInterrupted = false; + isLastAPressed = arduboy.pressed(A_BUTTON); + int eval = -alphabeta(&work, depth, -EVAL_INF, EVAL_INF); + arduboy.setRGBled(0, 0, 0); + if (isCpuInterrupted) { + dprintln(F("CPU was interrupted")); + return false; + } else { + dprint(F("CPU's evaluation=")); + dprintln(eval); + return true; + } +} + +static int alphabeta(GAME_T *p, int8_t depth, int alpha, int beta) +{ + bool isRoot = (alpha == -EVAL_INF && beta == EVAL_INF); + int eval = 0; + + /* Can win or is last move? */ + for (uint8_t y = 0; y < BOARD_SIZE; y++) { + for (uint8_t x = 0; x < BOARD_SIZE; x++) { + if (p->board[y][x] == BOARD_EMPTY) { + if (isRoot && p->turn >= TURN_MAX - 1) { + cpuX = x; + cpuY = y; + } + p->board[y][x] = p->currentPiece; + if (isWin(p, x, y)) { + eval += EVAL_WIN; + if (isRoot) { + cpuX = x; + cpuY = y; + } + } + p->board[y][x] = BOARD_EMPTY; + } + } + } + if (eval > 0 || p->turn >= TURN_MAX - 1 || depth == 0) { + eval = eval / (TURN_MAX - p->turn) + random(-128, 128); + if (eval > alpha) { + alpha = eval; + } + return -alpha; + } + + /* Search best move */ + p->turn++; + for (uint8_t y = 0; y < BOARD_SIZE; y++) { + for (uint8_t x = 0; x < BOARD_SIZE; x++) { + if (p->board[y][x] == BOARD_EMPTY) { + if (millis() >= nextCpuInterval) cpuThinkingInterval(); + p->board[y][x] = p->currentPiece; + for (uint8_t piece = 0; piece < PIECE_MAX; piece++) { + uint16_t pieceBit = 1 << piece; + if (p->restPieces & pieceBit) { + p->currentPiece = piece; + p->restPieces ^= pieceBit; + eval = alphabeta(p, depth - 1, -beta, -alpha); + p->restPieces ^= pieceBit; + if (eval > alpha) { + alpha = eval; + if (isRoot) { + cpuX = x; + cpuY = y; + cpuPiece = piece; + } + } + if (alpha >= beta) { + p->currentPiece = p->board[y][x]; + p->board[y][x] = BOARD_EMPTY; + p->turn--; + return -alpha; + } + } + } + p->currentPiece = p->board[y][x]; + p->board[y][x] = BOARD_EMPTY; + } + } + } + p->turn--; + return -alpha; +} + +static void cpuThinkingInterval(void) +{ + record.playFrames += CPU_INTERVAL_FRAMES; + counter += CPU_INTERVAL_FRAMES; + nextCpuInterval += CPU_INTERVAL_MILLIS; + bool isAPressed = arduboy.pressed(A_BUTTON); + if (!isLastAPressed && isAPressed) { + isCpuInterrupted = true; + } else { + drawGame(); + arduboy.display(); + if (record.settings & SETTING_BIT_THINK_LED) { + uint8_t r = counter << 1 & 0x70; + arduboy.setRGBled((r < 64) ? r + 4 : 132 - r, 0, 0); + } + } + isLastAPressed = isAPressed; +} + +static bool isWin(GAME_T *p, uint8_t x, uint8_t y) +{ + bool ret = isLined(p, x, 0, 0, 1) || isLined(p, 0, y, 1, 0); + if (!ret && x == y) ret = isLined(p, 0, 0, 1, 1); + if (!ret && x == BOARD_SIZE - y - 1) ret = isLined(p, BOARD_SIZE - 1, 0, -1, 1); + return ret; +} + +static bool isLined(GAME_T *p, uint8_t x, uint8_t y, int8_t vx, int8_t vy) +{ + uint8_t attrCnt[PIECE_ATTRS]; + memset(attrCnt, 0, PIECE_ATTRS); + for (uint8_t i = 0; i < BOARD_SIZE; i++) { + uint8_t piece = p->board[y][x]; + if (piece == BOARD_EMPTY) { + return false; + } + for (uint8_t j = 0; j < PIECE_ATTRS; j++) { + if (piece & 1 << j) attrCnt[j]++; + } + x += vx; + y += vy; + } + for (uint8_t i = 0; i < PIECE_ATTRS; i++) { + if (attrCnt[i] == 0 || attrCnt[i] == BOARD_SIZE) { + return true; + } + } + return false; +} diff --git a/quarto/logo.cpp b/quarto/logo.cpp new file mode 100644 index 0000000..5aeb235 --- /dev/null +++ b/quarto/logo.cpp @@ -0,0 +1,74 @@ +#include "common.h" + +/* Defines */ + +#define COUNTER_MAX (FPS * 2) // 2 secs +#define SIGNAL_PTN 0xEEE3AA3AU // "OBN" in Morse code + +/* Local Variables */ + +PROGMEM static const uint8_t imgOBN[][96] = { // 24x32 x3 + { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xE0, 0xF0, 0x58, 0xAC, 0x56, 0xAB, 0x55, + 0xAB, 0x55, 0x00, 0xFF, 0x01, 0x01, 0x01, 0x02, 0x02, 0x04, 0x08, 0x10, 0x60, 0x80, 0x00, 0x00, + 0x3F, 0xFF, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xA8, 0x5F, 0xAC, 0x5C, 0xAC, 0x5C, + 0xAC, 0x5C, 0xAC, 0x5C, 0x0C, 0xCF, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x03, 0x06, 0x0D, 0x1A, 0x15, + 0x2A, 0x25, 0x2A, 0x25, 0x2A, 0x25, 0x22, 0x15, 0x12, 0x09, 0x04, 0x02, 0x01, 0x00, 0x00, 0x00, // O + },{ + 0xFF, 0xFF, 0x57, 0xAB, 0x57, 0xAB, 0x57, 0xAB, 0x57, 0xAB, 0x03, 0xFF, 0x80, 0x80, 0x80, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, + 0x55, 0xAA, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x04, 0x08, 0x30, 0xC0, 0x00, 0x00, + 0x1F, 0x7F, 0xD5, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x54, 0xAF, 0x56, 0xAE, 0x56, 0xAE, + 0x56, 0xAE, 0x56, 0x2E, 0x86, 0x67, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x06, 0x0D, 0x0A, + 0x15, 0x12, 0x15, 0x12, 0x15, 0x12, 0x11, 0x0A, 0x09, 0x04, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, // B + }, { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0xC0, 0xC0, 0xC0, 0xC0, 0x80, 0x80, 0x80, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xF0, 0x78, 0xAC, 0x56, 0xAB, 0x55, 0xAA, + 0x55, 0xAA, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x04, 0x08, 0x30, 0xC0, 0x00, 0x00, + 0xFF, 0xFF, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x00, 0xFF, 0xFE, 0xAE, 0x56, 0xAE, + 0x56, 0xAE, 0x56, 0xAE, 0x56, 0x07, 0xFE, 0x00, 0x1F, 0x17, 0x15, 0x12, 0x15, 0x12, 0x15, 0x12, + 0x15, 0x12, 0x10, 0x1F, 0x17, 0x12, 0x15, 0x12, 0x15, 0x12, 0x15, 0x12, 0x15, 0x10, 0x1F, 0x00, // N + } +}; + +PROGMEM static const uint8_t imgSoft[] = { // 32x8 + 0xC0, 0xC6, 0xCF, 0xDF, 0xFF, 0xFB, 0x73, 0x03, 0x7C, 0xFE, 0xFF, 0xC7, 0xE3, 0xFF, 0x7F, 0x3E, + 0x00, 0xFF, 0xFF, 0xFF, 0x1B, 0x1B, 0x1B, 0x03, 0x00, 0x03, 0x03, 0xFF, 0xFF, 0xFF, 0x03, 0x03, +}; + +static uint8_t counter; +static bool signalOn; + +/*---------------------------------------------------------------------------*/ +/* Main Functions */ +/*---------------------------------------------------------------------------*/ + +void initLogo(void) +{ + counter = COUNTER_MAX; +} + +MODE_T updateLogo(void) +{ + counter--; + signalOn = (SIGNAL_PTN >> (counter - FPS / 4) / (FPS / 20)) & 1; + arduboy.setRGBled(0, 0, signalOn * 127); + MODE_T ret = (counter == 0) ? MODE_TITLE : MODE_LOGO; + if (ret) { + dprintln(F("Start " APP_TITLE " Version " APP_VERSION)); + } + return ret; +} + +void drawLogo(void) +{ + arduboy.clear(); + int shake = (COUNTER_MAX - counter) / (44 * FPS / 60); + for (int i = 0; i < 3; i++) { + int y = 12 + (i == shake) * signalOn; + arduboy.drawBitmap(28 + i * 24, y, imgOBN[i], 24, 32, WHITE); + } + arduboy.drawBitmap(68, 44, imgSoft, 32, 8, WHITE); + arduboy.printEx(16, 58, F(APP_CODE " VER " APP_VERSION)); +} diff --git a/quarto/menu.cpp b/quarto/menu.cpp new file mode 100644 index 0000000..2b42af7 --- /dev/null +++ b/quarto/menu.cpp @@ -0,0 +1,127 @@ +#include "common.h" + +/* Defines */ + +#define MENU_COUNT_MAX 6 + +/* Typedefs */ + +typedef struct +{ + void (*func)(void); + const __FlashStringHelper *label; +} ITEM_T; + +/* Local Variables */ + +PROGMEM static const uint8_t imgSound[14] = { + 0x3E, 0x47, 0x6B, 0x6D, 0x6D, 0x41, 0x3E, 0x00, 0x1C, 0x1C, 0x00, 0x1C, 0x3E, 0x7F +}; + +PROGMEM static const uint8_t imgSoundOffOn[2][6] = { + { 0x00, 0x00, 0x50, 0x20, 0x50, 0x00 }, + { 0x14, 0x08, 0x22, 0x1C, 0x41, 0x3E }, +}; + +static int8_t menuX, menuY, menuW, menuH; +static bool isFramed, isControlSound; +static int8_t menuItemCount; +static ITEM_T menuItemAry[MENU_COUNT_MAX]; +static int8_t menuItemPos; +static bool isInvalidMenu; + +/*---------------------------------------------------------------------------*/ + +void clearMenuItems(void) +{ + menuItemCount = 0; + dprintln(F("Clear menu items")); +} + +void addMenuItem(const __FlashStringHelper *label, void (*func)(void)) +{ + if (menuItemCount >= MENU_COUNT_MAX) return; + ITEM_T *pItem = &menuItemAry[menuItemCount]; + pItem->label = label; + pItem->func = func; + menuItemCount++; + dprint(F("Add menu items: ")); + dprintln(label); +} + +int8_t getMenuItemPos(void) +{ + return menuItemPos; +} + +int8_t getMenuItemCount(void) +{ + return menuItemCount; +} + +void setMenuCoords(int8_t x, int8_t y, int8_t w, int8_t h, bool f, bool s) +{ + menuX = x; + menuY = y; + menuW = w; + menuH = h; + isFramed = f; + isControlSound = s; +} + +void setMenuItemPos(int8_t pos) +{ + menuItemPos = pos; + isInvalidMenu = true; + dprint(F("menuItemPos=")); + dprintln(menuItemPos); +} + +void handleMenu(void) +{ + if (arduboy.buttonDown(UP_BUTTON) && menuItemPos > 0) { + menuItemPos--; + playSoundTick(); + isInvalidMenu = true; + dprint(F("menuItemPos=")); + dprintln(menuItemPos); + } + if (arduboy.buttonDown(DOWN_BUTTON) && menuItemPos < menuItemCount - 1) { + menuItemPos++; + playSoundTick(); + isInvalidMenu = true; + dprint(F("menuItemPos=")); + dprintln(menuItemPos); + } + if (isControlSound && arduboy.buttonDown(A_BUTTON)) { + setSound(!arduboy.isAudioEnabled()); + playSoundClick(); + isInvalidMenu = true; + } + if (arduboy.buttonDown(B_BUTTON)) { + menuItemAry[menuItemPos].func(); + } +} + +void drawMenuItems(bool isForced) +{ + if (!isInvalidMenu && !isForced) return; + arduboy.fillRect2(menuX - 1, menuY - 1, menuW + 2, menuH + 2, BLACK); + if (isFramed) { + arduboy.drawRect2(menuX - 2, menuY - 2, menuW + 4, menuH + 4, WHITE); + } + ITEM_T *pItem = menuItemAry; + for (int i = 0; i < menuItemCount; i++, pItem++) { + arduboy.printEx(menuX + 12 - (i == menuItemPos) * 4, menuY + i * 6, pItem->label); + } + arduboy.fillRect2(menuX, menuY + menuItemPos * 6, 5, 5, WHITE); + if (isControlSound) drawSoundEnabled(); + isInvalidMenu = false; +} + +void drawSoundEnabled(void) +{ + arduboy.fillRect2(106, 56, 22, 8, BLACK); + arduboy.drawBitmap(107, 57, imgSound, 14, 7, WHITE); + arduboy.drawBitmap(122, 57, imgSoundOffOn[arduboy.audio.enabled()], 6, 7, WHITE); +} diff --git a/quarto/quarto.ino b/quarto/quarto.ino new file mode 100644 index 0000000..455295d --- /dev/null +++ b/quarto/quarto.ino @@ -0,0 +1,91 @@ +#include "common.h" + +#if ARDUBOY_LIB_VER != ARDUBOY_LIB_VER_TGT +#error Unexpected version of Arduboy Library +#endif // It may work even if you use other version. So comment out the above line. + +/* Defines */ + +#define callInitFunc(idx) ((void (*)(void)) pgm_read_word((uint16_t) &moduleTable[idx]))() +#define callUpdateFunc(idx) ((MODE_T (*)(void)) pgm_read_word((uint16_t) &moduleTable[idx] + 2))() +#define callDrawFunc(idx) ((void (*)(void)) pgm_read_word((uint16_t) &moduleTable[idx] + 4))() + +/* Typedefs */ + +typedef struct +{ + void(*initFunc)(void); + MODE_T(*updateFunc)(void); + void(*drawFunc)(void); +} MODULE_FUNCS; + +/* Local Variables */ + +PROGMEM static const MODULE_FUNCS moduleTable[] = { + { initLogo, updateLogo, drawLogo }, + { initTitle, updateTitle, drawTitle }, + { initGame, updateGame, drawGame }, +}; + +static MODE_T mode; + +/* For Debugging */ + +#ifdef DEBUG +bool dbgPrintEnabled = true; +char dbgRecvChar = '\0'; + +static void dbgCheckSerialRecv(void) +{ + int recv; + while ((recv = Serial.read()) != -1) { + switch (recv) { + case 'd': + dbgPrintEnabled = !dbgPrintEnabled; + Serial.print("Debug output "); + Serial.println(dbgPrintEnabled ? "ON" : "OFF"); + break; + case 'r': + clearRecord(); + break; + } + if (recv >= ' ' && recv <= '~') { + dbgRecvChar = recv; + } + } +} +#endif + +/*---------------------------------------------------------------------------*/ + +void setup() +{ +#ifdef DEBUG + Serial.begin(115200); +#endif + arduboy.beginNoLogo(); + arduboy.setFrameRate(FPS); + //arduboy.setTextColor(WHITE, WHITE); + mode = MODE_LOGO; + callInitFunc(mode); +} + +void loop() +{ +#ifdef DEBUG + dbgCheckSerialRecv(); +#endif + if (!(arduboy.nextFrame())) return; + MODE_T nextMode = callUpdateFunc(mode); + callDrawFunc(mode); +#ifdef DEBUG + dbgRecvChar = '\0'; +#endif + arduboy.display(); + if (mode != nextMode) { + mode = nextMode; + dprint(F("mode=")); + dprintln(mode); + callInitFunc(mode); + } +} diff --git a/quarto/title.cpp b/quarto/title.cpp new file mode 100644 index 0000000..ef6eac1 --- /dev/null +++ b/quarto/title.cpp @@ -0,0 +1,235 @@ +#include "common.h" + +/* Defines */ + +enum STATE_T { + STATE_INIT = 0, + STATE_TITLE, + STATE_SETTINGS, + STATE_CREDIT, + STATE_STARTED, +}; + +/* Local Functions */ + +static void initTitleMenu(bool isFromSettings); +static void onVsCpu(void); +static void on2Players(void); +static void onLevel(void); +static void onBack(void); +static void onSettings(void); +static void onSettingChange(void); +static void onCredit(void); +static void handleAnyButton(void); + +static void drawTitleImage(void); +static void drawRecord(void); +static void drawCredit(void); + +/* Local Variables */ + +PROGMEM static const uint8_t imgTitle[] = { +}; + +PROGMEM static const char creditText[] = "- " APP_TITLE " -\0\0" APP_RELEASED \ + "\0PROGREMMED BY OBONO\0\0THIS PROGRAM IS\0RELEASED UNDER\0THE MIT LICENSE."; + +static STATE_T state = STATE_INIT; +static bool isSettingsChanged; + +/*---------------------------------------------------------------------------*/ +/* Main Functions */ +/*---------------------------------------------------------------------------*/ + +void initTitle(void) +{ + if (state == STATE_INIT) { + readRecord(); + invertScreen(record.settings & SETTING_BIT_SCREEN_INV); + } + initTitleMenu(false); +} + +MODE_T updateTitle(void) +{ + MODE_T ret = MODE_TITLE; + switch (state) { + case STATE_TITLE: + case STATE_SETTINGS: + handleMenu(); + if (state == STATE_STARTED) { + ret = MODE_GAME; + } + break; + case STATE_CREDIT: + handleAnyButton(); + break; + } + randomSeed(rand() ^ micros()); // Shuffle random + return ret; +} + +void drawTitle(void) +{ + if (isInvalid) { + arduboy.clear(); + switch (state) { + case STATE_TITLE: + drawTitleImage(); + break; + case STATE_SETTINGS: + drawRecord(); + break; + case STATE_CREDIT: + drawCredit(); + break; + } + } + if (state == STATE_TITLE || state == STATE_SETTINGS) { + drawMenuItems(isInvalid); + if (state == STATE_SETTINGS && + (arduboy.buttonDown(UP_BUTTON | DOWN_BUTTON) || isSettingsChanged)) { + int8_t pos = getMenuItemPos(); + for (int i = 0; i < 2; i++) { + bool on = (record.settings & 1 << i); + arduboy.printEx(106 - (i == pos) * 4, i * 6 + 12, (on) ? F("ON ") : F("OFF")); + } + isSettingsChanged = false; + } + } + isInvalid = false; +} + +/*---------------------------------------------------------------------------*/ +/* Control Functions */ +/*---------------------------------------------------------------------------*/ + +static void initTitleMenu(bool isFromSettings) +{ + clearMenuItems(); + addMenuItem(F("YOU FIRST"), onVsCpu); + addMenuItem(F("CPU FIRST"), onVsCpu); + addMenuItem(F("2 PLAYERS"), on2Players); + setMenuItemPos((isFromSettings) ? 3 : record.gameMode); + addMenuItem(F("SETTINGS"), onSettings); + addMenuItem(F("CREDIT"), onCredit); + setMenuCoords(57, 28, 71, 30, false, true); + if (isFromSettings) isInvalid = true; + state = STATE_TITLE; + isInvalid = true; + dprintln(F("Menu: title")); +} + +static void onVsCpu(void) +{ + record.gameMode = (getMenuItemPos() == 0) ? GAME_MODE_YOU_FIRST : GAME_MODE_CPU_FIRST; + playSoundClick(); + + int maxLevel = max(record.maxLevel, 3); + clearMenuItems(); + addMenuItem(F("LEVEL 1"), onLevel); + addMenuItem(F("LEVEL 2"), onLevel); + addMenuItem(F("LEVEL 3"), onLevel); + if (maxLevel >= 4) addMenuItem(F("LEVEL 4"), onLevel); + if (maxLevel >= 5) addMenuItem(F("LEVEL 5"), onLevel); + addMenuItem(F("BACK"), onBack); + setMenuCoords(57, 28, 71, 36, false, false); + setMenuItemPos(record.cpuLevel - 1); + dprintln(F("Menu: level")); +} + +static void on2Players(void) +{ + record.gameMode = GAME_MODE_2PLAYERS; + state = STATE_STARTED; + dprintln(F("Start 2 players game")); +} +static void onLevel(void) +{ + record.cpuLevel = getMenuItemPos() + 1; + state = STATE_STARTED; + dprint(F("Start vs CPU: level=")); + dprint(record.cpuLevel); + dprintln((record.gameMode == GAME_MODE_YOU_FIRST) ? F(" You first") : F(" CPU first")); +} + +static void onBack(void) +{ + playSoundClick(); + initTitleMenu(state == STATE_SETTINGS); +} + +static void onSettings(void) +{ + playSoundClick(); + clearMenuItems(); + addMenuItem(F("THINKING LED"), onSettingChange); + addMenuItem(F("INVERT SCREEN"), onSettingChange); + addMenuItem(F("EXIT"), onBack); + setMenuCoords(4, 12, 120, 30, false, false); + setMenuItemPos(0); + state = STATE_SETTINGS; + isInvalid = true; + isSettingsChanged = true; + dprintln(F("Menu: settings")); +} + +static void onSettingChange(void) +{ + playSoundTick(); + record.settings ^= 1 << getMenuItemPos(); + isSettingsChanged = true; + if (getMenuItemPos() == 1) { + invertScreen(record.settings & SETTING_BIT_SCREEN_INV); + } + dprint(F("Setting changed: ")); + dprintln(getMenuItemPos()); +} + +static void onCredit(void) +{ + playSoundClick(); + state = STATE_CREDIT; + isInvalid = true; + dprintln(F("Show credit")); +} + +static void handleAnyButton(void) +{ + if (arduboy.buttonDown(A_BUTTON | B_BUTTON)) { + playSoundClick(); + state = STATE_TITLE; + isInvalid = true; + } +} + +/*---------------------------------------------------------------------------*/ +/* Draw Functions */ +/*---------------------------------------------------------------------------*/ + +static void drawTitleImage(void) +{ + arduboy.printEx(0, 0, F(APP_TITLE)); + arduboy.printEx(0, 6, F("TITLE SCREEN")); + //arduboy.drawBitmap(0, 12, imgTitle, 16, 16, WHITE); +} + +static void drawRecord(void) +{ + arduboy.printEx(34, 4, F("[SETTINGS]")); + arduboy.drawFastHLine2(0, 44, 128, WHITE); + arduboy.printEx(10, 48, F("PLAY COUNT ")); + arduboy.print(record.playCount); + arduboy.printEx(10, 54, F("PLAY TIME")); + drawTime(76, 54, record.playFrames); +} + +static void drawCredit(void) +{ + const char *p = creditText; + for (int i = 0; i < 8; i++) { + uint8_t len = strnlen_P(p, 20); + arduboy.printEx(64 - len * 3, i * 6 + 8, (const __FlashStringHelper *) p); + p += len + 1; + } +}