五子棋基本玩法-AI實現
參考:http://game.onegreen.net/wzq/HTML/142336.html
對於正式接觸五子棋時間不長的朋友來說,瞭解和掌握一些基本棋型的名稱及其特點是非常重要的。不僅可以加深對棋的理解,更重要的是可以方便自己與其他棋友交流。
最常見的基本棋型大體有以下幾種:連五,活四,衝四,活三,眠三,活二,眠二。
①連五:顧名思義,五顆同色棋子連在一起,不需要多講。
圖2-1
②活四:有兩個連五點(即有兩個點可以形成五),圖中白點即為連五點。
稍微思考一下就能發現活四出現的時候,如果對方單純過來防守的話,是已經無法阻止自己連五了。
圖2-2
③衝四
相對比活四來說,衝四的威脅性就小了很多,因為這個時候,對方只要跟著防守在那個唯一的連五點上,衝四就沒法形成連五。
圖2-3 圖2-4 圖2-5
④活三:可以形成活四的三,如下圖,代表兩種最基本的活三棋型。圖中白點為活四點。
活三棋型是我們進攻中最常見的一種,因為活三之後,如果對方不以理會,將可以下一手將活三變成活四,而我們知道活四是已經無法單純防守住了。所以,當我們面對活三的時候,需要非常謹慎對待。在自己沒有更好的進攻手段的情況下,需要對其進行防守,以防止其形成可怕的活四棋型。
圖2-6
其中圖2-7中間跳著一格的活三,也可以叫做跳活三。
⑤眠三:只能夠形成衝四的三,如下各圖,分別代表最基礎的六種眠三形狀。圖中白點代表衝四點。眠三的棋型與活三的棋型相比,危險係數下降不少,因為眠三棋型即使不去防守,下一手它也只能形成衝四,而對於單純的衝四棋型,我們知道,是可以防守住的。
圖2-8 圖2-9 圖2-10
2-11 圖2-12 圖2-13
如上所示,眠三的形狀是很豐富的。對於初學者,在下棋過程中,很容易忽略不常見的眠三形狀,例如圖2-13所示的眠三。
有新手學了活三眠三後,會提出疑問,說活三也可以形成衝四啊,那豈不是也可以叫眠三?
會提出這個問題,說明對眠三定義看得不夠仔細:眠三的的定義是,只能夠形成衝四的三。而活三可以形成眠三,但也能夠形成活四。
此外,在五子棋中,活四棋型比衝四棋型具有更大的優勢,所以,我們在既能夠形成活四又能夠形成衝四時,會選擇形成活四。
溫馨提示
後邊禁手判斷的時候也會有所應用。
⑥活二:能夠形成活三的二,如下圖,是三種基本的活二棋型。圖中白點為活三點。
活二棋型看起來似乎很無害,因為他下一手棋才能形成活三,等形成活三,我們再防守也不遲。但其實活二棋型是非常重要的,尤其是在開局階段,我們形成較多的活二棋型的話,當我們將活二變成活三時,才能夠令自己的活三綿綿不絕微風裡,讓對手防不勝防。
圖2-14 圖2-15 圖2-16
⑦眠二:能夠形成眠三的二。圖中四個為最基本的眠二棋型,細心且喜歡思考的同學會根據眠三介紹中的圖2-13找到與下列四個基本眠二棋型都不一樣的眠二。圖中白點為眠三點。
圖2-17 圖2-18
圖2-19 圖2-20
由此 可以確定 所有的棋型,連五,活四,衝四,活三,眠三,活二,眠二。
每一種棋型對應不同的分數,依次遞減
在C#裡 可以用 一個字典,提前儲存起來
public class ChessAI :MonoBehaviour
{//分數字典
protected Dictionary<string, float> toScore = new Dictionary<string, float>();
public ChessType mChessType = ChessType.Black;//表示下棋的型別
protected float[,] score = new float[15, 15];//棋盤裡 棋子對應的分數的二維陣列,
void Start()
{
//眠二
toScore.Add("aa__", 100);
toScore.Add("__aa", 100);
toScore.Add("__a_a", 100);
toScore.Add("_a__a", 100);
toScore.Add("a__a", 100);
toScore.Add("a__a_", 100);
toScore.Add("a_a__", 100);
//活二
toScore.Add("__aa__", 500);
toScore.Add("__aa_", 500);
toScore.Add("_a_a_", 500);
toScore.Add("_a__a_", 500);
toScore.Add("_aa__", 500);
//眠3
//toScore.Add("__aaa", 1000);
toScore.Add("a_a_a", 1000);
toScore.Add("_aa_a", 1000);
toScore.Add("a_aa_", 1000);
toScore.Add("_a_aa", 1000);
toScore.Add("aa_a_", 1000);
toScore.Add("aa__a", 1000);
toScore.Add("aaa__", 1000);
toScore.Add("_aa_a_", 9000);//跳活三
toScore.Add("_a_aa_", 9000);
toScore.Add("_aaa_", 10000); //活三
toScore.Add("a_aaaa", 15000);//衝四
toScore.Add("aa_aa", 15000);
toScore.Add("_aaaa", 15000);
toScore.Add("aaa_a", 15000);
toScore.Add("aaaa_", 15000);
toScore.Add("_aaaa_", 100000);//活四
toScore.Add("aaaaa",float.MaxValue);//連五
}
protected virtual void FixedUpdate()
{
//下棋
if (ChessBoard.Instance.turn==mChessType&& ChessBoard.Instance.timer>0.3f)
{
PlayerChess();
}
}
//設定分數
public void SetScore(int[] pos)
{
score[pos[0], pos[1]] = 0;
//黑棋 加入評分
CheckOneLine(pos, new int[2] { 1, 0 }, 1);
CheckOneLine(pos, new int[2] { 1, 1 }, 1);
CheckOneLine(pos, new int[2] { 1, -1 }, 1);
CheckOneLine(pos, new int[2] { 0, 1 }, 1);
//白棋 加入評分
CheckOneLine(pos, new int[2] { 1, 0 }, 2);
CheckOneLine(pos, new int[2] { 1, 1 }, 2);
CheckOneLine(pos, new int[2] { 1, -1 }, 2);
CheckOneLine(pos, new int[2] { 0, 1 }, 2);
}
/// <summary>
/// 用來檢查一行裡 棋子的分數,上左, 左斜右斜,是右偏移量決定的,這裡分別對左邊,和右邊進行檢測。
/// </summary>
/// <param name="pos">當前下棋的位置</param>
/// <param name="offset">偏移量,0是x,1是y</param>
/// <param name="chess">棋子型別</param>
public virtual void CheckOneLine(int[] pos, int[] offset, int chess)
{
bool LFirst = true, lStop=false, rStop = false;//LFirst 是否掃左邊,LStop 左邊停止
int AllNum = 1;//記錄一共掃了多少課棋子,掃到7課退出迴圈
string str = "a";
int ri = offset[0], rj = offset[1];//右邊的棋子的位置 i是X位置, j是Y位置
int li = -offset[0], lj = -offset[1];//左邊的棋子的位置
while (AllNum<7 && (!lStop||!rStop))
{
//左邊
if (LFirst)
{
//邊界處理
if ((pos[0] + li >= 0 && pos[0] + li < 15) &&
pos[1] + lj >= 0 && pos[1] + lj < 15&&!lStop)
{
//碰到 chess棋
if (ChessBoard.Instance.grids[pos[0] + li, pos[1] + lj] == chess)
{
AllNum++;
str = "a" + str;
}
else if (ChessBoard.Instance.grids[pos[0] + li, pos[1] + lj] == 0)
{
AllNum++;
//空位置
str = "_" + str;
//如果右邊沒 停止 就 往右邊
if (!rStop)
LFirst = false;
}
else
{
lStop = true; //停左邊, 遍歷右邊
if (!rStop)
LFirst = false;
//chess的對手棋
}
li -= offset[0]; //尋找下個 位置
lj -= offset[1];
}
else //出邊界 停止 遍歷右邊
{
lStop = true; //停左邊, 遍歷右邊
if (!rStop)
LFirst = false;
}
}
else //右邊
{
//邊界處理
if ((pos[0] + ri >= 0 && pos[0] + ri < 15) &&
pos[1] + rj >= 0 && pos[1] + rj < 15 &&!LFirst )
{
//碰到 chess棋
if (ChessBoard.Instance.grids[pos[0] + ri, pos[1] + rj] == chess)
{
AllNum++;
str += "a" ;
}
else if (ChessBoard.Instance.grids[pos[0] + ri, pos[1] + rj] == 0)
{
AllNum++;
//空位置
str += "_" ;
//如果右邊沒 停止 就 往右邊
if (!lStop)
LFirst = true;
}
else
{
rStop = true; //停左邊, 遍歷右邊
if (!lStop)
LFirst = true;
//chess的對手棋
}
ri += offset[0]; //尋找下個 位置
rj += offset[1];
}
else //出邊界 停止 遍歷右邊
{
rStop = true; //停左邊, 遍歷右邊
if (!lStop)
LFirst = true;
//chess的對手棋
}
}
}
string cmpStr = "";
foreach (var keyVar in toScore )
{
//因為 str遍歷出來是7位 可能有多種情況, 遍歷取出 所有情況裡邊 值大的
if (str.Contains(keyVar.Key ))
{
if (cmpStr != "")
{
if (toScore[keyVar.Key]>toScore[cmpStr] )
{
cmpStr = keyVar.Key;
}
}
else
{//第一次
cmpStr = keyVar.Key;
}
}
}
if (cmpStr!="")
{
score[pos[0], pos[1]] += toScore[cmpStr];
}
}
public void PlayerChess()
{
if (ChessBoard.Instance.chessStack.Count == 0)
{
if (ChessBoard.Instance.PlayChess(new int[2] { 7, 7 }))
ChessBoard.Instance.timer = 0;
return;
}
float maxScore = 0;
int[] maxPos = new int[2] { 0, 0 };
//遍歷棋盤裡的棋子 尋找一個最佳的下棋位置,根據平方函式算出,
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
if (ChessBoard.Instance.grids[i, j] == 0)
{
SetScore(new int[2] { i, j });
if (score[i, j] >= maxScore)
{
maxPos[0] = i;
maxPos[1] = j;
maxScore = score[i, j];
}
}
}
}
if (ChessBoard.Instance.PlayChess(maxPos))
ChessBoard.Instance.timer = 0;
}
}
這裡的a就代表 黑棋子,_代表空位置
掃棋的演算法比較簡單,這裡以AI為黑子 為例。
先定義一個String變數 str="a", 表示落子,和一個變數Num來儲存一共掃了多少個,
先從左邊開始掃,遇到黑棋子,就讓它 str="a"+str;左邊掃,掃到這個棋子"a",肯定是在落子的左邊,再讓變數Num++
掃到左邊為空的 情況,就讓它str="_"+str;再讓變數Num++,遇到白子或者邊界,就開始掃右邊,處理情況和掃左邊類似,只不過在遇到黑子的時候,讓字元 str+="a";因為掃到的黑子是在落子的右邊的,因此,"a"在落子的右邊,遇到空子也一樣;
pos是當前落子的位置, offset是偏移量,用來應付不同的方法,上下左右,左斜右斜,chess是棋子型別,AI不僅要計算出自己的棋子的最佳落子位置,也要計算出對手的最佳落子位置,搶在對方之前落子。
這裡棋盤是15*15的,ChessBoard.Instance.grids儲存了所有的棋子,
CheckOneLine(int[] pos, int[] offset, int chess) 這裡的pos
public virtual void CheckOneLine(int[] pos, int[] offset, int chess)
{
bool LFirst = true, lStop=false, rStop = false;
int AllNum = 1;
string str = "a";
int ri = offset[0], rj = offset[1];//右邊 i是X位置, j是Y位置
int li = -offset[0], lj = -offset[1];//左邊
while (AllNum<7 && (!lStop||!rStop))
{
//左邊
if (LFirst)
{
//邊界處理
if ((pos[0] + li >= 0 && pos[0] + li < 15) &&
pos[1] + lj >= 0 && pos[1] + lj < 15&&!lStop)
{
//碰到 chess棋
if (ChessBoard.Instance.grids[pos[0] + li, pos[1] + lj] == chess)
{
AllNum++;
str = "a" + str;
}
else if (ChessBoard.Instance.grids[pos[0] + li, pos[1] + lj] == 0)
{
AllNum++;
//空位置
str = "_" + str;
//如果右邊沒 停止 就 往右邊
if (!rStop)
LFirst = false;
}
else
{
lStop = true; //停左邊, 遍歷右邊
if (!rStop)
LFirst = false;
//chess的對手棋
}
li -= offset[0]; //尋找下個 位置
lj -= offset[1];
}
else //出邊界 停止 遍歷右邊
{
lStop = true; //停左邊, 遍歷右邊
if (!rStop)
LFirst = false;
}
}
else //右邊
{
//邊界處理
if ((pos[0] + ri >= 0 && pos[0] + ri < 15) &&
pos[1] + rj >= 0 && pos[1] + rj < 15 &&!LFirst )
{
//碰到 chess棋
if (ChessBoard.Instance.grids[pos[0] + ri, pos[1] + rj] == chess)
{
AllNum++;
str += "a" ;
}
else if (ChessBoard.Instance.grids[pos[0] + ri, pos[1] + rj] == 0)
{
AllNum++;
//空位置
str += "_" ;
//如果右邊沒 停止 就 往右邊
if (!lStop)
LFirst = true;
}
else
{
rStop = true; //停左邊, 遍歷右邊
if (!lStop)
LFirst = true;
//chess的對手棋
}
ri += offset[0]; //尋找下個 位置
rj += offset[1];
}
else //出邊界 停止 遍歷右邊
{
rStop = true; //停左邊, 遍歷右邊
if (!lStop)
LFirst = true;
//chess的對手棋
}
}
}
string cmpStr = "";
foreach (var keyVar in toScore )
{
//因為 str遍歷出來是7位 可能有多種情況, 遍歷取出 所有情況裡邊 值大的
if (str.Contains(keyVar.Key ))
{
if (cmpStr != "")
{
if (toScore[keyVar.Key]>toScore[cmpStr] )
{
cmpStr = keyVar.Key;
}
}
else
{//第一次
cmpStr = keyVar.Key;
}
}
}
if (cmpStr!="")
{
score[pos[0], pos[1]] += toScore[cmpStr];
}
}
public class ChessBoard : MonoBehaviour
{
public static ChessBoard Instance { get { return _instance; } }
static ChessBoard _instance;
public ChessType turn = ChessType.Black;
public int[,] grids;//存 1 和2 ,表示黑棋和白棋
public GameObject[] prefabs;
public float timer = 0;
public bool gameStart = true;
public Stack< Transform> chessStack = new Stack<Transform>();//先入後出。
// Use this for initialization
Transform parent;
private void Awake()
{
if (Instance==null)
{
_instance = this;
}
grids = new int[15, 15];
parent = GameObject.Find("Parent").transform;
}
private void FixedUpdate()
{
timer += Time.deltaTime;
}
// Update is called once per frame
void Update ()
{
}
/// <summary>
///
/// </summary>
/// <param name="pos">落子的位置</param>
/// <returns></returns>
public bool PlayChess(int[] pos)
{
if (!gameStart) { return false; }
if (pos[0] < 0 || pos[0] > 14 || pos[1] < 0 || pos[1] > 14)
{
return false;
}
//判斷 當前點是否為0 是否下過棋
if (grids[pos[0], pos[1]] != 0) return false;
//黑 是1
if (turn == ChessType.Black)
{//生成黑棋
GameObject go= Instantiate(prefabs[0], new Vector3(pos[0] - 7f, pos[1] - 7f, 0), Quaternion.identity);
chessStack.Push(go.transform);
go.transform.SetParent(parent);
grids[pos[0], pos[1]] = 1;
//判斷勝負
if (CheckWiner(pos))
{
GameEnd();
}
// 下輪
turn = ChessType.White;
}
else if (turn == ChessType.White)
{
//白棋
GameObject go= Instantiate(prefabs[1], new Vector3(pos[0] - 7f, pos[1] - 7f, 0), Quaternion.identity);
chessStack.Push(go.transform);
go.transform.SetParent(parent);
grids[pos[0], pos[1]] = 2;
//判斷勝負
if (CheckWiner(pos))
{
GameEnd();
}
turn = ChessType.Black;
}
return true;
}
public bool CheckWiner(int[] pos)
{
//橫
if (CheckOneLine(pos, new int[2] { 1, 0 })) return true;
//豎
if (CheckOneLine(pos, new int[2] { 0, 1 })) return true;
//左斜
if (CheckOneLine(pos, new int[2] { 1,1 })) return true;
//右斜
if (CheckOneLine(pos, new int[2] { 1,-1 })) return true;
return false;
}
void GameEnd()
{
gameStart = false;
Debug.Log(turn + "勝利");
}
/// <summary>
///
/// </summary>
/// <param name="pos">落子的位置</param>
/// <param name="offest">偏移量</param>
/// <returns></returns>
public void RetractChess()
{
if (chessStack.Count>1)
{
Transform tran = chessStack.Pop();//出棧
grids[(int) (tran.position.x + 7), (int)(tran.position.y + 7)] = 0;
Destroy(tran.gameObject);
tran = chessStack.Pop();//出棧
grids[(int)(tran.position.x + 7), (int)(tran.position.y + 7)] = 0;
Destroy(tran.gameObject);
}
}
public bool CheckOneLine(int[] pos,int[] offest)
{
int linkNum = 1;
//i為x j 為y 左邊
for (int i = offest[0], j = offest[1];( i + pos[0] >= 0 && i + pos[0] < 15 )&& (j+pos[1] >= 0 && j + pos[1] < 15); i += offest[0], j += offest[1])
{
if ((int)turn== grids[i+pos[0],j+pos[1]])
{
linkNum++;
}
else
{
break;
}
}
//右邊邊
for (int i =-offest[0], j = -offest[1]; (i + pos[0] >= 0 && i + pos[0] < 15) && (j + pos[1] >= 0 && j + pos[1] < 15); i -= offest[0], j -= offest[1])
{
if ((int)turn == grids[i + pos[0], j + pos[1]])
{
linkNum++;
}
else
{
break;
}
}
if (linkNum>4)
{
return true;
}
return false;
}
}
public enum ChessType
{
Watch,
Black,
White,
}
用博弈樹 和極小極大MiniMax演算法,和剪枝演算法, 可以實現更加智慧的AI