Bash Shell 脚本:构建更好的疯狂三月分组表
去年,我为 Linux Journal 撰写了一篇文章,标题为 “构建您的疯狂三月分组表”。我的文章很及时,正好赶上“疯狂三月”大学篮球系列赛。您知道,我不关注大学篮球(或者实际上,任何体育运动),但我喜欢参加办公室的彩池。而且似乎每年,我的办公室都喜欢填写疯狂三月分组表,看看谁能最好地预测结果。
由于我不关注大学篮球,因此我不太擅长判断哪些球队可能表现更好。但幸运的是,NCAA 为您对球队进行了排名,因此我编写了一个 Bash 脚本,为我填写了疯狂三月分组表。由于球队排名为 1-16,我使用了从桌面游戏中借用的“D16”方法。我认为这是一种预测结果的优雅方法。
但是,我的脚本中存在一个错误。具体来说,D16 算法的关键假设中存在错误,因此我想在此处使用改进的疯狂三月脚本来纠正该错误。
让我们回顾一下哪里出错了我的 Bash 脚本通过比较每支球队的排名来预测比赛结果。因此,您可以掷一个 D16“骰子”来确定 A 队是否获胜,再掷一个 D16“骰子”来确定 B 队是否失败,反之亦然。如果两次投掷结果一致,您就知道比赛结果:A 队获胜且 B 队失败,或者 A 队失败且 B 队获胜。
我认为排名第一的球队应该是一支强队,所以我假设排名第一的球队有 16 分之 15 的“机会”获胜,以及 16 分之 1 的“机会”失败。在没有任何其他输入的情况下,如果排名第一的球队的 D16 投掷结果为 2 或更大,则该球队将获胜,并且只有当 D16 值为 1 时,排名第一的球队才可能失败。基于该假设,我编写了这个函数
function guesswinner {
rankA=$1
rankB=$2
d16A=$(( ( $RANDOM % 16 ) + 1 ))
d16B=$(( ( $RANDOM % 16 ) + 1 ))
if [ $d16A -gt $rankA -a $d16B -le $rankB ] ; then
# team A wins and team B loses
return $rankA
elif [ $d16A -le $rankA -a $d16B -gt $rankB ] ; then
# team A loses and team B wins
return $rankB
else
# no winner
return 0
fi
}
在 guesswinner 函数中,每个 D16 掷骰都会生成一个 1-16 的随机数。如果 A 队的排名为“rankA”,B 队的排名为“rankB”,并且 A 队的 D16 掷骰结果为“A”,B 队的掷骰结果为“B”,则该函数会像这样测试两个 D16 掷骰结果
如果 A 大于 rankA(A 队获胜)且 B 小于或等于 rankB(B 队失败),则 A 队获胜。
如果 A 小于或等于 rankA(A 队失败)且 B 大于 rankB(B 队获胜),则 B 队获胜。
但是看看如果 A 队排名第 1,B 队排名第 16 会发生什么。A 队将始终获胜
1-16 的掷骰结果有 16 分之 15 的机会大于 1(A 队获胜),而 1-16 的掷骰结果将始终小于或等于 16(B 队失败)。
1-16 的掷骰结果有 16 分之 1 的机会小于或等于 1(A 队失败),但 1-16 的掷骰结果永远不会大于 16(B 队获胜)。
在任何情况下,排名第 16 的 B 队都不可能战胜排名第 1 的 A 队。在任何排名第 1 的球队对阵排名第 16 的球队的比赛中,排名第 1 的球队总是会获胜,这是一个必然的结论。这是不对的。排名第 16 的球队应该有很小的机会战胜排名第 1 的球队。
更好的算法我们需要一个自定义的“骰子”,而不是“静态”的 D16 骰子,该骰子的面数与每支球队的获胜机会相关。让我们考虑以下简单的算法来生成自定义骰子
A 队获得 a=16-rankA+1 个面。
B 队获得 b=16-rankB+1 个面。
在这种假设下,排名第 1 的球队对阵排名第 16 的球队将生成一个骰子,其中 a=16-1+1=16 个“A 队”面,b=16-16+1=1 个“B 队”面,从而产生一个 17 面的骰子。同样,更势均力敌的比赛,例如排名第 8 的球队对阵排名第 9 的球队,将创建一个骰子,其中 a=16-8+1=9 个“A 队”面,b=16-9+1=8 个“B 队”面,从而产生另一个 17 面的骰子。
然而,它并不总是 17 面的骰子。排名第 1 的球队对阵排名第 9 的球队将生成一个骰子,其中 a=16-1+1=16 个“A 队”面,b=16-9+1=8 个“B 队”面,或者一个 24 面的骰子。
在 Bash 中,您可以通过文件模拟虚拟自定义“骰子”。生成一个包含正确数量的“A 队”面和“B 队”面的文件非常简单。如果您已经计算出如上的 a 和 b,您可以编写如下文件
( for teamA in $(seq 0 $a) ; do echo $1 ; done
for teamB in $(seq 0 $b) ; do echo $2 ; done ) > die.file
从此文件中选取随机值就像随机化或“洗牌”文件,然后选择第一行一样简单。在 Linux 系统上,您可以使用 GNU coreutils 中的 shuf(1) 程序来生成文件中行的随机排列。这将随机化您输入到 shuf
中的任何数据。洗牌后,您可以使用 head
轻松选择随机输出的第一行
( for teamA in $(seq 0 $a) ; do echo $1 ; done
for teamB in $(seq 0 $b) ; do echo $2 ; done ) | shuf | head -1
这个简单的表达式成为了改进的疯狂三月脚本的核心。它的运行方式符合我的意愿:排名第 1 的球队几乎总是(但并非总是)会战胜排名第 16 的球队,但更势均力敌的比赛,例如排名第 8 的球队对阵排名第 9 的球队,或排名第 2 的球队对阵排名第 3 的球队,将呈现更均衡的赔率。
构建更好的疯狂三月脚本以上内容可以包装到一个新的 guesswinner
函数中,以预测两支球队之间的比赛,其排名作为参数传递。该函数生成虚拟“骰子”并使用它来猜测获胜者
function guesswinner {
# $1 = team A rank
# $2 = team B rank
a=$(( 16 - $1 + 1 ))
b=$(( 16 - $2 + 1 ))
win=$( ( for teamA in $(seq 1 $a) ; do echo $1 ; done
for teamB in $(seq 1 $b) ; do echo $2 ; done ) | shuf | head -1 )
echo "$1 vs $2 : $win"
return $win
}
由于疯狂三月分组表总是按顺序进行比赛,您可以编写一个 playbracket
函数来运行分组表的不同迭代。第一轮的获胜者进入第二轮和第三轮,以在第四轮中选出分组表的最终获胜者
function playbracket {
# $1 = name of bracket
echo -e "\n___ $1 ___"
echo -e '\nround 1\n'
guesswinner 1 16
round1A=$?
guesswinner 8 9
round1B=$?
guesswinner 5 12
round1C=$?
guesswinner 4 13
round1D=$?
guesswinner 6 11
round1E=$?
guesswinner 3 14
round1F=$?
guesswinner 7 10
round1G=$?
guesswinner 2 15
round1H=$?
echo -e '\nround 2\n'
guesswinner $round1A $round1B
round2A=$?
guesswinner $round1C $round1D
round2B=$?
guesswinner $round1E $round1F
round2C=$?
guesswinner $round1G $round1H
round2D=$?
echo -e '\nround 3\n'
guesswinner $round2A $round2B
round3A=$?
guesswinner $round2C $round2D
round3B=$?
echo -e '\nround 4\n'
guesswinner $round3A $round3B
return $?
}
最后,您只需要为四个区域中的每一个调用 playbracket
函数。您将剩下“四强”以及每个分组表的获胜者,但我将把这些比赛的最终决定留给您自己解决
#!/bin/bash
# improved basketball March Madness prediction
function guesswinner {
...
}
function playbracket {
...
}
playbracket 'Midwest'
playbracket 'East'
playbracket 'West'
playbracket 'South'
每次运行脚本时,您都将生成一个新的 NCAA 疯狂三月篮球分组表。它是完全随机的,因此分组表的每次迭代都会有所不同。这是一个示例运行
$ ./basketball2.sh
___ Midwest ___
round 1
1 vs 16 : 1
8 vs 9 : 9
5 vs 12 : 12
4 vs 13 : 4
6 vs 11 : 11
3 vs 14 : 3
7 vs 10 : 7
2 vs 15 : 2
round 2
1 vs 9 : 1
12 vs 4 : 4
11 vs 3 : 3
7 vs 2 : 7
round 3
1 vs 4 : 1
3 vs 7 : 7
round 4
1 vs 7 : 1
___ East ___
round 1
1 vs 16 : 16
8 vs 9 : 9
5 vs 12 : 5
4 vs 13 : 13
6 vs 11 : 6
3 vs 14 : 3
7 vs 10 : 10
2 vs 15 : 2
round 2
16 vs 9 : 9
5 vs 13 : 5
6 vs 3 : 3
10 vs 2 : 2
round 3
9 vs 5 : 5
3 vs 2 : 2
round 4
5 vs 2 : 2
___ West ___
round 1
1 vs 16 : 1
8 vs 9 : 8
5 vs 12 : 5
4 vs 13 : 4
6 vs 11 : 6
3 vs 14 : 3
7 vs 10 : 10
2 vs 15 : 15
round 2
1 vs 8 : 8
5 vs 4 : 5
6 vs 3 : 6
10 vs 15 : 10
round 3
8 vs 5 : 8
6 vs 10 : 10
round 4
8 vs 10 : 8
___ South ___
round 1
1 vs 16 : 1
8 vs 9 : 8
5 vs 12 : 5
4 vs 13 : 4
6 vs 11 : 6
3 vs 14 : 3
7 vs 10 : 7
2 vs 15 : 2
round 2
1 vs 8 : 1
5 vs 4 : 4
6 vs 3 : 6
7 vs 2 : 7
round 3
1 vs 4 : 4
6 vs 7 : 6
round 4
4 vs 6 : 4
在这个示例运行中,我的脚本选择了中西部的 1 号球队、东部的 2 号球队、西部的 8 号球队和南部的 4 号球队。更重要的是,请注意排名第 16 的球队在东部分组表的第一轮中战胜了排名第 1 的球队。这在我去年发布的脚本中是不可能发生的。我的错误已修复!
使用脚本构建您的 NCAA 疯狂三月篮球分组表的重点不是剥夺游戏的乐趣。相反,由于我对篮球不太熟悉,因此以编程方式构建我的分组表让我可以参加办公室篮球彩池。它很有趣,而且不需要太熟悉体育统计数据。我的脚本给了我关注比赛的理由,但如果我的分组表表现不佳,我也没有情感上的投入——这对我来说已经足够了。