20254105张家毓-实验四实验报告
20254105 2025-2026-2 《Python程序设计》实验4报告
课程:《Python程序设计》
班级: 2541
姓名: 张家毓
学号:20254105
实验教师:王志强
实验日期:2026年6月14日
必修/选修:专选课
1.实验内容
Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。
Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。
例如:编写从社交网络爬取数据,实现可视化舆情监控或者情感分析。
例如:利用公开数据集,开展图像分类、恶意软件检测等
例如:利用Python库,基于OCR技术实现自动化提取图片中数据,并填入excel中。
例如:爬取天气数据,实现自动化微信提醒
例如:利用爬虫,实现自动化下载网站视频、文件等。
例如:编写小游戏:坦克大战、贪吃蛇、扫雷等等
2. 实验过程及结果
(一)实验设计
设计一款名为《千防万防,神人难防》的解密小游戏,游玩逻辑为玩家作为电商客服通过输入包含通关关键词的语句触发剧情,以维护店铺的权益,满足顾客的要求。本小游戏分为三关,每关包含两个触发剧情的关键词,成功触发关键词即为通关。
(二)实现过程与功能
(1)使用random实现NPC从语料库里面随机弹出回复、随机回答抽取的塔罗牌以提高游戏的可玩性。
通过导入标准模块random实现随机生成回复,初级的random主要用于随机生成数字,如:四位验证码。那么通过丰富reply的内容就能实现,随即回复出预定的语句,既能保证不重复留言又能保证留言与游戏内容的相关性


(2)使用字典生成星座表格,为玩家提供通关线索。之后我借助大模型的帮助,让其生成出可以检索的星座性格字典。

(3)具备输入中文功能,通过借助大模型并下载中文输入安装包,达到在python程序种输入汉字的目的,迎合玩家需要。由于程序本身对英文具有识别功能,故而没有进行另外的安装或设置。
(4)关键词匹配,触发剧情。扩大keyword的范围,达到顺利沟通的目的,避免玩家输入同义词但不能触发剧情的情况。

(5)HTML按钮+onclick。onclick是点击事件,鼠标单击元素时触发执行代码。例如:通过点击“第一关”就能进入游戏界面,就像是一个隐藏的界面,一个显示的界面,这个“第一关”就像按钮,通过点击将没有显示出来的界面显示出来。

(6)界面设计拥有丰富色彩。使用RGB的色彩形式,R指的时红色,G指的是绿色,B指的时蓝色,三种颜色的不同占比形成不同的颜色。使用元组的形式更改和设置颜色。不同的颜色就有不同颜色的标准编号,就像在PS中选颜色一样,通过输入标准编号就能得到预期当中的颜色。


(7)拥有图片放大功能。游戏过程中的图片具备放大的效果,能让玩家更加容易地观察到线索,这里也使用到了onclick。通过点击,是图片呈现在之前隐藏的黑色图层之上,有放大的效果。

