遊戲裡的程式設計遊戲
掃雷是一個男女老少皆宜的一個小遊戲,讀大學的時候見同學玩的多,自己主要以暗黑2,泡泡堂,街霸為主。那個時候覺得掃雷是個很神奇的東西,滑鼠左擊兩下居然可以掃出一片,當時連模擬掃雷的想法都沒有。
在我的大學四年,從教學資源上確實乏善可陳,作為醫學院校的第一屆工科專業,我搜索了整個三層的圖書館,只找到一本vb資料庫程式設計的書。說好的理科學位,最後居然拿到管理學位,對於滿滿的計算機課程,我只能黯然神傷。記得當初學習計算機的時候,好像每學會一種技術,甚至一個API,都覺得自己技能升級了一般,內心裡都是練出獨孤九劍,唯我獨尊的想法,直到多年後,才慢慢認識到一個人無論怎麼學都有限,唯有一個開放的思維,才能運用各家所長,為我所用。門戶之見,閉門造車,害人不淺。電影四大名捕完結了,有網友說,最遺憾的是,再也見不到黃秋生吃火鍋了,對我而言,最遺憾的莫過於再也聽不到裡面能讓我感動的臺詞。記得名捕2開頭,諸葛正我和捕神的對話:"就像這杯水,你拿著它不放,能裝的水一輩子就這麼多了 ,放棄了,它可以裝的水就不可估量了”“無論何門何派 ,我們修的都是心 ,都是一條路 ,往上能到那裡,就看我們自己了”。有人會說我入戲太深,但換角度想想,你是不是剛好就拿著一杯滿滿的水呢。
扯遠了,現在說說掃雷的整體思路,它大致分為幾個部分,初始化,生成地雷的資訊,計算雷周圍的資訊,滑鼠左鍵單擊,雙擊和右鍵單擊事件。
地雷的初始化的關鍵是隨機生成地雷的位置,程式隨機生成雷的位置,將這幾個位置標誌為地雷,然後對整個雷區進行掃描,針對每個點統計其周圍八個點的地雷數目,然後設定該點的數字。
先解釋一下關於雷區塊的計算機,為了簡便,我用一個位元組去表示一個塊的資訊,低四位(b0-b4)表示雷的基本資訊(0-8)表示塊周圍的雷的個數,按上圖所示,八領域最多有8個雷,9表示地雷。除了這些基本資訊之外,塊還有是否被滑鼠點開過,是否被標記過等資訊。
這裡用D7和D6分別表示,對於D7而言0表示未被點開,1表示已經被點開過,同理,對於D6而言,0表示未標記,1表示已經被標記
雷的左鍵事件需要一個遞迴的過程,當點中一個周圍沒有地雷的塊或者相應的周邊地雷已經全部被標記的,需要對其八領域做遞迴搜尋,這是掃雷有時候能掃出一片的原理所在。具體參見程式碼
以下為初始化效果
全部未點開的狀態
全部點開的狀態
程式碼測試
以下為全部程式碼資訊,跟windows的那套演算法不完全一樣,為了演示方便,做了一些簡化。
#include<stdlib.h>
#include <stdio.h>
#include <string.h>
#include<time.h>
#define UNFOLD_MASK 0x80
#define FOLD_MASK 0x7F
#define UNFOLD(X) X = (X|UNFOLD_MASK)
#define FOLD(X) X = (X&FOLD_MASK);MINER_UNMASK(X);
#define IS_UNFOLD(X) (X&UNFOLD_MASK)
#define M_MASK 0x40
#define M_UNMASK 0xBF
#define MINER_MASK(X) X = (X|M_MASK)
#define MINER_UNMASK(X) X = (X&M_UNMASK)
#define IS_M_MASK(X) (X&M_MASK)
#define FOLD_EMPTY 0
#define MINER 9
#define BLOCK_SIZE 10 //雷區大小
#define MINE_COUNT 10 //地雷的數量
#define IS_OUT_BOUND(X) (X<0||X>=BLOCK_SIZE)
int blocks[BLOCK_SIZE*BLOCK_SIZE] = {0};
char *block_empty = "■";
char *block_encodings[12] = {
"□","1 ","2 ","3 ",
"4 ","5 ","6 ","7 ",
"8 ","¤"
};
/*force==1時,強制輸出資訊,除錯用*/
void print_one_block(int block_data,int force)
{
if(force) UNFOLD(block_data);
if(IS_M_MASK(block_data)&&(!IS_UNFOLD(block_data)))
{
printf("★");
}
else if(!IS_UNFOLD(block_data)) //未掃塊
{
printf("%s",block_empty);
}
else
{
//block_data =
block_data &= 0x0F;
printf("%s",block_encodings[block_data]);
}
}
void srand_time()
{
srand((unsigned)time(NULL));
}
/*對點x,y所在塊的八個相鄰塊進行檢測目標數量,
這裡可以是地雷數,也可以標記雷數*/
int cal_mine_count(int x,int y,int *bs,int target,int target_mask)
{
int idx = x*BLOCK_SIZE+y;
int i,j,count=0;
int k =x,z = y;
bs[idx] = bs[idx]&0x0F;
if(bs[idx]==MINER) return bs[idx];//該點為地雷,直接返回
for(i=-1;i<=1;i++)
for(j=-1;j<=1;j++)
{
x =k+i;
y =z+j;
if(IS_OUT_BOUND(x)||IS_OUT_BOUND(y)) continue;//超過上下限返回
idx = x*BLOCK_SIZE+y;
if((bs[idx]&target_mask)==target) count++;
}
return count;//返回目標個數
}
void init_mine(int *bs)
{
int i,j,x;
for(i=0;i<MINE_COUNT;i++)
{
x = rand()%(BLOCK_SIZE*BLOCK_SIZE);
if(bs[x]==FOLD_EMPTY)//該點未被設定過
{bs[x] = MINER;}
else //如果已經被設定,放棄這次操作
{i--;} //效率不高,但是相對簡便,
}
for(i=0;i<BLOCK_SIZE;i++)
for(j=0;j<BLOCK_SIZE;j++)
{
x= i*BLOCK_SIZE+j;
bs[x] = cal_mine_count(i,j,bs,MINER,0x0F);
}
}
void print_blocks(int *bs)
{
int i,j;
for(j=0;j<BLOCK_SIZE;j++) printf(" %d",j);
printf("\n");
for(i=0;i<BLOCK_SIZE;i++)
{
printf("%d ",i);
for(j=0;j<BLOCK_SIZE;j++)
{
print_one_block(bs[i*BLOCK_SIZE+j],0);
}
printf("\n");
}
}
void check_blank_point(int x,int y,int *bs)
{
int idx = x*BLOCK_SIZE+y;
int i,j,count=0;
int k =x,z = y;
//if(IS_UNFOLD(bs[idx])) return;//已經掃描過,返回
UNFOLD(bs[idx]); //標記已經掃描
if((bs[idx]&0x0F)!=0) return;
for(i=-1;i<=1;i++)
for(j=-1;j<=1;j++)
{
x =k+i;
y =z+j;
if(IS_OUT_BOUND(x)||IS_OUT_BOUND(y)) continue;
idx = x*BLOCK_SIZE+y;
if(bs[idx]==MINER) continue;
if(IS_UNFOLD(bs[idx])) continue;
check_blank_point(x,y,bs);
}
// return count;//返回目標個數
}
/*模擬右鍵點選點x,y事件*/
void right_click(int x,int y,int *bs)
{
int idx = x*BLOCK_SIZE+y;
if(IS_UNFOLD(bs[idx]))
{
printf("位置(%d,%d)已經被掃描過,無需標記\n",x,y);
return;
}
if(IS_M_MASK(bs[idx]))
{
MINER_UNMASK(bs[idx]);
printf("位置(%d,%d)被取消標記\n",x,y);
}
else
{
MINER_MASK(bs[idx]);
printf("位置(%d,%d)已經被標記\n",x,y);
}
}
void left_click(int x,int y,int *bs)
{
int idx = x*BLOCK_SIZE+y;
int i;
if(IS_UNFOLD(bs[idx])) return; //已經掃描部分,跳過
UNFOLD(bs[idx]);
if((bs[idx]&0x0F)==MINER)
{
printf("踩到雷(%d,%d)了,遊戲結束\n",x,y);
return;
}
bs[idx]&=0x0F;
if((bs[idx]&0x0F)==0)//遞迴掃描所有空點
{
check_blank_point(x,y,bs);//對於周邊地雷數為0的點進行遞迴掃描,
//開啟周邊非地雷的快,若有同樣的點,
//對該點進行同樣的操作
}
else
{
i = cal_mine_count(x,y,bs,M_MASK,M_MASK);
if(i==(bs[idx]&0x0F))
{
check_blank_point(x,y,bs);
}
}
}
void main()
{
init_mine(blocks);
right_click(0,0,blocks);
right_click(0,0,blocks);
left_click(0,9,blocks);
left_click(0,0,blocks);
print_blocks(blocks);
}
有的朋友很多會發現用上面的程式碼生成的雷區資訊都是一樣的,原因是出在rand呼叫的隨機序列沒有發生變化,這裡需要在init_mine函式前,加入srand_time函式以生成新的偽隨機序列,為了說明問題,我需要保持雷區資訊不變,就沒有加入該函式。
我個人曾經除錯過windows自帶的掃雷程式,它關於雷區的表示方法不大一樣。後續會說說怎麼編寫一個掃雷的作弊器,功能是改寫雷區的資訊,達到快速掃雷的目的。這裡牽涉到動態除錯和反彙編的一些基本技能,所以,高手笑過,新手飄過。
下一節預告 FLASH 提燈過橋遊戲