//OLED相关的变量声明都放到OLED.h里了
#include "OLED.h"
//像这样引用“C:\Users\zc1415926\Documents\Arduino\libraries\Adafruit_GFX_Library\Fonts”文件夹里的字体
#include <Fonts/FreeSans12pt7b.h>
#include "pitches.h"
#include <Servo.h>
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Servo BombServo;
//LED针脚数组,按LED实际摆放的位置从左到右存入引脚号,便于想象显示二进制数时的亮灭情况
//const byte ledPinArr[] = {5, 4, 3, 2};
const byte ledPinArr[] = {2, 3, 4, 5};
//要拆除的引线所在的引脚,没有特殊的存入顺序
const int fuseinPins[] = {6, 7, 8, 9, 10, 11, 12,13};
//蜂鸣器引脚,Wokwi里边只有无源蜂鸣器,实际使用有源蜂鸣器应该要更响一些
const int BuzzPin = 16;
//游戏胜利时播放的音乐(音符和时长)
// notes in the melody:
int melody[] = {NOTE_C4, NOTE_G3,NOTE_G3, NOTE_A3, NOTE_G3,0, NOTE_B3, NOTE_C4};
// note durations: 4 = quarter note, 8 = eighth note, etc.:
int noteDurations[] = {4, 8, 8, 4,4,4,4,4 };
//游戏逻辑类
int timer=15; //游戏时长15秒
int On_Led_Pin=0;
boolean StartStatus=false;
boolean EndStatus=false;
boolean FirstSucess=false;
byte defusePinIndex=0;
//要是0的话,就搞不清上一次拆的是0号线,还是没有拆线
byte previousdefusePinIndex=255;
//定义一个函数的引用,将地址指向“0”,调用这个函数可以重启Arduino,
//详见:https://blog.csdn.net/acktomas/article/details/102776270
void(* resetFunc) (void) = 0;
const byte ResetPin = 15;
void setup() {
Serial.begin(9600);
randomSeed(analogRead(0));
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
display.setTextColor(SSD1306_WHITE);
display.setFont(&FreeSans12pt7b);
showLogo();
BombServo.attach(17); // attaches the servo on pin 17(A3) to the servo object
BombServo.write(5);
pinMode(BuzzPin, OUTPUT);
//设置引线为输入上拉模式
for(int i=6;i<14;i++)
{
//给每一条线加一个LED,附带一个限流电阻可以显示各条线接好没有
pinMode(i, INPUT_PULLUP);
}
//设备启动LED动画效果
for(int i=2;i<6;i++)
{
pinMode(i, OUTPUT);
digitalWrite(i,LOW);
delay(100);
digitalWrite(i,HIGH);
}
//设置游戏重新开始按钮的引脚
pinMode(ResetPin, INPUT_PULLUP);
gameStart();
EndStatus=false;
}
void loop()
{
//如果游戏还没有结束,那么执行游戏逻辑
if(EndStatus==false)
{
//执行游戏逻辑,把GameLogic放到lcd控制代码前边,让LED闪烁动画出现在倒计时前
GameLogic();
//不判断一下的话,即使炸弹拆除,也会执行一次下面的程序,屏幕还会显示倒计时,并发出声音
if(EndStatus!=true)
{
//显示还剩多少秒
timerCount(timer);
//如果时间用完则引爆炸弹,否则把时间减少1,并播放音效
if(timer==0)
{
//触发爆炸
TriggerBomb();
}
else
{
timer--;
//播放读秒音效
SoundEffect(timer);
}
delay(1000);
}
}
//如果游戏结束了,就只有重启按钮可以按
else
{
//重启按钮按下后,检查所有引线的状态,只有全部插好了才能重启设备
if(digitalRead(ResetPin) == 0)
{
//将所有引线的状态值求和。如果全部插好了,sum的结果为0,设备重启,重新开始游戏。
//只要有一根线没连好,sum就大于0,播放报错音,设备无法重启。
byte sum = 0;
for(int i=0;i<8;i++)
{
sum+=digitalRead(fuseinPins[i]);
}
Serial.print("sum: ");
Serial.println(sum);
if(sum > 0)
{
tone(BuzzPin, NOTE_C4, 200);
delay(200);
tone(BuzzPin, NOTE_C3, 500);
delay(500);
}
else
{
//调用地址为0的重启函数
resetFunc();
}
}
}
}
void GameLogic()
{
//游戏未开始,且没有引爆
if(StartStatus==false && EndStatus==false)
{
RandomLed();
//用传引用的方式修改全局亦是
RandomCondition(defusePinIndex);
StartStatus=true;
}
//不停地检查爆炸的状态
CheckBombStatus();
}
//在生成题目前,播一个灯光动画,和音效
void RandomLed()
{
//led随机亮起
for(int i=0;i<20;i++)
{
//随机选出下面要闪烁的是哪个LED
//TODO: 使用RandomCondition中的引脚数组的方法,将数字和具体的引脚分开
On_Led_Pin=random(2,6);
digitalWrite(On_Led_Pin, LOW);
//音效中的时间,是把下边灯光的时间算法抄过来的
tone(BuzzPin, NOTE_C5+i*15, 20);
delay(20+i*15);
digitalWrite(On_Led_Pin, HIGH);
}
//播放开始游戏音效
tone(BuzzPin, NOTE_C6, 500);
delay(500);
noTone(BuzzPin);
}
//生成一个十进制数字,通过LED用二进制的形式展示出来,作为目标引线
void RandomCondition(byte &defusePinIndex)
{
//生成1~8的随机数
byte condition = random(1, 9);
//转化成存储引线所在引脚的数组的下标(数字-1),这样,随机数和具体的引脚就分离开了
if(defusePinIndex == 0)
{
defusePinIndex = condition-1;
}
else
{
//如果选出的下标出上一次出题的下标相同,则再次求随机数,直到选出不同的数
while(previousdefusePinIndex == condition-1)
{
condition = random(1, 9);
}
//形式参数defusePinIndex传入了全局变量的引用,一改变它的值,全局函数的值就跟着变了
defusePinIndex = condition-1;
}
//Serial.print("题目随机数(十进制):");
Serial.print("目标引线序号(从1开始,从下往上数):");
Serial.println(condition);
//将目标引线序号用两进制表示
bool ledStateArr[4];
for(int i=3; i>= 0; i--)
{
//bitRead函数把十进制数读取为二进制数时,序号参数最右边是0,往左数是1、2、3。
//而数组序号最左边是0,往右数是1、2、3,两者相反。
//所以,在把bitRead函数读取的数据存入数组时,要把序号(下标)颠倒一下,
//把bitRead的3号位(最左边)存到数组的0号位(3-i,对应也在最左边),相应于只把数值的序号颠倒而数值不变。
//最后,数组中的值从左(小序号)到右(大序号)输出,就可以用LED显示二进制数了。
ledStateArr[3-i] = bitRead(condition, i);
}
//在控制台输出目标引线序号(两进制数)
Serial.print("目标引线序号(二进制):");
for(int i=0; i<4; i++)
{
Serial.print(ledStateArr[i]);
}
Serial.println();
//在LED上显示目标引线序号(二进制)
for(int i=0; i<4; i++)
{
digitalWrite(ledPinArr[i], ledStateArr[i]);
}
}
//检查是否要引爆炸弹
void CheckBombStatus()
{
//遍历8根引线
for(int i=0;i<8;i++)//Scan Status of FusePin
{
//如果有一条引线被拔了,且不是上一轮的正确答案,再进行进一步判断
if(digitalRead(fuseinPins[i])==HIGH && i != previousdefusePinIndex)
{
//如果被拔的不是目标引线,就引爆炸弹
if(fuseinPins[i]!=fuseinPins[defusePinIndex])
{
Serial.println();
Serial.print("Wrong Pin! Trigger Bomb!");
Serial.println();
TriggerBomb();
}
else
{
//如果拆的这根是目标引线,且之前已经成功拆过一次引线(共需拆两次),则游戏胜利
//20241029今天这里不知怎么地把FirstSucess==true变成了FirstSucess=true,只有一个等号,
//就成了赋值,费力检查了半天,还是要学C专家编程里的方法与值写在左边
if(true==FirstSucess)
{
BombDefused();
}
//否则进入下一题,并播放提示音
else
{
nextQuestion();
Serial.println("下一题:");
delay(1000);
tone(BuzzPin, NOTE_C4, 500);
delay(500);
tone(BuzzPin, NOTE_E4, 500);
delay(500);
tone(BuzzPin, NOTE_G4, 500);
delay(500);
tone(BuzzPin, NOTE_C5, 500);
delay(500);
//出下一道题的时间要吃掉一秒,这里加上
timer++;
//当前是第一次成功拆线,把当前的目标引线下标存储为“上一次目标引线下标”
previousdefusePinIndex = defusePinIndex;
//RandomCondition(defusePinIndex);修改了StartStatus变量值,GameLogic函数会自动调用它,这里不用单独调用
FirstSucess=true;
StartStatus=false;
}
}
Serial.println();
}
}
}
//屏幕显示成功,并播放音乐
void BombDefused()
{
EndStatus=true;
gameVictory();
//响一阵音乐
for(int thisNote = 0; thisNote < 8; thisNote++) {
// to calculate the note duration, take one second
// divided by the note type.
//e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc.
int noteDuration = 1000/noteDurations[thisNote];
tone(BuzzPin, melody[thisNote],noteDuration);
// to distinguish the notes, set a minimum time between them.
// the note's duration + 30% seems to work well:
int pauseBetweenNotes = noteDuration * 1.30;
delay(pauseBetweenNotes);
// stop the tone playing:
noTone(BuzzPin);
}
}
void TriggerBomb()
{
//舵机转动,刺破气球
BombServo.write(30);
gameOver();
//digitalWrite(BuzzPin, HIGH);
tone(BuzzPin, NOTE_G3, 1000);
delay(1000);
tone(BuzzPin, NOTE_E3, 1000);
delay(1000);
tone(BuzzPin, NOTE_C3, 1000);
delay(1000);
//标记游戏结束
EndStatus=true;
//后边研究一下这个继电器什么效果
//delay(100);
//digitalWrite(BombRelayA, HIGH);
// digitalWrite(BombRelayB, HIGH);
// delay(500);
//digitalWrite(BombRelayA, LOW);
//digitalWrite(BombRelayB, LOW);
//delay(5000);
//digitalWrite(BuzzPin, LOW);
// noTone(BuzzPin);
}
//剩余时间越少,响的频率越快
//不过在wokwi里效果始终不太明显,用实物再试试
void SoundEffect(int time)
{
if(time>10)
{
tone(BuzzPin, NOTE_C6, 100);
delay(100);
}
if(time<=10&&time>=5)
{
for(int i=0;i<2;i++)
{
tone(BuzzPin, NOTE_C6, 50);
delay(100);
noTone(BuzzPin);
}
}
if(time<=5)
{
for(int i=0;i<4;i++)
{
tone(BuzzPin, NOTE_C6, 20);
delay(50);
noTone(BuzzPin);
}
}
}
void explosionAnimation(void) {
//每一个函数开始之前,先清除之前的缓存
display.clearDisplay();
for(int16_t i=0 ; i< 100; i+=3) {
// The INVERSE color is used so circles alternate white/black
display.fillCircle(display.width() / 2, display.height() / 2, i, SSD1306_INVERSE);
display.display(); // Update screen with each newly-drawn circle
delayMicroseconds(10);
}
}
/*
void blink(void){
for(int i = 0; i<5; i++)
{
display.clearDisplay();
display.display();
delay(50);
display.clearDisplay();
for(int16_t i=0 ; i< 100; i+=3) {
// The INVERSE color is used so circles alternate white/black
display.fillCircle(display.width() / 2, display.height() / 2, i, SSD1306_INVERSE);
}
display.display();
delay(100);
}
}*/
void showLogo(void) {
display.clearDisplay();
display.drawBitmap(0, 0, logo_bmp, LOGO_WIDTH, LOGO_HEIGHT, 1);
display.drawBitmap(64, 0, tdxx_bmp1, 32, 32, 1);
display.drawBitmap(96, 0, tdxx_bmp2, 32, 32, 1);
display.drawBitmap(64, 32, tdxx_bmp3, 32, 32, 1);
display.drawBitmap(96, 32, tdxx_bmp4, 32, 32, 1);
display.display();
delay(2000);
}
void timerCount(int num) {
display.clearDisplay();
display.drawBitmap(0, 16, timer_bmp1, 32, 32, 1);
display.drawBitmap(32, 16, timer_bmp2, 32, 32, 1);
display.drawBitmap(96, 16, timer_bmp3, 32, 32, 1);
if(num > 9){
display.setCursor(65, 40);
}
else{
display.setCursor(74, 40);
}
display.print(num);
display.display();
}
void gameStart()
{
display.clearDisplay();
display.drawBitmap(0, 16, gameStart_bmp1, 32, 32, 1);
display.drawBitmap(32, 16, gameStart_bmp2, 32, 32, 1);
display.drawBitmap(64, 16, gameStart_bmp3, 32, 32, 1);
display.drawBitmap(96, 16, gameStart_bmp4, 32, 32, 1);
display.display();
}
void nextQuestion()
{
display.clearDisplay();
display.drawBitmap(0, 16, nextQuestion_bmp1, 32, 32, 1);
display.drawBitmap(32, 16, nextQuestion_bmp2, 32, 32, 1);
display.drawBitmap(64, 16, nextQuestion_bmp3, 32, 32, 1);
display.drawBitmap(96, 16, nextQuestion_bmp4, 32, 32, 1);
display.display();
}
void gameOver()
{
explosionAnimation();
display.clearDisplay();
display.drawBitmap(0+1, 16, gameOver_bmp1, 32, 32, 1);
display.drawBitmap(32+1, 16, gameOver_bmp2, 32, 32, 1);
display.drawBitmap(64+1, 16, gameOver_bmp3, 32, 32, 1);
display.drawBitmap(96+1, 16, gameOver_bmp4, 32, 32, 1);
display.display();
}
void gameVictory()
{
flowerAnimate(flower_bmp, FLOWER_WIDTH, FLOWER_HEIGHT);
display.clearDisplay();
display.drawBitmap(0, 16, gameVictory_bmp1, 32, 32, 1);
display.drawBitmap(32, 16, gameVictory_bmp2, 32, 32, 1);
display.drawBitmap(64, 16, gameVictory_bmp3, 32, 32, 1);
display.drawBitmap(96, 16, gameVictory_bmp4, 32, 32, 1);
display.display();
}
void flowerAnimate(const uint8_t *bitmap, uint8_t w, uint8_t h) {
int8_t f, icons[NUMFLAKES][3];
// Initialize 'snowflake' positions
for(f=0; f< NUMFLAKES; f++) {
icons[f][XPOS] = random(1 - LOGO_WIDTH, display.width());
icons[f][YPOS] = -LOGO_HEIGHT;
icons[f][DELTAY] = random(1, 6);
}
for(int i=0; i < 40; i++) { // Loop forever...
display.clearDisplay(); // Clear the display buffer
// Draw each snowflake:
for(f=0; f< NUMFLAKES; f++) {
display.drawBitmap(icons[f][XPOS], icons[f][YPOS], bitmap, w, h, SSD1306_WHITE);
}
display.display(); // Show the display buffer on the screen
delay(10); // Pause for 1/10 second
// Then update coordinates of each flake...
for(f=0; f< NUMFLAKES; f++) {
icons[f][YPOS] += icons[f][DELTAY];
// If snowflake is off the bottom of the screen...
if (icons[f][YPOS] >= display.height()) {
// Reinitialize to a random position, just off the top
icons[f][XPOS] = random(1 - LOGO_WIDTH, display.width());
icons[f][YPOS] = -LOGO_HEIGHT;
icons[f][DELTAY] = random(1, 6);
}
}
}
}