(三)运行代码与运行视频
点击查看代码
# app.py - 完整代码(第三关增加星座搜索功能)
from flask import Flask, render_template_string, request, jsonify, session
import random
import base64
from io import BytesIO
from PIL import Image, ImageDraw
app = Flask(__name__)
app.secret_key = 'game_secret_key_2025'
LEVEL1_RANDOM_REPLIES = [
"这零食味道太差了!我要求退款,东西你们拿回去吧。",
"难吃死了!我只想退款,不想退货,太麻烦了。",
"这什么破零食,味道怪怪的,赶紧退钱给我。",
"零食太难吃了,我要退款,东西你们自己处理吧。",
"味道很差,我要求仅退款,不退货。",
"这零食吃起来像过期了一样,必须退钱!",
"太难吃了,我咬了一口就扔了,退钱!",
"零食味道很奇怪,我怀疑是假货,退款!",
"不好吃,我要退货退款...不对,仅退款就够了。",
"这味道我接受不了,请给我退款。"
]
LEVEL1_INITIAL_MESSAGE = "这零食味道太差了!我要退货,但是东西我就不寄回去了,直接退钱给我就行。"
LEVEL1_PICTURE_KEYWORDS = ["照片", "图片", "图", "看下", "发图", "看看", "拍照", "相片", "pic", "picture"]
LEVEL1_DOG_KEYWORDS = ["狗粮", "狗食", "宠物粮", "dog", "dogfood"]
LEVEL2_RANDOM_REPLIES = [
"衣服我已经寄回去了,凭什么不给我退款?",
"你们仓库是不是搞错了?衣服明明是干净的!",
"我寄回去的时候衣服是好的,你们是不是弄脏了?",
"退款呢?我等了好几天了!",
"我要投诉你们!衣服没问题你们不给退款!",
"你们是不是想吞我的钱?快退款!",
"衣服我都没怎么穿,凭什么不退款?",
"我要求马上退款,不然我去消费者协会投诉!"
]
LEVEL2_DIRTY_KEYWORDS = ["衣服脏", "穿脏了", "弄脏了", "有污渍", "衣服不干净", "脏了", "衣服上有", "脏衣服"]
LEVEL2_SHOULDER_KEYWORDS = ["右肩"] # 必须包含"右肩"
TAROT_CARDS = [
"愚者", "魔术师", "女祭司", "皇后", "皇帝", "教皇", "恋人", "战车",
"力量", "隐士", "命运之轮", "正义", "倒吊人", "死神", "节制", "恶魔",
"高塔", "星星", "月亮", "太阳", "审判", "世界"
]
LEVEL3_RANDOM_MESSAGES = [
"我最近总是感觉心里空空的,不知道是不是因为单身太久了,想找个伴但又怕受伤。",
"其实我抽这张牌的时候心里特别忐忑,因为上一段感情给我留下了很大的阴影。",
"塔罗牌真的能预测未来吗?我有点半信半疑,但还是希望能得到一个好的答案。",
"我朋友都说我太挑剔了,可是我觉得感情的事情不能将就,你说对不对呢?",
"每次看到别人成双成对我就特别羡慕,也不知道什么时候才能轮到我。",
"我其实条件也不差,但就是遇不到合适的人,是不是我的性格有问题?",
"家里人一直在催我结婚,可我不想随便找个人凑合过一辈子。",
"之前相亲过几次,总觉得对方不够真诚,现在对感情都没什么信心了。",
"我是不是应该主动一点?但女孩子太主动会不会显得很不矜持啊?",
"有时候觉得一个人也挺好的,自由自在,但过节的时候又觉得特别孤单。"
]
ZODIAC_DATA = {
"白羊座": {"start": "3.21", "end": "4.19", "personality": "热情冲动,充满活力,做事果断", "values_love": True},
"金牛座": {"start": "4.20", "end": "5.20", "personality": "稳重务实,忠诚可靠,喜欢享受", "values_love": True},
"双子座": {"start": "5.21", "end": "6.21", "personality": "聪明善变,好奇心强,沟通能力佳", "values_love": False},
"巨蟹座": {"start": "6.22", "end": "7.22", "personality": "温柔体贴,家庭观念强,情感丰富", "values_love": True},
"狮子座": {"start": "7.23", "end": "8.22", "personality": "自信大方,热情慷慨,喜欢被关注", "values_love": True},
"处女座": {"start": "8.23", "end": "9.22", "personality": "理性挑剔,追求完美,注重细节", "values_love": False},
"天秤座": {"start": "9.23", "end": "10.23", "personality": "优雅随和,追求平衡,社交能力强", "values_love": True},
"天蝎座": {"start": "10.24", "end": "11.22", "personality": "神秘深沉,意志坚定,情感强烈", "values_love": True},
"射手座": {"start": "11.23", "end": "12.21", "personality": "乐观自由,热爱冒险,直率坦诚", "values_love": False},
"摩羯座": {"start": "12.22", "end": "1.19", "personality": "踏实稳重,有责任心,事业心强", "values_love": False},
"水瓶座": {"start": "1.20", "end": "2.18", "personality": "独立创新,思想前卫,重视友情", "values_love": False},
"双鱼座": {"start": "2.19", "end": "3.20", "personality": "浪漫多情,富有同情心,直觉敏锐", "values_love": True}
}
ZODIAC_LIST = list(ZODIAC_DATA.keys())
def generate_tarot_card():
card = random.choice(TAROT_CARDS)
position = random.choice(["正位", "逆位"])
return f"{position} {card}"
def generate_dog_food_image():
img = Image.new('RGB', (300, 200), color=(210, 180, 140))
draw = ImageDraw.Draw(img)
draw.rectangle([10, 10, 290, 190], outline=(139, 69, 19), width=3)
draw.rectangle([15, 15, 285, 185], fill=(160, 110, 60))
for i in range(0, 280, 20):
draw.polygon([(20 + i, 10), (27 + i, 0), (34 + i, 10)], fill=(139, 69, 19))
for bx in [60, 140, 220]:
draw.ellipse([bx - 8, 80, bx + 2, 100], fill=(255, 255, 255))
draw.ellipse([bx + 2, 80, bx + 12, 100], fill=(255, 255, 255))
draw.rectangle([bx - 3, 88, bx + 7, 96], fill=(255, 255, 255))
draw.text((15, 175), "DOG FOOD", fill=(80, 50, 30))
for i in range(5):
draw.line([(50 + i * 40, 120), (50 + i * 40, 160)], fill=(120, 80, 40), width=1)
buffer = BytesIO()
img.save(buffer, format='PNG')
return f"data:image/png;base64,{base64.b64encode(buffer.getvalue()).decode()}"
def generate_dirty_shirt_image():
img = Image.new('RGB', (400, 400), color=(240, 248, 255))
draw = ImageDraw.Draw(img)
draw.rectangle([150, 80, 250, 280], fill=(100, 150, 200), outline=(50, 80, 120), width=2)
draw.polygon([(180, 80), (200, 110), (220, 80)], fill=(240, 248, 255))
draw.rectangle([100, 100, 150, 180], fill=(100, 150, 200), outline=(50, 80, 120), width=2)
draw.rectangle([250, 100, 300, 180], fill=(100, 150, 200), outline=(50, 80, 120), width=2)
draw.ellipse([260, 105, 295, 140], fill=(180, 80, 40), outline=(120, 50, 20), width=1)
draw.ellipse([268, 115, 292, 138], fill=(160, 70, 30))
draw.ellipse([255, 115, 278, 132], fill=(190, 90, 50))
draw.ellipse([250, 110, 260, 120], fill=(180, 80, 40))
draw.ellipse([293, 108, 302, 118], fill=(160, 70, 30))
draw.ellipse([286, 100, 294, 108], fill=(170, 75, 35))
draw.rectangle([0, 280, 400, 400], fill=(139, 69, 19))
draw.ellipse([280, 260, 360, 320], fill=(255, 255, 255), outline=(200, 200, 200), width=1)
draw.ellipse([295, 275, 345, 310], fill=(255, 200, 100))
draw.line([320, 260, 340, 300], fill=(139, 69, 19), width=3)
draw.line([325, 260, 345, 300], fill=(139, 69, 19), width=3)
draw.line([280, 95, 310, 75], fill=(255, 0, 0), width=2)
draw.text((285, 70), "← 右肩污渍", fill=(255, 0, 0))
buffer = BytesIO()
img.save(buffer, format='PNG')
return f"data:image/png;base64,{base64.b64encode(buffer.getvalue()).decode()}"
DOG_FOOD_IMG_URL = generate_dog_food_image()
DIRTY_SHIRT_IMG_URL = generate_dirty_shirt_image()
HTML_TEMPLATE = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>千防万防神人难防</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', 'Comic Sans MS', cursive, sans-serif;
background: linear-gradient(135deg, #87CEEB 0%, #98D8E8 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.game-container {
width: 1100px;
max-width: 95vw;
height: 700px;
max-height: 95vh;
background: #FFF8E7;
border-radius: 40px;
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
}
/* 加载界面 */
.loading-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #87CEEB 0%, #6CB4EE 100%);
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: opacity 0.8s ease;
}
.loading-screen.hide {
opacity: 0;
pointer-events: none;
}
.game-title {
text-align: center;
margin-top: 20px;
margin-bottom: 20px;
}
.game-title h1 {
font-size: 3.2rem;
color: #FFD700;
text-shadow: 5px 5px 0 #FF8C00;
letter-spacing: 4px;
}
.characters-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
gap: 30px;
margin: 20px 0;
}
.character-card {
text-align: center;
}
.character-name {
margin-top: 8px;
font-size: 14px;
color: #5D3A1A;
font-weight: bold;
background: rgba(255,255,255,0.6);
display: inline-block;
padding: 2px 12px;
border-radius: 20px;
}
canvas.character-canvas {
image-rendering: crisp-edges;
image-rendering: pixelated;
background: rgba(255,255,255,0.3);
border-radius: 20px;
padding: 10px;
}
.progress-section {
width: 80%;
margin-bottom: 40px;
text-align: center;
}
.progress-bar-bg {
width: 100%;
height: 30px;
background: rgba(255,255,255,0.5);
border-radius: 20px;
overflow: hidden;
border: 2px solid #2E8B57;
}
.progress-fill {
width: 0%;
height: 100%;
background: #32CD32;
border-radius: 20px;
transition: width 0.1s linear;
}
.progress-text {
margin-top: 10px;
font-weight: bold;
color: #333;
}
/* 主内容区(聊天+右侧面板) */
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
/* 关卡选择界面 - 垂直居中 */
.level-select {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 25px;
}
.level-select.active {
display: flex;
}
.level-btn {
width: 260px;
padding: 20px 0;
font-size: 2rem;
font-weight: bold;
border: none;
border-radius: 60px;
cursor: pointer;
transition: transform 0.2s;
font-family: inherit;
box-shadow: 0 8px 0 rgba(0,0,0,0.2);
}
.level-btn:active {
transform: translateY(4px);
box-shadow: 0 4px 0 rgba(0,0,0,0.2);
}
.level-1 { background: #4CAF50; color: white; }
.level-2 { background: #2196F3; color: white; }
.level-3 { background: linear-gradient(135deg, #ff0000, #00ff00, #0000ff); color: white; text-shadow: 1px 1px 0 rgba(0,0,0,0.3); }
/* 聊天区域 */
.chat-section {
flex: 2;
display: flex;
flex-direction: column;
background: #FFF8E7;
border-radius: 20px;
margin: 10px;
}
.chat-header {
background: #4A90D9;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
color: white;
border-radius: 20px 20px 0 0;
}
.exit-btn {
background: #E74C3C;
border: none;
padding: 6px 16px;
border-radius: 30px;
color: white;
font-weight: bold;
cursor: pointer;
font-family: inherit;
}
.messages-area {
flex: 1;
overflow-y: auto;
padding: 15px;
display: flex;
flex-direction: column;
gap: 12px;
background: #F5F0E0;
}
.message {
max-width: 80%;
padding: 10px 14px;
border-radius: 20px;
font-size: 0.9rem;
line-height: 1.4;
word-wrap: break-word;
}
.npc-message {
background: #E8E0D0;
align-self: flex-start;
border-bottom-left-radius: 5px;
color: #5D3A1A;
}
.player-message {
background: #A8E6CF;
align-self: flex-end;
border-bottom-right-radius: 5px;
color: #1A5D3A;
}
.system-message {
background: #DDD;
align-self: center;
font-style: italic;
color: #666;
max-width: 90%;
}
.image-message {
max-width: 280px;
padding: 5px;
background: #DDD;
border-radius: 16px;
cursor: pointer;
transition: transform 0.2s;
}
.image-message:hover {
transform: scale(1.02);
}
.image-message img {
width: 100%;
border-radius: 12px;
display: block;
}
.image-label {
font-size: 0.7rem;
color: #666;
text-align: center;
margin-top: 5px;
}
.input-area {
display: flex;
padding: 12px;
background: #FFF;
gap: 10px;
border-top: 2px solid #DDD;
border-radius: 0 0 20px 20px;
}
.input-area input {
flex: 1;
padding: 12px;
border: 2px solid #DDD;
border-radius: 30px;
font-size: 1rem;
font-family: inherit;
outline: none;
}
.send-btn {
background: #4A90D9;
border: none;
padding: 0 20px;
border-radius: 30px;
color: white;
font-weight: bold;
cursor: pointer;
}
/* 右侧星座查询面板(字典范式表格 + 搜索功能) */
.zodiac-panel {
flex: 1;
background: #FFF0D4;
border-radius: 20px;
margin: 10px 10px 10px 0;
padding: 15px;
overflow-y: auto;
display: none;
flex-direction: column;
border: 3px solid #FFD700;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.zodiac-panel.active {
display: flex;
}
.zodiac-panel h3 {
text-align: center;
color: #FF8C00;
margin-bottom: 10px;
font-size: 1.3rem;
}
/* 搜索框样式 */
.search-box {
margin-bottom: 12px;
padding: 8px 12px;
border: 2px solid #FFD700;
border-radius: 30px;
font-size: 0.85rem;
font-family: inherit;
outline: none;
background: white;
}
.search-box:focus {
border-color: #FF8C00;
}
.zodiac-table {
width: 100%;
border-collapse: collapse;
font-size: 0.75rem;
}
.zodiac-table th, .zodiac-table td {
border: 1px solid #D4A574;
padding: 6px 4px;
text-align: center;
}
.zodiac-table th {
background: #FFD700;
color: #5D3A1A;
}
.love-true {
color: #E74C3C;
font-weight: bold;
}
.love-false {
color: #3498DB;
font-weight: bold;
}
/* 弹窗 */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.6);
z-index: 200;
justify-content: center;
align-items: center;
}
.modal-overlay.active {
display: flex;
}
.popup-window {
background: #FFF8E7;
border-radius: 20px;
width: 350px;
max-width: 80%;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
overflow: hidden;
}
.popup-header {
background: #E74C3C;
padding: 12px 15px;
display: flex;
justify-content: space-between;
align-items: center;
color: white;
font-weight: bold;
}
.popup-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
}
.popup-body {
padding: 20px;
text-align: center;
font-size: 1.2rem;
color: #333;
}
/* 图片放大 */
.image-zoom-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.85);
z-index: 250;
justify-content: center;
align-items: center;
cursor: pointer;
}
.image-zoom-modal.active {
display: flex;
}
.image-zoom-modal img {
max-width: 90%;
max-height: 90%;
border-radius: 20px;
}
/* 庆祝界面 */
.celebration-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle, #FFD700, #FF8C00);
z-index: 300;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
.celebration-modal.active {
display: flex;
}
.celebration-modal h1 {
font-size: 2.8rem;
color: white;
text-shadow: 4px 4px 0 #C0392B;
animation: bounce 0.5s ease infinite alternate;
}
.celebration-modal p {
font-size: 1.3rem;
color: #FFF;
margin: 20px 0;
}
.close-celebration {
background: #2ECC71;
border: none;
padding: 12px 30px;
font-size: 1.2rem;
border-radius: 50px;
color: white;
font-weight: bold;
cursor: pointer;
margin-top: 20px;
}
@keyframes bounce {
from { transform: translateY(0px); }
to { transform: translateY(-20px); }
}
</style>
</head>
<body>
<div class="game-container">
<div class="loading-screen" id="loadingScreen">
<div class="game-title">
<h1>千防万防<br>神人难防</h1>
</div>
<div class="characters-container" id="charactersContainer"></div>
<div class="progress-section">
<div class="progress-bar-bg">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">加载中... 0%</div>
</div>
</div>
<div class="level-select" id="levelSelect">
<button class="level-btn level-1" onclick="selectLevel(1)">第一关</button>
<button class="level-btn level-2" onclick="selectLevel(2)">第二关</button>
<button class="level-btn level-3" onclick="selectLevel(3)">第三关</button>
</div>
<div class="main-content" id="mainContent" style="display: none;">
<div class="chat-section">
<div class="chat-header">
<h3>🐾 客服聊天窗口</h3>
<button class="exit-btn" onclick="exitToLevels()">退出</button>
</div>
<div class="messages-area" id="messagesArea"></div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="输入你的回复..." autocomplete="off">
<button class="send-btn" onclick="sendMessage()">发送</button>
</div>
</div>
<div class="zodiac-panel" id="zodiacPanel">
<h3>📜 星座查询表(字典范式)</h3>
<input type="text" class="search-box" id="zodiacSearch" placeholder="🔍 搜索星座..." onkeyup="filterZodiac()">
<table class="zodiac-table" id="zodiacTable">
<thead>
<tr><th>星座</th><th>生日</th><th>性格</th><th>看重爱情</th></tr>
</thead>
<tbody id="zodiacTableBody">
{% for zodiac, data in zodiac_data.items() %}
<tr class="zodiac-row" data-name="{{ zodiac }}">
<td>{{ zodiac }}</td>
<td>{{ data.start }}-{{ data.end }}</td>
<td>{{ data.personality }}</td>
<td class="love-true" style="color:{% if data.values_love %}#E74C3C{% else %}#3498DB{% endif %};font-weight:bold">
{% if data.values_love %}✅ 是{% else %}❌ 否{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- 弹窗 -->
<div class="modal-overlay" id="popupModal">
<div class="popup-window">
<div class="popup-header">
<span>⚠️ 系统通知</span>
<button class="popup-close" onclick="closePopup()">✕</button>
</div>
<div class="popup-body" id="popupBody">仓库:衣服穿脏了</div>
</div>
</div>
<!-- 图片放大 -->
<div class="image-zoom-modal" id="imageZoomModal" onclick="closeImageZoom()">
<img id="zoomImage" src="">
</div>
<!-- 庆祝界面 -->
<div class="celebration-modal" id="celebrationModal">
<h1>🎉 通关成功! 🎉</h1>
<p>✨ 你识破了骗局! ✨</p>
<p>🏆 成功保护了店铺的利益! 🏆</p>
<button class="close-celebration" onclick="closeCelebration()">退出</button>
</div>
<script>
let currentLevel = null;
let gameActive = true;
// 第一关状态
let level1Stage = 0;
// 第二关状态
let level2Stage = 0;
let waitingForPopupClose = false;
// 第三关状态
let level3Stage = 0; // 0=等待发牌完成, 1=等待询问星座, 2=等待解牌(含爱情), 3=等待"我用python"
let currentZodiac = null;
let currentTarotCard = null;
let zodiacValuesLove = null;
// 图片URL
const DOG_FOOD_IMG = "{{ DOG_FOOD_IMG }}";
const DIRTY_SHIRT_IMG = "{{ DIRTY_SHIRT_IMG }}";
// 关键词库
const PICTURE_KEYWORDS = ["照片", "图片", "图", "看下", "发图", "看看", "拍照", "相片", "pic", "picture"];
const DOG_KEYWORDS = ["狗粮", "狗食", "宠物粮", "dog", "dogfood"];
const DIRTY_KEYWORDS = ["衣服脏", "穿脏了", "弄脏了", "有污渍", "衣服不干净", "脏了", "衣服上有", "脏衣服"];
const ASK_ZODIAC_KEYWORDS = ["星座", "什么星座", "你是啥星座", "你星座", "请问星座", "属什么星座", "你是什么星座"];
const LOVE_KEYWORDS = ["爱情", "感情", "恋爱", "姻缘", "情感", "爱情运势"];
const PYTHON_KEYWORDS = ["我用python", "我用Python", "我用PYTHON", "python", "Python", "用python"];
// 回复库
const LEVEL1_RANDOM_REPLIES = {{ LEVEL1_REPLIES | tojson }};
const LEVEL2_RANDOM_REPLIES = {{ LEVEL2_REPLIES | tojson }};
const LEVEL3_RANDOM_MESSAGES = {{ LEVEL3_MESSAGES | tojson }};
const ZODIAC_LIST = {{ zodiac_list | tojson }};
const ZODIAC_DATA = {{ zodiac_data | tojson }};
// ==================== 星座搜索功能 ====================
function filterZodiac() {
const searchInput = document.getElementById('zodiacSearch');
const filter = searchInput.value.toLowerCase();
const rows = document.querySelectorAll('#zodiacTableBody tr');
rows.forEach(row => {
const zodiacName = row.getAttribute('data-name') || row.cells[0].innerText;
if (zodiacName.toLowerCase().includes(filter)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
}
function drawCharacter(ctx, offsetX, offsetY, skinColor, hairColor, shirtColor, pantsColor, eyeColor, gender) {
const size = 4;
function pixel(x,y,color){
ctx.fillStyle = color;
ctx.fillRect(offsetX + x*size, offsetY + y*size, size, size);
}
const hairCells = [[7,0],[8,0],[9,0],[10,0],[11,0],[6,1],[7,1],[8,1],[9,1],[10,1],[11,1],[12,1],[5,2],[6,2],[12,2],[13,2]];
hairCells.forEach(p => pixel(p[0], p[1], hairColor));
const faceCells = [[7,3],[8,3],[9,3],[10,3],[11,3],[6,4],[7,4],[8,4],[9,4],[10,4],[11,4],[12,4],[6,5],[7,5],[8,5],[9,5],[10,5],[11,5],[12,5],[7,6],[8,6],[9,6],[10,6],[11,6],[8,7],[9,7],[10,7]];
faceCells.forEach(p => pixel(p[0], p[1], skinColor));
pixel(7, 4, eyeColor);
pixel(11, 4, eyeColor);
pixel(8, 4, '#FFF');
pixel(12, 4, '#FFF');
pixel(5, 5, '#FFA07A');
pixel(13, 5, '#FFA07A');
pixel(9, 6, '#FF5050');
pixel(10, 6, '#FF5050');
pixel(8, 7, '#FF5050');
pixel(11, 7, '#FF5050');
const bodyCells = [[7,8],[8,8],[9,8],[10,8],[11,8],[6,9],[7,9],[8,9],[9,9],[10,9],[11,9],[12,9],[6,10],[7,10],[8,10],[9,10],[10,10],[11,10],[12,10],[7,11],[8,11],[9,11],[10,11],[11,11]];
bodyCells.forEach(p => pixel(p[0], p[1], shirtColor));
const pantsCells = [[7,12],[8,12],[9,12],[10,12],[11,12],[7,13],[8,13],[9,13],[10,13],[11,13],[8,14],[9,14],[10,14]];
pantsCells.forEach(p => pixel(p[0], p[1], pantsColor));
[[4,8],[4,9],[5,9],[14,8],[14,9],[13,9]].forEach(p => pixel(p[0], p[1], shirtColor));
pixel(7, 15, '#555');
pixel(8, 15, '#555');
pixel(10, 15, '#555');
pixel(11, 15, '#555');
if(gender === 'girl') {
pixel(9, 2, '#FF69B4');
pixel(10, 2, '#FF69B4');
pixel(8, 3, '#FF69B4');
pixel(11, 3, '#FF69B4');
} else {
pixel(8, 2, '#FFD700');
pixel(9, 2, '#FFD700');
pixel(10, 2, '#FFD700');
pixel(11, 2, '#FFD700');
}
}
function createThreeCharacters() {
const container = document.getElementById('charactersContainer');
if(!container) return;
container.innerHTML = '';
const characters = [
{ name: '小安', skin: '#FFDAB9', hair: '#8B4513', shirt: '#FF6347', pants: '#1E90FF', eye: '#000', gender: 'boy' },
{ name: '小全', skin: '#F5C6A0', hair: '#FFD700', shirt: '#32CD32', pants: '#9370DB', eye: '#000', gender: 'girl' },
{ name: '小盾', skin: '#FFE4C4', hair: '#2F4F4F', shirt: '#FF69B4', pants: '#00CED1', eye: '#000', gender: 'boy' }
];
characters.forEach((char) => {
const card = document.createElement('div');
card.className = 'character-card';
const canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 130;
canvas.className = 'character-canvas';
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 100, 130);
drawCharacter(ctx, 0, 0, char.skin, char.hair, char.shirt, char.pants, char.eye, char.gender);
const nameSpan = document.createElement('div');
nameSpan.className = 'character-name';
nameSpan.innerText = char.name;
card.appendChild(canvas);
card.appendChild(nameSpan);
container.appendChild(card);
});
}
let bounceFrame = 0;
function animateCharacters() {
const canvases = document.querySelectorAll('.character-canvas');
const offset = Math.abs(Math.sin(bounceFrame * 0.1)) * 6;
canvases.forEach(canvas => {
canvas.style.transform = `translateY(${offset}px)`;
});
bounceFrame++;
requestAnimationFrame(animateCharacters);
}
let progress = 0;
function startLoading() {
createThreeCharacters();
animateCharacters();
const interval = setInterval(() => {
progress += Math.random() * 15;
if(progress >= 100) {
progress = 100;
clearInterval(interval);
document.getElementById('progressFill').style.width = '100%';
document.getElementById('progressText').innerText = '加载中... 100%';
setTimeout(() => {
document.getElementById('loadingScreen').classList.add('hide');
document.getElementById('levelSelect').classList.add('active');
}, 500);
} else {
document.getElementById('progressFill').style.width = progress + '%';
document.getElementById('progressText').innerText = `加载中... ${Math.floor(progress)}%`;
}
}, 80);
}
function selectLevel(level) {
// 重置所有状态
currentLevel = level;
gameActive = true;
level1Stage = 0;
level2Stage = 0;
level3Stage = 0;
waitingForPopupClose = false;
currentZodiac = null;
currentTarotCard = null;
zodiacValuesLove = null;
// 清空聊天区域
const area = document.getElementById('messagesArea');
if(area) area.innerHTML = '';
// 清空输入框
const input = document.getElementById('messageInput');
if(input) input.value = '';
// 清空搜索框
const searchBox = document.getElementById('zodiacSearch');
if(searchBox) searchBox.value = '';
// 显示主界面
document.getElementById('levelSelect').classList.remove('active');
document.getElementById('mainContent').style.display = 'flex';
// 第三关显示星座面板,其他关隐藏
const zodiacPanel = document.getElementById('zodiacPanel');
if(level === 3) {
zodiacPanel.classList.add('active');
// 重置星座表格显示
if(typeof filterZodiac === 'function') filterZodiac();
} else {
zodiacPanel.classList.remove('active');
}
// 初始化关卡
if(level === 1) initLevel1();
else if(level === 2) initLevel2();
else if(level === 3) initLevel3();
}
function initLevel1() {
addMessageToUI("顾客: " + "{{ LEVEL1_INITIAL_MSG }}", 'npc');
}
function initLevel2() {
waitingForPopupClose = true;
addMessageToUI("顾客: 我已经把衣服退回去了,为什么不退款?", 'npc');
showPopup("仓库:衣服穿脏了");
}
function initLevel3() {
// 随机生成塔罗牌
const tarotCards = ["愚者", "魔术师", "女祭司", "皇后", "皇帝", "教皇", "恋人", "战车", "力量", "隐士", "命运之轮", "正义", "倒吊人", "死神", "节制", "恶魔", "高塔", "星星", "月亮", "太阳", "审判", "世界"];
const card = tarotCards[Math.floor(Math.random() * tarotCards.length)];
const position = Math.random() > 0.5 ? "正位" : "逆位";
currentTarotCard = `${position} ${card}`;
addMessageToUI(`顾客: 我抽了一张塔罗牌:${currentTarotCard}`, 'npc');
setTimeout(() => {
// 随机选择一句50字左右的语句
const randomMsg = LEVEL3_RANDOM_MESSAGES[Math.floor(Math.random() * LEVEL3_RANDOM_MESSAGES.length)];
addMessageToUI(`顾客: ${randomMsg} 你能帮我解解牌吗?`, 'npc');
level3Stage = 1; // 进入等待询问星座阶段
}, 1500);
}
function addMessageToUI(text, type, imageUrl = null) {
const area = document.getElementById('messagesArea');
if(!area) return;
const msgDiv = document.createElement('div');
if(imageUrl) {
msgDiv.className = 'message image-message';
msgDiv.onclick = () => openImageZoom(imageUrl);
msgDiv.innerHTML = `<img src="${imageUrl}" alt="图片"><div class="image-label">🔍 点击放大</div>`;
} else {
if(type === 'player') msgDiv.className = 'message player-message';
else if(type === 'system') msgDiv.className = 'message system-message';
else msgDiv.className = 'message npc-message';
msgDiv.innerText = text;
}
area.appendChild(msgDiv);
area.scrollTop = area.scrollHeight;
}
function showPopup(text) {
document.getElementById('popupBody').innerText = text;
document.getElementById('popupModal').classList.add('active');
}
function closePopup() {
document.getElementById('popupModal').classList.remove('active');
if(currentLevel === 2 && waitingForPopupClose) {
waitingForPopupClose = false;
level2Stage = 1;
addMessageToUI("系统: 弹窗已关闭,请回复顾客(例如:衣服穿脏了)", 'system');
}
}
function openImageZoom(url) {
document.getElementById('zoomImage').src = url;
document.getElementById('imageZoomModal').classList.add('active');
}
function closeImageZoom() {
document.getElementById('imageZoomModal').classList.remove('active');
}
function showCelebration() {
document.getElementById('celebrationModal').classList.add('active');
}
function closeCelebration() {
document.getElementById('celebrationModal').classList.remove('active');
exitToLevels();
}
function exitToLevels() {
document.getElementById('mainContent').style.display = 'none';
document.getElementById('levelSelect').classList.add('active');
// 清空输入框
const input = document.getElementById('messageInput');
if(input) input.value = '';
// 清空聊天区域
const area = document.getElementById('messagesArea');
if(area) area.innerHTML = '';
// 清空搜索框
const searchBox = document.getElementById('zodiacSearch');
if(searchBox) searchBox.value = '';
}
function sendMessage() {
if(!gameActive) return;
// 第二关弹窗未关闭时不能发送
if(currentLevel === 2 && waitingForPopupClose) {
addMessageToUI("系统: 请先关闭弹窗再继续对话", 'system');
return;
}
const input = document.getElementById('messageInput');
const msg = input.value.trim();
if(msg === "") return;
addMessageToUI(msg, 'player');
input.value = '';
if(currentLevel === 1) handleLevel1(msg);
else if(currentLevel === 2) handleLevel2(msg);
else if(currentLevel === 3) handleLevel3(msg);
}
function handleLevel1(msg) {
const lowerMsg = msg.toLowerCase();
if(level1Stage === 0) {
let hasPicture = PICTURE_KEYWORDS.some(kw => lowerMsg.includes(kw.toLowerCase()));
if(hasPicture) {
addMessageToUI("顾客: 好的,你看这张照片!", 'npc');
addMessageToUI("", 'npc', DOG_FOOD_IMG);
level1Stage = 1;
} else {
const randomReply = LEVEL1_RANDOM_REPLIES[Math.floor(Math.random() * LEVEL1_RANDOM_REPLIES.length)];
addMessageToUI("顾客: " + randomReply, 'npc');
}
}
else if(level1Stage === 1) {
let hasDog = DOG_KEYWORDS.some(kw => lowerMsg.includes(kw.toLowerCase()));
if(hasDog) {
addMessageToUI("顾客: 啊...竟然被发现了...好吧我错了。", 'npc');
gameActive = false;
showCelebration();
} else {
const randomReply = LEVEL1_RANDOM_REPLIES[Math.floor(Math.random() * LEVEL1_RANDOM_REPLIES.length)];
addMessageToUI("顾客: " + randomReply, 'npc');
}
}
}
function handleLevel2(msg) {
const lowerMsg = msg.toLowerCase();
if(level2Stage === 1) {
let hasDirty = DIRTY_KEYWORDS.some(kw => lowerMsg.includes(kw));
if(hasDirty) {
addMessageToUI("顾客: 根本没有脏!你看,我穿着它吃饭的照片,哪里脏了?", 'npc');
addMessageToUI("", 'npc', DIRTY_SHIRT_IMG);
level2Stage = 2;
} else {
const randomReply = LEVEL2_RANDOM_REPLIES[Math.floor(Math.random() * LEVEL2_RANDOM_REPLIES.length)];
addMessageToUI("顾客: " + randomReply, 'npc');
}
}
else if(level2Stage === 2) {
let hasRightShoulder = lowerMsg.includes("右肩");
if(hasRightShoulder) {
addMessageToUI("顾客: 啊...被你发现了...好吧我认了。", 'npc');
gameActive = false;
showCelebration();
} else {
addMessageToUI("顾客: 你仔细看看照片,哪里脏了?我看你是想赖账!", 'npc');
}
}
}
function handleLevel3(msg) {
const lowerMsg = msg.toLowerCase();
if(level3Stage === 1) {
// 等待询问星座
let isAskingZodiac = ASK_ZODIAC_KEYWORDS.some(kw => lowerMsg.includes(kw));
if(isAskingZodiac) {
// 随机生成顾客的星座
const randomIndex = Math.floor(Math.random() * ZODIAC_LIST.length);
currentZodiac = ZODIAC_LIST[randomIndex];
zodiacValuesLove = ZODIAC_DATA[currentZodiac].values_love;
addMessageToUI(`顾客: 我是${currentZodiac},这和我解牌有关系吗?`, 'npc');
level3Stage = 2;
} else {
addMessageToUI("顾客: 你能先告诉我,我是什么星座吗?我想结合星座让你帮我解牌~", 'npc');
}
}
else if(level3Stage === 2) {
// 等待包含"爱情"关键词的解牌
let hasLove = LOVE_KEYWORDS.some(kw => lowerMsg.includes(kw));
if(hasLove) {
let reply = "";
if(zodiacValuesLove) {
reply = `根据你的${currentZodiac}性格,结合${currentTarotCard},你的爱情会很顺利!放心吧~`;
} else {
reply = `根据你的${currentZodiac}性格,结合${currentTarotCard},你的爱情可能不太顺利,但生活其他方面会很顺利哦~`;
}
addMessageToUI("顾客: " + reply, 'npc');
setTimeout(() => {
addMessageToUI("顾客: 那生活苦短,我该怎么办呢?", 'npc');
level3Stage = 3;
}, 2000);
} else {
addMessageToUI("顾客: 请帮我解牌,告诉我关于爱情方面的运势~", 'npc');
}
}
else if(level3Stage === 3) {
// 等待"我用python"关键词
let hasPython = PYTHON_KEYWORDS.some(kw => lowerMsg.includes(kw));
if(hasPython) {
addMessageToUI("顾客: 哈哈,说得对!用Python充实生活,我明白了!谢谢你的建议~", 'npc');
gameActive = false;
showCelebration();
} else {
addMessageToUI("顾客: 生活苦短,我该用什么来充实自己呢?", 'npc');
}
}
}
// 启动加载
startLoading();
// 监听回车
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if(e.key === 'Enter') sendMessage();
});
</script>
</body>
</html>
'''
@app.route('/')
def index():
return render_template_string(
HTML_TEMPLATE,
DOG_FOOD_IMG=DOG_FOOD_IMG_URL,
DIRTY_SHIRT_IMG=DIRTY_SHIRT_IMG_URL,
LEVEL1_INITIAL_MSG=LEVEL1_INITIAL_MESSAGE,
LEVEL1_REPLIES=LEVEL1_RANDOM_REPLIES,
LEVEL2_REPLIES=LEVEL2_RANDOM_REPLIES,
LEVEL3_MESSAGES=LEVEL3_RANDOM_MESSAGES,
zodiac_data=ZODIAC_DATA,
zodiac_list=ZODIAC_LIST
)
if __name__ == '__main__':
print("=" * 50)
print("游戏已启动!请在浏览器中打开: http://127.0.0.1:5000")
print("=" * 50)
app.run(debug=True, host='0.0.0.0', port=5000)
3. 实验过程中遇到的问题和解决过程
- 问题1:实验过程中出现不能输入汉字的问题
- 问题1解决方案:首先在pycharm中下载安装包,之后利用大模型生成跳转网页的地址实现汉字的顺利输入和后续的识别。
- 问题2:关卡按键错位
- 问题2解决方案:通过调整网上学习发现这个可以通过更改页面布局的数值实现想要的效果,通过将数值整体减小,实现按键位置整体下移。
- 问题3:程序运行不流畅,不能正常运行
- 问题3解决方案:因为能力不足,不能实现自己很多设计的想法,也不能把老师教的设计的程序有效的串联起来,这个字典部分就是一直不能融入网页,之后将我写的代码发给大模型,让大模型帮我串联一下我写的程序。
全课总结、感想体会与意见建议
(一)全课总结
第一章:初识Python
我第一次接触Python。当我安装开发环境、写下第一行print("Hello World")时的成就感,是入门编程的第一步。这一章让我明白了Python"人生苦短,我用Python"的设计哲学——把复杂的事情变简单,这也是我这款小游戏的最后通关密码。python是跨平台,高级的语言,为人所用为人所懂。
第二章:Python语法
学习了变量、数据类型、运算符、输入输出等基础语法。Python是动态类型语言,在课堂上我跟着老师的讲解步骤对程序进行编写,但是我总是跟不上老师的节奏,我打字太慢了。我明白python的基本的逻辑,从最简单的print、if之后又学到了循环。
第三章:流程控制语句
掌握了if-elif-else条件判断和for、while循环语句。流程控制让程序有了"判断能力"和"重复执行能力"。比如用if判断用户输入的关键词是否包含"狗粮",用while控制游戏循环直到通关。通过看那个流程图,通过判断"T"或"F"来决定接下来的流程走向。
第四章:序列
学习了列表、元组、字典、集合这四种序列类型。列表像"购物清单"可以随意增删改查,字典像"电话本"通过键快速找到值。在游戏开发中,用列表存储随机回复语料库,用字典存储星座数据(键是星座名,值是生日、性格等信息)。
第五章:字符串
字符串是编程中最常用的数据类型。学习了拼接、切片、格式化、查找替换等操作。在聊天游戏中,用户输入的消息就是字符串,通过in关键词判断是否包含特定词语(如"照片"、"狗粮")。
第六章:函数
函数是代码复用的核心。把重复使用的代码封装成函数,需要时调用即可,避免重复书写。在游戏中,addMessageToUI()、selectLevel()、handleLevel1()等都是函数。同时我还学到了不同的符号代表的不同的运算法则,有利于我在公式设计上的编写。
第七章:面向对象
面向对象是Python的核心编程范式。类(Class)和对象(Object)的概念让代码更贴近现实世界。比如游戏中,可以把"顾客"定义为一个类,有属性(星座、对话内容)和方法(发送消息、发送图片)。虽然本次游戏主要用函数式编程,但面向对象的思想已经渗透其中。
第八章:模块
模块让Python拥有"拿来主义"的便利。通过import导入别人写好的功能,比如import random使用随机数,from PIL import Image生成图片。我现在只能比较熟练的运用随机模块,主要用于随机生成提前准备好的语料库里的内容。
第九章:异常处理及程序调试
程序不可能一次写对,异常处理和调试是写出健壮程序的保障。用try-except捕获可能出错的代码(如文件不存在、用户输入非法),避免程序直接崩溃。在游戏开发中,可以添加异常处理防止因网络问题导致图片加载失败。
(二)感想体会
python让我有机会接触到所谓“文科生学不明白”的学科内容,虽然说我们的课程内容没有展现出python的最终形态,但是能接触到我已经很开心了。通过学习,我现在拥有了编写程序的能力,尽管进步空间极大,但是这确确实实对我来说是一次突破。不同于上学期的人工智能导论,python的课程更考验同学的实验与动手能力,以我自己为例,我与同学进行和做实验的时候进行了不下几十次的修正和改错,过程非常的痛苦,但是试验成功的那一刻我是真的很开心,切实的感受到了自己能力的提升,而不是单纯的依靠一次又一次喂给大模型。在课程过程当中,我学的比较认真,老师打字太快了有的时候确实跟不上。在最后这次自主设计的实验中,我记得老师说实验是你自己喜欢的我才会喜欢,所以说我做了一个超出我能力的一个小游戏,虽然借助了大模型的算力,但是能把自己的想法落地确实是一件令人有成就感的事。
(三)意见建议
1.希望可以能让所有的技巧贯穿在一个实验里,就是每次都完善这个实验一点点,整个学期之后,每个人都能有一个质量不低的程序。其实我是想再优化一下我这个游戏的,但是一时间的想法还是比较稚嫩,所以说有这样的一个建议。
2.主要是学生再英文打字的方面比较生疏,以我为例,我中文打字都有困难,经过一个学期终于是能快速的打字了,但是让我打英文就又很慢。老师你打字太快了,你慢点,我都要变成四驱的学生了,我真的想跟上。
3.老师打卡英语的任务特别好,没有老师的这个“任务指标”我都不知道自己能坚持这么长时间,果然坚持就是胜利。
4.老师特别好,回复问题总是很及时又很耐心,尽管我问的问题都是相当简单的事情,老师也没有不耐烦,老师真的超级好。
参考资料
- [《零基础学python》]
- B站

浙公网安备 33010602011771号