![]() LivingRPS 界面样例 | |
基本资料 | |
用语名称 | LivingRPS |
---|---|
其他表述 | 赛博石头剪子布、石头剪子布版斗蛐蛐 |
用语出处 | 生态领域学术研究 |
相关条目 | 电子斗蛐蛐 |
LivingRPS是一种以石头剪子布为主题的电子斗蛐蛐游戏,中外皆有流行。
RPS 即 Rock-Paper-Scissors 的英文缩写。
由于石头剪刀布之间循环克制的特性,很容易据此构造出“三个阵营相互拉扯”的场景。
表现形式一般是在空白画面上放置相等数量的石头、剪刀和布(或者纸)三种贴图(或emoji),然后所有的贴图在画面上随机游走。过程中,如果某个剪刀贴图碰到某个布就会将该布的贴图原地替换成剪刀贴图,其余情况同理。
在这种“循环捕食”的机制下,最后有可能出现:
上述这些情况观赏性和寓意性兼具,而随机要素也为其加入了悬念性,因此也会演化出“猜测谁最后一统天下”的玩法,催生出大量“省流”之类的弹幕。
这类动态系统以及其可视化最早可追溯到20世纪末和21世纪初的数学与生态学研究。石头剪刀布是一种典型的循环竞争模型[1],在三次元生态中真实存在,但是有别于食物链这种单向的捕食竞争。
此后,一些有关动力学系统、生物竞争和数学建模的科普向教学视频使用了这种石头剪子布混战的动画进行可视化,用以解说讲解,被认为是此种“石头剪子布”电子斗蛐蛐的首开先河者。
作为一个标准的动力学体系,游戏的规则是,两个不同的emoji碰到一起就会按照石头剪子布规则将对方转化。因此,所有emoji均为石头/剪刀/布是整个体系唯三的稳定状态(或称吸收态)。在吸收态下,这样的转化不会继续发生。因此,网友们看到的“旷日持久的拉锯战”只是过程性的暂态(换句话说,不稳定平衡),终究会走向终结,这也为视频增加了悬链的成分。
使用 Python 的 pygame 就可以实现这样的电子斗蛐蛐程序。下面的程序执行后,即可显示条目封面图片的界面和 HUD。
import pygame
import random
import math
pygame.init()
W, H = 1000, 800
screen = pygame.display.set_mode((W, H))
clock = pygame.time.Clock()
pygame.display.set_caption("Rock–Paper–Scissors dynamics")
# 参数
NUM_AGENTS = 300
RADIUS = 12 # 碰撞半径
SPEED = 3 # 速度
# 类型 & 贴图路径(你可以替换为本地 PNG)
TYPES = {
"rock": {"emoji": "🪨", "color": (200, 50, 50)},
"paper": {"emoji": "📄", "color": (50, 200, 50)},
"scissors": {"emoji": "✂️", "color": (50, 50, 200)}
}
# 剪刀石头布逻辑
def beats(a, b):
return (a == "rock" and b == "scissors") or \
(a == "scissors" and b == "paper") or \
(a == "paper" and b == "rock")
# Agent 类
class Agent:
def __init__(self):
self.x = random.uniform(RADIUS, W - RADIUS)
self.y = random.uniform(RADIUS, H - RADIUS)
angle = random.uniform(0, 2 * math.pi)
self.vx = math.cos(angle) * SPEED
self.vy = math.sin(angle) * SPEED
self.type = random.choice(["rock", "paper", "scissors"])
def move(self):
self.x += self.vx
self.y += self.vy
# 边界反弹
if self.x - RADIUS < 0:
self.x = RADIUS
self.vx *= -1
elif self.x + RADIUS > W:
self.x = W - RADIUS
self.vx *= -1
if self.y - RADIUS < 0:
self.y = RADIUS
self.vy *= -1
elif self.y + RADIUS > H:
self.y = H - RADIUS
self.vy *= -1
def draw(self, screen, font):
# 渲染 emoji(中心对齐)
emoji_surface = font.render(TYPES[self.type]["emoji"], True, (0, 0, 0))
rect = emoji_surface.get_rect(center=(int(self.x), int(self.y)))
screen.blit(emoji_surface, rect)
# 初始化粒子 & 字体
agents = [Agent() for _ in range(NUM_AGENTS)]
emoji_font = pygame.font.SysFont("Segoe UI Emoji", 24)
hud_font = pygame.font.SysFont("Sen", 20)
# 主循环
running = True
while running:
screen.fill((255, 255, 255))
# 移动 & 画出粒子
for agent in agents:
agent.move()
agent.draw(screen, emoji_font)
# 粒子碰撞检测
for i in range(len(agents)):
for j in range(i + 1, len(agents)):
a, b = agents[i], agents[j]
dx = b.x - a.x
dy = b.y - a.y
dist = math.hypot(dx, dy)
if dist < 2 * RADIUS and dist > 0:
# 类型转换
if beats(a.type, b.type):
b.type = a.type
elif beats(b.type, a.type):
a.type = b.type
# 反弹处理
nx, ny = dx / dist, dy / dist
dvx = b.vx - a.vx
dvy = b.vy - a.vy
impact = dvx * nx + dvy * ny
if impact < 0:
a.vx += impact * nx
a.vy += impact * ny
b.vx -= impact * nx
b.vy -= impact * ny
# 推开重叠
overlap = 2 * RADIUS - dist
a.x -= nx * overlap / 2
a.y -= ny * overlap / 2
b.x += nx * overlap / 2
b.y += ny * overlap / 2
# 绘制 HUD(半透明背景 + 文字)
hud_surface = pygame.Surface((160, 90), pygame.SRCALPHA)
hud_surface.fill((240, 240, 240, 200)) # 最后一个值是透明度 (0~255)
# 统计数量
counts = {"rock": 0, "paper": 0, "scissors": 0}
for agent in agents:
counts[agent.type] += 1
# 绘制文字
lines = [
f"Rock: {counts['rock']}",
f"Scissors: {counts['scissors']}",
f"Paper: {counts['paper']}"
]
for idx, line in enumerate(lines):
text = hud_font.render(line, True, (0, 0, 0))
hud_surface.blit(text, (10, 10 + idx * 25))
screen.blit(hud_surface, (10, 10))
# 事件
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.flip()
clock.tick(60)
pygame.quit()
(待补充)