引子
这个项目其实是为了班级元旦晚会的抽奖环节而设计的。由于班上几位原批+点子王一拍脑袋,决定整场晚会从幻灯片到背景音乐到幕间游戏全用原神的风格,并且发现现有的抽奖软件都 UI 太过古早且不支持自定义权重,于是就决定自己完成一个程序。
另外,我加入了抽到相应学生会显示对应图片的功能,以提高节目效果(e gao)。
前端设计
项目的前端基于 Pygame 框架,以此来达成绘制出类似于原神祈愿界面的目的。
素材查找
项目所使用的图片素材来源于开源项目Mantan21/Genshin-Impact-Wish-Simulator。通过其我们可以很轻松地得到无背景的按钮、边框及祈愿图片、背景、图标等素材。
面向对象的编程
获得素材之后,紧接着要做的就是完成绘制图片所需要的方法。
经过整理,发现要想完成界面的渲染,我们需要以下对象类。
| object | description |
|---|---|
| Text | 渲染文字 |
| Image | 渲染图片 |
| ImageFixed | 渲染固定尺寸的图片 |
| ColorSurface | 渲染图形 |
| ButtonText | 渲染文字按钮 |
| ButtonImage | 渲染图片按钮 |
| ButtonColorSurface | 渲染图形按钮 |
其中,每个类都需要一个draw方法来完成对象的绘制,而按钮类需要handle_event方法来会调事件。
这些代码均在ui.py中。
图形类的编写
下面以Text对象为例子,讲讲图形类是如何编写的。
| |
需要注意的是,为了方便主程序的编写,在draw方法中我进行了坐标的转译,使得在调用时不需要左上角坐标而只需要中心坐标就行。
另外,为了方便图形的渲染,可以使用如下方法进行放缩。
| |
按钮类的编写
按钮类与上述大体相同,唯一的区别就是需要一个handle_event方法。
| |
如是,我们即可在主循环中重复检测是否将鼠标移动到按钮上并点击,以此来达到承载事件的目的。
主界面的配置
程序的主界面主要有三部份构成:
- 背景
- 结果卡片(result_card)
- 用户图片(image)
- 文字背景(text_background)
- 中奖人姓名(text)
- 按钮
- 开始抽奖按钮(roll_button)
- 配置按钮(config_button)
在本章节中,我们只讲背景和结果卡片的渲染。完整主界面的渲染与按钮我们在后文中会提及。
背景
很简单,没什么可讲的。
| |
结果卡片
这是三者中最复杂的部分。其中主要分为三部份,要依次渲染以面上面的内容被下面的内容覆盖,
在未开始抽奖的情况下,先渲染初始的结果卡片。
| |
在开始抽奖的情况下,渲染响应抽中人的图片和名称。
| |
后端设计
本项目中使用的随机抽取算法为A-Res算法,其通过关键值变换将权重转化为随机优先级,并用最小堆动态维护优先级最高的几个元素,实现了高效的加权随机抽样。
对于$n$个元素中抽取$m$个样本,其时间复杂度为$O(n\log{m})$。
核心思想:关键值变换
对每个元素(item, weight),计算一个关键值$k_i = u_i^{1/w_i}$:
$u_i$:从 [0,1] 均匀分布的随机数。
$w_i$:当前元素的权重。
其具有如下数学意义:该变换确保元素被选中的概率与其权重$w_i$成正比。权重越大,$k_i$的期望值越高(因为指数$1/w_i$越小,$u_i^{(1/w_i)}$越接近 1)。
算法流程
- 初始化:创建一个空的最小堆
heap(堆顶始终是关键值最小的元素)。 - 遍历所有元素:
- 未填满堆时(
len(heap) < m):直接将当前元素的关键值 ki 和元素本身压入堆。 - 堆已满(
len(heap) = m):若当前ki > 堆顶的关键值,则替换堆顶元素:- 将
(ki, 元素)压入堆。 - 弹出堆顶元素(保持堆大小为
m)。
- 将
- 未填满堆时(
- 反回结果:遍历结束后,堆中所有元素即为被选中的样本。
伪代码实现
| |
Python实现
| |
主程序
主循环
| |
两个按钮
| |
杂项:播放音视频
| |
