Bash Shell 脚本:构建更好的疯狂三月分组表

作者:Jim Hall

去年,我为 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 疯狂三月篮球分组表的重点不是剥夺游戏的乐趣。相反,由于我对篮球不太熟悉,因此以编程方式构建我的分组表让我可以参加办公室篮球彩池。它很有趣,而且不需要太熟悉体育统计数据。我的脚本给了我关注比赛的理由,但如果我的分组表表现不佳,我也没有情感上的投入——这对我来说已经足够了。

Jim Hall 是一位开源软件倡导者和开发人员,最著名的是 FreeDOS 的创始人。Jim 还非常积极地参与 GNOME 等开源软件项目的可用性测试。在工作中,Jim 是 Hallmentum 的 CEO,这是一家 IT 执行咨询公司,帮助 CIO 和 IT 领导者进行战略规划和组织发展。

加载 Disqus 评论