游戏规则
- 任意数量的玩家可以加入(包含1), 多对相通的牌被随机排列在一个网格中,牌面朝下。每个玩家依次选择两张牌,并将其正面朝上,这被认为是一次移动。如果牌面相等,则该玩家赢得这对牌,再进行一次。如果两张牌不同,则再次将牌面朝下,轮到下一个玩家。当最后一对被拿起时,游戏结束。拥有最多对子的玩家获胜。有可能出现并列第一的情况。
实际上这就是我们以前多多少少会玩到的记忆卡片游戏,从逻辑上讲并不复杂,限定了我们只能使用R语言和基础的包,那就稍有难度了
问题分析
我们可以通过将游戏规则拆分,将不同的逻辑部分独立成函数,在主函数体里调用他们,这样既可以减少代码量又可以增加代码可读性。
这个游戏显然包含以下逻辑:
- 玩家数量、场地大小直接决定游戏走向
- 谁先开始是随机,下一次轮到谁也是随机的,那么就一定要避免使用for循环,因为我们不可能按照顺序循环完所有对局的可能
- 输赢虽然包括平局,但也存在着绝对胜利的情况,比如8分制,玩家1已经获得5分,又或是4:3时,场地只剩一对卡片,那玩家1依然必然胜利。那么就不需要写复杂的胜利规则,直接计分即可
- 不假设玩家是理性的,也就是说他可能输入错误的标识或者重复得分的卡片,要避免程序奔溃需要在每次输入时进行检测,这意味着我们需要使用while循环
- 需要对玩家的得分情况进行显示和记录
- 将已经得分的卡片移除
- 给出明确的游戏提示
编写游戏逻辑(以2名玩家,16张牌为例)
- 加载package
library(stats, graphics, grDevices)
library(utils)
library(datasets, methods, base)
options(repr.plot.width = 5, repr.plot.height = 5) #设定绘制方图
op <- par(bg = "white") #背景为白色
- 限定玩家的输入规范
将横纵坐标都限定在(1,4)内
check <- function(cells_possible) {
cell_valid <- FALSE
while (!cell_valid) {
cell <- scan(what = integer(0) , nmax = 1, quiet = TRUE)
# Check validity of input
if (!(cell %in% cells_possible)) {
cat("Cell not valid. Again:")
} else{
cell_valid <- TRUE
}
}
return(cell)
}
- 限制玩家输入已经得分的卡片
check_rep <- function(v, df){
while (FALSE %in% is.na(df[which(df$first==v[1] & df$second==v[2]), ][1,])){
print('Card not valid, Again')
v = scan(what=integer(0), nmax=2, quiet = TRUE)
return(v)
}
return(v)
}
- 编写主体函数
n_row 和 n_col决定了游戏场地的大小,pch为卡片的花色, col为卡片的颜色,n_player为玩家数据。均由玩家客制
memory <- function(n_row = 1, n_col = 3,
pch=1:13, col = 1:8,
n_player = 2){}
- 绘制场地
主函数一旦被调用就意味着游戏开始,所以首先绘制游戏场地
x = 0.5:4.5 #画板x坐标
y = 0.5:4.5 #画板y坐标
plot(x,y, type='n', xlab="", ylab="", main="Memory",
xlim = c(0.5, 4.3), ylim = c(0.5, 4.3), xaxt = "n", yaxt = "n") #绘制游戏场地
grid(4, 4, lwd = 2) # 同时在x,y方向加入网格
axis(1, 1:4, las=0, tck=0) #修改坐标轴的表达方式(x)
axis(2, 1:4, las=0, tck=0) #修改坐标轴的表达方式(y)
- 预加载与规则
提前制定好一些全局规则,以免程序中途奔溃
if (n%%2==1){
stop("n_row * n_col must be an even number.\n")
}
if (length(pch) * length(col) < n/2){
stop("Not enough different \
possible cared(combinations of pch and col\
)were specified for the given
size of the playing field.")
}
p = length(pch) #花色的数量
c = length(col) #颜色的数量
set.seed(1234)
rows <- sample((1:(p*c)), size=n/2, replace=FALSE) #从所有组合(p*c)中抽取n/2张card的横坐标
set.seed(1234)
copy_rows <- sample(1:8, size=8, replace=FALSE) #另外n/2个card的横坐标, 相当于打乱rows的结果
coor <- data.frame(xc=rep(c(1:4), each=4), yc=rep(c(1:4), each=1)) #抽取card在场地中的x,y坐标
cards <- data.frame(face=rep(c(1:p), c), color=rep(c(1:c), each=p)) #生成card的花色与颜色
cards_n <- cards[rows, ] #按照rows的结果抽出n/2张card
copy <- cards_n[copy_rows,] #按照copy_rows的结果抽出另外n/2张card
cards_n <- rbind(cards_n, copy) #将两部分card合并为n张卡片
rownames(cards_n) <- 1:nrow(cards_n) #由于是随机抽出的, 所以重置索引
cards_n$xc <- coor$xc #将x坐标加入到卡组中去
cards_n$yc <- coor$yc #将y坐标加入到卡组中去
cex = 1.5 #空值卡片图像大小
pair = n_row * n_col / 2 #卡片的对数
score1 = 0 #1号玩家得分
score2 = 0 #2号玩家得分
x_scan1 <- c(1:2) #生成一个初始坐标,当玩家输入后会被覆盖
x_scan2 <- c(1:2) #同上
df = data.frame(first=0, second=0) #一个空数据框,用来存放已经得分的卡片
由于每次谁输谁赢是随机的,显然不能用for+ if的循环方式,因为每一次得分与不得分的情况太多,全部穷举完代码量已经爆炸了。方案是通过定义变量pair来判断现在还剩多少对未得分的卡片可供选择。 p a i r < 1 \rm{pair}<1 pair<1就代表着所有牌组都得分了,很明显这是一个while循环。现在,游戏大体上的思路就是:建立场地 → \to →制定规则 → \to →扫描玩家输入并开始循环 → \to →逻辑判断输赢和一些附属输出 → \to →比赛结束判断输赢 → \to →初始化。从整体上来看,代码的框架就是
while (pair) >1{
if (no==1){} #玩家1先开始的话执行
else{} #玩家2先开始的话执行
if (score1 > score2){} #1获胜
if (score1 == score2){} #平局
if (score1 < score2){} #2获胜
}
当随机抽中的玩家是1时,进入if体,2时进入else体,各自循环各自的,互不干扰
- 游戏进行
由于现在只有两个玩家,所以玩家号不是一就是二,所以使用直接编写if 和 else里的内容
if (no==1){
#第1玩家第1牌
cat(paste('Player', no, 'choose your first card (1: row, 2: colunm)!')) #提示玩家选牌
x_scan1[1] <- check(c(1:4))
x_scan1[2] <- check(c(1:4))#玩家选牌(最多两张)的坐标 [1]:横坐标 [2]:纵坐标 判断输入是否正确
x_scan1 <- check_rep(x_scan1, df) #检查这张牌是否得分
face1_1 = cards_n[which(cards_n$xc==x_scan1[1] & cards_n$yc==x_scan1[2]), 'face'] #根据坐标条件查询相应的花色
color1_1 = cards_n[which(cards_n$xc==x_scan1[1] & cards_n$yc==x_scan1[2]), 'color']#根据坐标条件查询相应的颜色
points(x_scan1[1], x_scan1[2], pch=face1_1, col=color1_1, cex=cex) #将玩家翻出的牌绘制出来
#第1玩家第2牌
cat(paste('Player', no, 'choose your second card (1: row, 2: colunm)!')) #提示玩家选牌
x_scan2[1] <- check(c(1:4))
x_scan2[2] <- check(c(1:4)) #scan方法get玩家选牌(最多两张)的坐标 [1]:横坐标 [2]:纵坐标
x_scan2 <- check_rep(x_scan2, df)
face1_2 = cards_n[which(cards_n$xc==x_scan2[1] & cards_n$yc==x_scan2[2]), 'face'] #根据坐标条件查询相应的花色
color1_2 = cards_n[which(cards_n$xc==x_scan2[1] & cards_n$yc==x_scan2[2]), 'color']#根据坐标条件查询相应的颜色
points(x_scan2[1], x_scan2[2], pch=face1_2, col=color1_2, cex=cex) #将玩家翻出的牌绘制出来
if (face1_1 == face1_2 & color1_1 == color1_2){ #判断两次卡牌是否一样
while (face1_1 == face1_2 & color1_1 == color1_2 & pair > 1) { #一样那就一直抽抽牌到不一样
pair = pair - 1 #显然while体里都是得分的牌,所以对数要-1
score1 = score1 + 1 #同理得分要+1
df <- rbind(df, x_scan1, x_scan2) #得分的牌要加入废牌堆用于查重
cat(paste('Correct Player', no, 'plays again! Current leaderboard:\n')) #告知玩家获胜, 继续翻牌
no2 = 2 #这是二号玩家
cat(paste(' Player', no, ' Player', no2, '\n'))
cat(paste(' ', score1, ' ', score2, '\n'))
cat(paste('Press [y] when you are ready to move on!')) #打印他们的比分
yes_no = scan(what = 'character', nmax = 1, quiet = TRUE)
points(x_scan1[1], x_scan1[2], pch=face1_1, col='white', cex=cex) #已经得分的牌就变为玩家的序号这样可以标识出谁在什么位置上得分了
points(x_scan2[1], x_scan2[2], pch=face1_2, col='white', cex=cex)
text(x_scan1[1], x_scan1[2], label=no, cex=2)#已经得分的牌就变为玩家的序号这样可以标识出谁在什么位置上得分了
text(x_scan2[1], x_scan2[2], label=no, cex=2)
if (yes_no == 'y'){ #我们要给玩家记忆和思考的时间,等玩家输入y后在继续执行
cat(paste('Player', no, 'choose your first card (1: row, 2: colunm)!')) #提示玩家选牌
x_scan1[1] <- check(c(1:4))
x_scan1[2] <- check(c(1:4)) #scan方法get玩家选牌(最多两张)的坐标 [1]:横坐标 [2]:纵坐标
x_scan1 <- check_rep(x_scan1, df)
face1_1 = cards_n[which(cards_n$xc==x_scan1[1] & cards_n$yc==x_scan1[2]), 'face'] #根据坐标条件查询相应的花色
color1_1 = cards_n[which(cards_n$xc==x_scan1[1] & cards_n$yc==x_scan1[2]), 'color']#根据坐标条件查询相应的颜色
points(x_scan1[1], x_scan1[2], pch=face1_1, col=color1_1, cex=cex) #将玩家翻出的牌绘制出来
cat(paste('Player', no, 'choose your second card (1: row, 2: colunm)!')) #提示玩家选牌
x_scan2[1] <- check(c(1:4))
x_scan2[2] <- check(c(1:4)) #scan方法get玩家选牌(最多两张)的坐标 [1]:横坐标 [2]:纵坐标
x_scan2 <- check_rep(x_scan2, df)
face1_2 = cards_n[which(cards_n$xc==x_scan2[1] & cards_n$yc==x_scan2[2]), 'face'] #根据坐标条件查询相应的花色
color1_2 = cards_n[which(cards_n$xc==x_scan2[1] & cards_n$yc==x_scan2[2]), 'color']#根据坐标条件查询相应的颜色
points(x_scan2[1], x_scan2[2], pch=face1_2, col=color1_2, cex=cex) #将玩家翻出的牌绘制出来
}
}
}
no = 2 #这里说明已经跳出while了,表明要么玩家1全胜,要玩家1至少有一次失败, 那2号玩家就成为新的1号玩家
if (pair==1){next}
else{ #由于玩家选出的牌不配对,需要给双方记忆和思考的时间
cat(paste('Wrong, Player', no, 'plays! Press [y], when you are ready to move on!\n'))
yes_no = scan(what = 'character', nmax = 1, quiet = TRUE)}
if (yes_no == 'y'){
points(x_scan1[1], x_scan1[2], pch=face1_1, col='white', cex=cex) #将牌重新隐藏起来
points(x_scan2[1], x_scan2[2], pch=face1_2, col='white', cex=cex) #将牌重新隐藏起来
next
}
}
- 另一个人的逻辑基本相同
else{
#第2玩家第1牌
cat(paste('Player', no, 'choose your first card (1: row, 2: colunm)!')) #提示玩家选牌
x_scan1[1] <- check(c(1:4))
x_scan1[2] <- check(c(1:4)) #scan方法get玩家选牌(最多两张)的坐标 [1]:横坐标 [2]:纵坐标
x_scan1 <- check_rep(x_scan1, df)
face1_1 = cards_n[which(cards_n$xc==x_scan1[1] & cards_n$yc==x_scan1[2]),'face'] #根据坐标条件查询相应的花色
color1_1 = cards_n[which(cards_n$xc==x_scan1[1] & cards_n$yc==x_scan1[2]),'color']#根据坐标条件查询相应的颜色
points(x_scan1[1], x_scan1[2], pch=face1_1, col=color1_1, cex=cex) #将玩家翻出的牌绘制出来
#第2玩家第2牌
cat(paste('Player', no, 'choose your second card (1: row, 2: colunm)!')) #提示玩家选牌
x_scan2[1] <- check(c(1:4))
x_scan2[2] <- check(c(1:4)) #scan方法get玩家选牌(最多两张)的坐标 [1]:横坐标 [2]:纵坐标
x_scan2 <- check_rep(x_scan2, df)
face1_2 = cards_n[which(cards_n$xc==x_scan2[1] & cards_n$yc==x_scan2[2]),'face'] #根据坐标条件查询相应的花色
color1_2 = cards_n[which(cards_n$xc==x_scan2[1] & cards_n$yc==x_scan2[2]),'color']#根据坐标条件查询相应的颜色
points(x_scan2[1], x_scan2[2], pch=face1_2, col=color1_2, cex=cex) #将玩家翻出的牌绘制出来
if (face1_1 == face1_2 & color1_1 == color1_2){ #判断两次卡牌是否一样
while (face1_1 == face1_2 & color1_1 == color1_2 & pair > 1) {
pair = pair - 1
score2 = score2 + 1
df <- rbind(df, x_scan1, x_scan2)
cat(paste('Correct Player', no, 'plays again! Current leaderboard:\n')) #如果获胜, 继续翻牌
no2 = 1
cat(paste(' Player', no2, ' Player', no, '\n'))
cat(paste(' ', score1, ' ', score2, '\n'))
cat(paste('Press [y] when you are ready to move on!'))
yes_no = scan(what = 'character', nmax = 1, quiet = TRUE)
points(x_scan1[1], x_scan1[2], pch=face1_1, col='white', cex=cex)
points(x_scan2[1], x_scan2[2], pch=face1_2, col='white', cex=cex)
text(x_scan1[1], x_scan1[2], label=no, cex=2)
text(x_scan2[1], x_scan2[2], label=no, cex=2)
if (yes_no == 'y'){
cat(paste('Player', no, 'choose your first card (1: row, 2: colunm)!')) #提示玩家选牌
x_scan1[1] <- check(c(1:4))
x_scan1[2] <- check(c(1:4)) #scan方法get玩家选牌(最多两张)的坐标 [1]:横坐标 [2]:纵坐标
x_scan1 <- check_rep(x_scan1, df)
face1_1 = cards_n[which(cards_n$xc==x_scan1[1] & cards_n$yc==x_scan1[2]), 'face'] #根据坐标条件查询相应的花色
color1_1 = cards_n[which(cards_n$xc==x_scan1[1] & cards_n$yc==x_scan1[2]), 'color']#根据坐标条件查询相应的颜色
points(x_scan1[1], x_scan1[2], pch=face1_1, col=color1_1, cex=cex) #将玩家翻出的牌绘制出来
cat(paste('Player', no, 'choose your second card (1: row, 2: colunm)!')) #提示玩家选牌
x_scan2[1] <- check(c(1:4))
x_scan2[2] <- check(c(1:4)) #scan方法get玩家选牌(最多两张)的坐标 [1]:横坐标 [2]:纵坐标
x_scan2 <- check_rep(x_scan2, df)
face1_2 = cards_n[which(cards_n$xc==x_scan2[1] & cards_n$yc==x_scan2[2]),'face'] #根据坐标条件查询相应的花色
color1_2 = cards_n[which(cards_n$xc==x_scan2[1] & cards_n$yc==x_scan2[2]),'color']#根据坐标条件查询相应的颜色
points(x_scan2[1], x_scan2[2], pch=face1_2, col=color1_2, cex=cex) #将玩家翻出的牌绘制出来
}
}
}
no = 1
if (pair==1){next}
else{
cat(paste('Wrong, Player', no, 'plays! Press [y], when you are ready to move on!\n'))
yes_no = scan(what = 'character', nmax = 1, quiet = TRUE)}
if (yes_no == 'y'){
points(x_scan1[1], x_scan1[2], pch=face1_1, col='white', cex=cex)
points(x_scan2[1], x_scan2[2], pch=face1_2, col='white', cex=cex)
next
}
}
}
比赛结束后对比得分
if (score1 > score2){
cat(paste('Correct, plays 1 wins! Final leaderboard:\n'))
cat(paste(' Player 1 Player 2 \n'))
cat(paste(' ', score1, ' ', score2, '\n'))
}
if (score1 == score2){
cat(paste('Correct, plays 1 and Player2 are tied wins! Final leaderboard:\n'))
cat(paste(' Player 1 Player 2 \n'))
cat(paste(' ', score1, ' ', score2, '\n'))
}
if(score1 < score2){
cat(paste('Correct, plays 2 wins! Final leaderboard:\n'))
cat(paste(' Player 2 Player 1 \n'))
cat(paste(' ', score2, ' ', score1, '\n'))
}
将这些模块组装起来后试运行
以上我们就实现了这样一个简单的小游戏,虽然游戏的形式简单,但其中涉及的编程内容并不简单,包括要求合理的判断循环方式,画图与逻辑表现相结合,人机交互控制程序运行等。