jiang buqing commited on
Commit
e6cc357
·
1 Parent(s): dc256b3
Files changed (9) hide show
  1. Dockerfile +12 -0
  2. README.md +31 -0
  3. app.py +98 -0
  4. requirements.txt +4 -0
  5. space.yaml +9 -0
  6. static/css/style.css +305 -0
  7. static/js/app.js +360 -0
  8. templates/index.html +49 -0
  9. words.txt +510 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+ COPY . /app
5
+
6
+ RUN pip install --upgrade pip
7
+ RUN pip install -r requirements.txt
8
+
9
+ EXPOSE 7860
10
+ ENV PORT=7860
11
+
12
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -9,3 +9,34 @@ short_description: Semantic Hunter Game
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
12
+
13
+ # Semantic Hunter (词猎人)
14
+
15
+ A semantic word guessing game using Hugging Face Transformers. The game chooses a random word, and players try to guess it based on semantic similarity.
16
+
17
+ ## How to Play
18
+
19
+ 1. The game selects a random word from the word list
20
+ 2. You try to guess the word
21
+ 3. After each guess, you get a semantic similarity percentage
22
+ 4. Try to find the secret word with the fewest guesses!
23
+
24
+ ## Deploy to Hugging Face Spaces
25
+
26
+ This application is configured to run on Hugging Face Spaces with Docker:
27
+
28
+ 1. Create a new Space on [Hugging Face Spaces](https://huggingface.co/spaces)
29
+ 2. Select Docker as the SDK
30
+ 3. Link your GitHub repository or upload files directly
31
+ 4. The Space will automatically build and deploy your app
32
+
33
+ ## Development
34
+
35
+ To run the application locally:
36
+
37
+ ```bash
38
+ pip install -r requirements.txt
39
+ python app.py
40
+ ```
41
+
42
+ The application will be available at http://localhost:7860
app.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ import random
3
+ from sentence_transformers import SentenceTransformer, util
4
+ import torch
5
+ import os
6
+
7
+ app = Flask(__name__)
8
+
9
+ # Load model
10
+ model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
11
+
12
+ # Load word pool
13
+ with open("words.txt", "r", encoding="utf-8") as f:
14
+ word_pool = [line.strip() for line in f if line.strip()]
15
+
16
+ # Game state
17
+ game_state = {
18
+ "secret_word": "",
19
+ "secret_vec": None,
20
+ "guesses": [],
21
+ "latest_guess": None
22
+ }
23
+
24
+ @app.route('/')
25
+ def home():
26
+ return render_template('index.html')
27
+
28
+ @app.route('/new-game', methods=['POST'])
29
+ def new_game():
30
+ global game_state
31
+ # Reset game state
32
+ game_state["secret_word"] = random.choice(word_pool)
33
+ game_state["secret_vec"] = model.encode(game_state["secret_word"], convert_to_tensor=True)
34
+ game_state["guesses"] = []
35
+ game_state["latest_guess"] = None
36
+
37
+ return jsonify({"status": "success", "message": "New game started"})
38
+
39
+ @app.route('/guess', methods=['POST'])
40
+ def guess():
41
+ data = request.json
42
+ guess_word = data.get('guess', '').strip()
43
+
44
+ if not guess_word:
45
+ return jsonify({"status": "error", "message": "No guess provided"})
46
+
47
+ # Encode the guess
48
+ guess_vec = model.encode(guess_word, convert_to_tensor=True)
49
+
50
+ # Calculate similarity
51
+ similarity = util.pytorch_cos_sim(game_state["secret_vec"], guess_vec)
52
+ percent = round(float(similarity[0][0]) * 100, 2)
53
+
54
+ # Check if correct
55
+ is_correct = (game_state["secret_word"] == guess_word)
56
+
57
+ # Update latest guess
58
+ game_state["latest_guess"] = guess_word
59
+
60
+ # Store guess
61
+ game_state["guesses"].append({
62
+ "word": guess_word,
63
+ "similarity": percent,
64
+ "is_correct": is_correct,
65
+ "is_latest": True
66
+ })
67
+
68
+ # Update previous guesses to not be latest
69
+ for i in range(len(game_state["guesses"]) - 1):
70
+ if "is_latest" in game_state["guesses"][i]:
71
+ game_state["guesses"][i]["is_latest"] = False
72
+
73
+ # Sort guesses by similarity (descending)
74
+ sorted_guesses = sorted(game_state["guesses"], key=lambda x: x["similarity"], reverse=True)
75
+
76
+ response = {
77
+ "status": "success",
78
+ "similarity": percent,
79
+ "is_correct": is_correct,
80
+ "guesses": game_state["guesses"],
81
+ "latest_guess": guess_word
82
+ }
83
+
84
+ if is_correct:
85
+ response["message"] = "猜对啦!"
86
+
87
+ return jsonify(response)
88
+
89
+ @app.route('/give-up', methods=['POST'])
90
+ def give_up():
91
+ return jsonify({
92
+ "status": "success",
93
+ "secret_word": game_state["secret_word"]
94
+ })
95
+
96
+ if __name__ == '__main__':
97
+ port = int(os.environ.get("PORT", 7860))
98
+ app.run(host='0.0.0.0', port=port)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ flask
2
+ sentence-transformers
3
+ numpy
4
+
space.yaml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Semantic Hunter
3
+ emoji: 🔍
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
static/css/style.css ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Main Styles for Semantic Hunter */
2
+ :root {
3
+ --primary-color: #0f172a;
4
+ --secondary-color: #334155;
5
+ --highlight-color: #f59e0b;
6
+ --text-color: #f8fafc;
7
+ --error-color: #ef4444;
8
+ --success-color: #22c55e;
9
+ --input-bg: #1e293b;
10
+ --card-bg: #1e293b;
11
+ --separator-color: rgba(255, 255, 255, 0.1);
12
+ --yellow-bright: #ffee00;
13
+ --yellow-medium: #ffd700;
14
+ --orange-color: #ff9900;
15
+ }
16
+
17
+ * {
18
+ margin: 0;
19
+ padding: 0;
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ body {
24
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
25
+ background-color: var(--primary-color);
26
+ color: var(--text-color);
27
+ line-height: 1.6;
28
+ }
29
+
30
+ .container {
31
+ max-width: 800px;
32
+ margin: 0 auto;
33
+ padding: 2rem 1rem;
34
+ }
35
+
36
+ header {
37
+ display: flex;
38
+ justify-content: space-between;
39
+ align-items: center;
40
+ margin-bottom: 2rem;
41
+ flex-wrap: wrap;
42
+ gap: 1rem;
43
+ }
44
+
45
+ h1 {
46
+ font-size: 2.5rem;
47
+ color: var(--highlight-color);
48
+ }
49
+
50
+ .subtitle {
51
+ font-size: 0.7em;
52
+ opacity: 0.8;
53
+ }
54
+
55
+ .controls {
56
+ display: flex;
57
+ gap: 0.5rem;
58
+ }
59
+
60
+ .btn {
61
+ padding: 0.6rem 1.2rem;
62
+ border: none;
63
+ border-radius: 0.5rem;
64
+ background-color: var(--secondary-color);
65
+ color: var(--text-color);
66
+ cursor: pointer;
67
+ transition: all 0.3s ease;
68
+ font-size: 1rem;
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 0.5rem;
72
+ }
73
+
74
+ .btn:hover {
75
+ opacity: 0.9;
76
+ transform: translateY(-2px);
77
+ }
78
+
79
+ .btn-primary {
80
+ background-color: var(--highlight-color);
81
+ color: var(--primary-color);
82
+ }
83
+
84
+ .btn-danger {
85
+ background-color: var(--error-color);
86
+ }
87
+
88
+ .game-area {
89
+ margin-bottom: 2rem;
90
+ background-color: var(--card-bg);
91
+ padding: 2rem;
92
+ border-radius: 1rem;
93
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
94
+ }
95
+
96
+ h2 {
97
+ margin-bottom: 1.5rem;
98
+ text-align: center;
99
+ }
100
+
101
+ .input-area {
102
+ display: flex;
103
+ gap: 0.5rem;
104
+ margin-bottom: 1.5rem;
105
+ }
106
+
107
+ #guess-input {
108
+ flex: 1;
109
+ padding: 0.8rem 1rem;
110
+ border: none;
111
+ border-radius: 0.5rem;
112
+ background-color: var(--input-bg);
113
+ color: var(--text-color);
114
+ font-size: 1.1rem;
115
+ }
116
+
117
+ #guess-input:focus {
118
+ outline: 2px solid var(--highlight-color);
119
+ }
120
+
121
+ .message-area {
122
+ text-align: center;
123
+ min-height: 1.5rem;
124
+ }
125
+
126
+ .history-area {
127
+ background-color: var(--card-bg);
128
+ padding: 2rem;
129
+ border-radius: 1rem;
130
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
131
+ }
132
+
133
+ h3 {
134
+ margin-bottom: 1.5rem;
135
+ text-align: center;
136
+ color: var(--highlight-color);
137
+ display: none; /* Hide the Guess History heading */
138
+ }
139
+
140
+ .guesses-list {
141
+ display: flex;
142
+ flex-direction: column;
143
+ }
144
+
145
+ /* Latest guess container styles */
146
+ .latest-guess-container {
147
+ margin-bottom: 0.5rem;
148
+ }
149
+
150
+ /* Latest guess now has a background */
151
+ .latest-guess {
152
+ display: flex;
153
+ justify-content: space-between;
154
+ padding: 0.8rem 1rem;
155
+ border-radius: 0.5rem;
156
+ background-color: var(--secondary-color);
157
+ color: var(--text-color);
158
+ }
159
+
160
+ /* Separator line */
161
+ .guess-separator {
162
+ height: 1px;
163
+ background-color: var(--separator-color);
164
+ margin: 0.5rem 0 1rem 0;
165
+ width: 100%;
166
+ }
167
+
168
+ /* Regular history containers */
169
+ .history-guesses-container {
170
+ display: flex;
171
+ flex-direction: column;
172
+ gap: 0.8rem;
173
+ margin-bottom: 1.5rem;
174
+ }
175
+
176
+ /* History items now have no background */
177
+ .guess-item {
178
+ display: flex;
179
+ justify-content: space-between;
180
+ padding: 0rem 1rem;
181
+ border-radius: 0.5rem;
182
+ background-color: transparent;
183
+ color: #ddd;
184
+ transition: all 0.3s ease;
185
+ }
186
+
187
+ /* Special style for 100% match */
188
+ .perfect-match {
189
+ background-color: rgba(34, 197, 94, 0.2);
190
+ border-radius: 0.5rem;
191
+ border: 1px solid var(--success-color);
192
+ }
193
+
194
+ .guess-word {
195
+ font-weight: bold;
196
+ }
197
+
198
+ /* Base similarity style */
199
+ .guess-similarity {
200
+ font-family: 'Courier New', Courier, monospace;
201
+ font-size: 1.25rem;
202
+ font-weight: bold;
203
+ letter-spacing: 0.05em;
204
+ }
205
+
206
+ /* 1. 0-50%: Default style */
207
+ .similarity-low {
208
+ color: #aaa;
209
+ }
210
+
211
+ /* 2. 50-70%: Orange */
212
+ .similarity-medium {
213
+ color: var(--orange-color);
214
+ }
215
+
216
+ /* 3. 70-80%: Yellow */
217
+ .similarity-high {
218
+ color: var(--yellow-medium);
219
+ }
220
+
221
+ /* 4. 80-90%: Bright yellow with glow */
222
+ .similarity-very-high {
223
+ color: var(--yellow-bright);
224
+ text-shadow: 0 0 5px var(--yellow-bright);
225
+ }
226
+
227
+ /* 5. 90-99%: Orbitron font + bright yellow with glow */
228
+ .similarity-super-high {
229
+ font-family: 'Orbitron', 'Courier New', Courier, monospace;
230
+ color: var(--yellow-bright);
231
+ text-shadow: 0 0 8px var(--yellow-bright);
232
+ }
233
+
234
+ /* 6. 100%: Green with special container */
235
+ .similarity-perfect {
236
+ font-family: 'Orbitron', 'Courier New', Courier, monospace;
237
+ color: var(--success-color);
238
+ text-shadow: 0 0 10px var(--success-color);
239
+ }
240
+
241
+ /* Pagination styles */
242
+ .pagination-container {
243
+ display: flex;
244
+ justify-content: center;
245
+ align-items: center;
246
+ padding: 1rem 0;
247
+ gap: 1.5rem;
248
+ }
249
+
250
+ .pagination-btn {
251
+ display: flex;
252
+ justify-content: center;
253
+ align-items: center;
254
+ width: 2.5rem;
255
+ height: 2.5rem;
256
+ border-radius: 50%;
257
+ background-color: var(--secondary-color);
258
+ color: var(--text-color);
259
+ border: none;
260
+ font-size: 1rem;
261
+ cursor: pointer;
262
+ transition: all 0.2s ease;
263
+ }
264
+
265
+ .pagination-btn:hover:not(:disabled) {
266
+ background-color: var(--highlight-color);
267
+ color: var(--primary-color);
268
+ transform: translateY(-2px);
269
+ }
270
+
271
+ .pagination-btn:disabled {
272
+ opacity: 0.5;
273
+ cursor: not-allowed;
274
+ }
275
+
276
+ .page-indicator {
277
+ font-family: monospace;
278
+ font-size: 1.1rem;
279
+ color: var(--text-color);
280
+ opacity: 0.8;
281
+ }
282
+
283
+ footer {
284
+ margin-top: 3rem;
285
+ text-align: center;
286
+ opacity: 0.7;
287
+ font-size: 0.9rem;
288
+ }
289
+
290
+ /* Responsive */
291
+ @media (max-width: 600px) {
292
+ header {
293
+ flex-direction: column;
294
+ align-items: flex-start;
295
+ }
296
+
297
+ .controls {
298
+ width: 100%;
299
+ justify-content: space-between;
300
+ }
301
+
302
+ .game-area, .history-area {
303
+ padding: 1.5rem 1rem;
304
+ }
305
+ }
static/js/app.js ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Semantic Hunter Game Frontend
2
+
3
+ document.addEventListener('DOMContentLoaded', () => {
4
+ // DOM Elements
5
+ const guessInput = document.getElementById('guess-input');
6
+ const guessBtn = document.getElementById('guess-btn');
7
+ const messageArea = document.getElementById('message-area');
8
+ const guessesList = document.getElementById('guesses-list');
9
+ const newGameBtn = document.getElementById('new-game-btn');
10
+ const giveUpBtn = document.getElementById('give-up-btn');
11
+
12
+ // Game state
13
+ let gameActive = false;
14
+ let latestGuessId = null;
15
+ let allGuesses = [];
16
+ let currentPage = 1;
17
+ const itemsPerPage = 7;
18
+
19
+ // Initialize game
20
+ initGame();
21
+
22
+ // Event listeners
23
+ guessBtn.addEventListener('click', submitGuess);
24
+ guessInput.addEventListener('keypress', (e) => {
25
+ if (e.key === 'Enter') submitGuess();
26
+ });
27
+ newGameBtn.addEventListener('click', initGame);
28
+ giveUpBtn.addEventListener('click', giveUp);
29
+
30
+ // Functions
31
+ function initGame() {
32
+ fetch('/new-game', {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Content-Type': 'application/json'
36
+ }
37
+ })
38
+ .then(response => response.json())
39
+ .then(data => {
40
+ if (data.status === 'success') {
41
+ gameActive = true;
42
+ guessesList.innerHTML = '';
43
+ messageArea.innerHTML = '';
44
+ guessInput.value = '';
45
+ guessInput.focus();
46
+ allGuesses = [];
47
+ currentPage = 1;
48
+ latestGuessId = null;
49
+ messageArea.innerHTML = `<p class="message">游戏开始! 尝试猜测秘密词语!</p>`;
50
+ } else {
51
+ showError(data.message || '初始化游戏时出错');
52
+ }
53
+ })
54
+ .catch(err => {
55
+ showError('无法连接到服务器');
56
+ console.error(err);
57
+ });
58
+ }
59
+
60
+ function submitGuess() {
61
+ const guess = guessInput.value.trim();
62
+ if (!guess) return;
63
+ if (!gameActive) {
64
+ showError('游戏未开始,请点击新游戏');
65
+ return;
66
+ }
67
+
68
+ // Check if this word has already been guessed, but just proceed silently
69
+ const alreadyGuessed = allGuesses.some(g => g.word === guess) ||
70
+ (latestGuessId && latestGuessId === guess);
71
+
72
+ // Always send the request to get similarity, but we'll handle duplicates when updating the UI
73
+ fetch('/guess', {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json'
77
+ },
78
+ body: JSON.stringify({ guess })
79
+ })
80
+ .then(response => response.json())
81
+ .then(data => {
82
+ if (data.status === 'success') {
83
+ // If the word was already guessed, just update the latest guess display
84
+ // without adding a duplicate to the history
85
+ if (alreadyGuessed) {
86
+ // Find this guess in our existing data to get its similarity
87
+ const existingGuess = allGuesses.find(g => g.word === guess);
88
+ if (existingGuess) {
89
+ // Show just the latest guess with this word
90
+ updateLatestGuess({
91
+ word: guess,
92
+ similarity: existingGuess.similarity,
93
+ is_correct: existingGuess.is_correct
94
+ });
95
+
96
+ // Show similarity message
97
+ showMessage(`相似度: ${existingGuess.similarity}%`);
98
+ }
99
+ } else {
100
+ // Normal flow for new guesses
101
+ updateGuessList(data.guesses, guess);
102
+
103
+ if (data.is_correct) {
104
+ gameActive = false;
105
+ showSuccess(`恭喜你猜对了! 秘密词语是: ${guess}`);
106
+ } else {
107
+ showMessage(`相似度: ${data.similarity}%`);
108
+ }
109
+ }
110
+
111
+ // Always update latest guess ID and clear input
112
+ latestGuessId = guess;
113
+ guessInput.value = '';
114
+ guessInput.focus();
115
+ } else {
116
+ showError(data.message || '提交猜测时出错');
117
+ }
118
+ })
119
+ .catch(err => {
120
+ showError('无法连接到服务器');
121
+ console.error(err);
122
+ });
123
+ }
124
+
125
+ function giveUp() {
126
+ if (!gameActive) {
127
+ showError('游戏未开始,请点击新游戏');
128
+ return;
129
+ }
130
+
131
+ fetch('/give-up', {
132
+ method: 'POST',
133
+ headers: {
134
+ 'Content-Type': 'application/json'
135
+ }
136
+ })
137
+ .then(response => response.json())
138
+ .then(data => {
139
+ if (data.status === 'success') {
140
+ gameActive = false;
141
+ showError(`你放弃了! 秘密词语是: ${data.secret_word}`);
142
+ } else {
143
+ showError(data.message || '放弃游戏时出错');
144
+ }
145
+ })
146
+ .catch(err => {
147
+ showError('无法连接到服务器');
148
+ console.error(err);
149
+ });
150
+ }
151
+
152
+ function updateGuessList(guesses, latestGuess) {
153
+ guessesList.innerHTML = '';
154
+
155
+ // Create a copy of the guesses array to avoid modifying the original
156
+ const sortedGuesses = [...guesses];
157
+
158
+ // Find the latest guess and remove it from the sorted array
159
+ const latestGuessIndex = sortedGuesses.findIndex(g => g.word === latestGuess);
160
+ let latestGuessItem = null;
161
+ if (latestGuessIndex !== -1) {
162
+ latestGuessItem = sortedGuesses.splice(latestGuessIndex, 1)[0];
163
+ }
164
+
165
+ // Sort the remaining guesses by similarity (descending)
166
+ sortedGuesses.sort((a, b) => b.similarity - a.similarity);
167
+
168
+ // Update our allGuesses array for pagination
169
+ allGuesses = sortedGuesses;
170
+
171
+ // If we have a latest guess, add it to the top of the list
172
+ if (latestGuessItem) {
173
+ // Create a container for the latest guess
174
+ const latestGuessContainer = document.createElement('div');
175
+ latestGuessContainer.className = 'latest-guess-container';
176
+
177
+ // Add the latest guess with a different style
178
+ const latestGuessElement = document.createElement('div');
179
+ latestGuessElement.className = 'latest-guess';
180
+
181
+ // Add perfect-match class if 100% similarity
182
+ if (latestGuessItem.similarity === 100) {
183
+ latestGuessElement.classList.add('perfect-match');
184
+ }
185
+
186
+ // Determine similarity color class
187
+ let similarityClass = getSimilarityClass(latestGuessItem.similarity);
188
+
189
+ latestGuessElement.innerHTML = `
190
+ <span class="guess-word">${latestGuessItem.word}</span>
191
+ <span class="guess-similarity ${similarityClass}">${latestGuessItem.similarity}%</span>
192
+ `;
193
+
194
+ latestGuessContainer.appendChild(latestGuessElement);
195
+
196
+ // Add separator line
197
+ const separator = document.createElement('div');
198
+ separator.className = 'guess-separator';
199
+ latestGuessContainer.appendChild(separator);
200
+
201
+ guessesList.appendChild(latestGuessContainer);
202
+ }
203
+
204
+ // Create a container for history guesses
205
+ const historyContainer = document.createElement('div');
206
+ historyContainer.className = 'history-guesses-container';
207
+
208
+ // Get current page of items
209
+ const startIndex = (currentPage - 1) * itemsPerPage;
210
+ const endIndex = Math.min(startIndex + itemsPerPage, allGuesses.length);
211
+ const currentPageItems = allGuesses.slice(startIndex, endIndex);
212
+
213
+ // Add the current page of guesses
214
+ currentPageItems.forEach(guess => {
215
+ const guessItem = document.createElement('div');
216
+ guessItem.className = 'guess-item';
217
+
218
+ // Add perfect-match class if 100% similarity
219
+ if (guess.similarity === 100) {
220
+ guessItem.classList.add('perfect-match');
221
+ }
222
+
223
+ // Determine similarity color class
224
+ let similarityClass = getSimilarityClass(guess.similarity);
225
+
226
+ guessItem.innerHTML = `
227
+ <span class="guess-word">${guess.word}</span>
228
+ <span class="guess-similarity ${similarityClass}">${guess.similarity}%</span>
229
+ `;
230
+
231
+ historyContainer.appendChild(guessItem);
232
+ });
233
+
234
+ guessesList.appendChild(historyContainer);
235
+
236
+ // Add pagination if we have more than one page
237
+ if (allGuesses.length > itemsPerPage) {
238
+ addPagination();
239
+ }
240
+ }
241
+
242
+ function addPagination() {
243
+ const totalPages = Math.ceil(allGuesses.length / itemsPerPage);
244
+
245
+ // Create pagination container
246
+ const paginationContainer = document.createElement('div');
247
+ paginationContainer.className = 'pagination-container';
248
+
249
+ // Previous button
250
+ const prevBtn = document.createElement('button');
251
+ prevBtn.className = 'pagination-btn prev-btn';
252
+ prevBtn.innerHTML = `<i class="fas fa-chevron-left"></i>`;
253
+ prevBtn.disabled = currentPage === 1;
254
+ prevBtn.addEventListener('click', () => {
255
+ if (currentPage > 1) {
256
+ currentPage--;
257
+ updateGuessList(allGuesses.concat([]), ''); // Pass empty string for latestGuess to avoid highlighting
258
+ }
259
+ });
260
+
261
+ // Page indicator
262
+ const pageIndicator = document.createElement('div');
263
+ pageIndicator.className = 'page-indicator';
264
+ pageIndicator.textContent = `${currentPage} / ${totalPages}`;
265
+
266
+ // Next button
267
+ const nextBtn = document.createElement('button');
268
+ nextBtn.className = 'pagination-btn next-btn';
269
+ nextBtn.innerHTML = `<i class="fas fa-chevron-right"></i>`;
270
+ nextBtn.disabled = currentPage === totalPages;
271
+ nextBtn.addEventListener('click', () => {
272
+ if (currentPage < totalPages) {
273
+ currentPage++;
274
+ updateGuessList(allGuesses.concat([]), ''); // Pass empty string for latestGuess to avoid highlighting
275
+ }
276
+ });
277
+
278
+ // Append buttons to container
279
+ paginationContainer.appendChild(prevBtn);
280
+ paginationContainer.appendChild(pageIndicator);
281
+ paginationContainer.appendChild(nextBtn);
282
+
283
+ // Add to the DOM
284
+ guessesList.appendChild(paginationContainer);
285
+ }
286
+
287
+ function showMessage(msg) {
288
+ messageArea.innerHTML = `<p class="message">${msg}</p>`;
289
+ }
290
+
291
+ function showError(msg) {
292
+ messageArea.innerHTML = `<p class="message error">${msg}</p>`;
293
+ }
294
+
295
+ function showSuccess(msg) {
296
+ messageArea.innerHTML = `<p class="message success">${msg}</p>`;
297
+ }
298
+
299
+ // Function to determine similarity class based on percentage
300
+ function getSimilarityClass(similarity) {
301
+ if (similarity === 100) {
302
+ return 'similarity-perfect';
303
+ } else if (similarity >= 90) {
304
+ return 'similarity-super-high';
305
+ } else if (similarity >= 80) {
306
+ return 'similarity-very-high';
307
+ } else if (similarity >= 70) {
308
+ return 'similarity-high';
309
+ } else if (similarity >= 50) {
310
+ return 'similarity-medium';
311
+ } else {
312
+ return 'similarity-low';
313
+ }
314
+ }
315
+
316
+ // Function to just update the latest guess display without changing history
317
+ function updateLatestGuess(guessItem) {
318
+ // Remove existing latest guess container if it exists
319
+ const existingContainer = document.querySelector('.latest-guess-container');
320
+ if (existingContainer) {
321
+ existingContainer.remove();
322
+ }
323
+
324
+ // Create a new container for the latest guess
325
+ const latestGuessContainer = document.createElement('div');
326
+ latestGuessContainer.className = 'latest-guess-container';
327
+
328
+ // Add the latest guess with its style
329
+ const latestGuessElement = document.createElement('div');
330
+ latestGuessElement.className = 'latest-guess';
331
+
332
+ // Add perfect-match class if 100% similarity
333
+ if (guessItem.similarity === 100) {
334
+ latestGuessElement.classList.add('perfect-match');
335
+ }
336
+
337
+ // Determine similarity color class
338
+ let similarityClass = getSimilarityClass(guessItem.similarity);
339
+
340
+ latestGuessElement.innerHTML = `
341
+ <span class="guess-word">${guessItem.word}</span>
342
+ <span class="guess-similarity ${similarityClass}">${guessItem.similarity}%</span>
343
+ `;
344
+
345
+ latestGuessContainer.appendChild(latestGuessElement);
346
+
347
+ // Add separator line
348
+ const separator = document.createElement('div');
349
+ separator.className = 'guess-separator';
350
+ latestGuessContainer.appendChild(separator);
351
+
352
+ // Add to DOM before the history container
353
+ const historyContainer = document.querySelector('.history-guesses-container');
354
+ if (historyContainer) {
355
+ guessesList.insertBefore(latestGuessContainer, historyContainer);
356
+ } else {
357
+ guessesList.appendChild(latestGuessContainer);
358
+ }
359
+ }
360
+ });
templates/index.html ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>词猎人 | Semantic Hunter</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap" rel="stylesheet">
12
+ </head>
13
+ <body>
14
+ <div class="container">
15
+ <header>
16
+ <h1>词猎人 <span class="subtitle">| Semantic Hunter</span></h1>
17
+ <div class="controls">
18
+ <button id="new-game-btn" class="btn"><i class="fas fa-sync-alt"></i> 新游戏</button>
19
+ <button id="give-up-btn" class="btn btn-danger"><i class="fas fa-flag"></i> 放弃</button>
20
+ </div>
21
+ </header>
22
+
23
+ <main>
24
+ <div class="game-area">
25
+ <h2>Can you guess the secret word?</h2>
26
+ <div class="input-area">
27
+ <input type="text" id="guess-input" placeholder="Can you guess the word?" autofocus>
28
+ <button id="guess-btn" class="btn btn-primary">
29
+ <i class="fas fa-arrow-right"></i>
30
+ </button>
31
+ </div>
32
+ <div id="message-area" class="message-area"></div>
33
+ </div>
34
+
35
+ <div class="history-area">
36
+ <div id="guesses-list" class="guesses-list">
37
+ <!-- Guesses will be added here dynamically -->
38
+ </div>
39
+ </div>
40
+ </main>
41
+
42
+ <footer>
43
+ <p>词猎人 | Semantic Hunter &copy; 2023</p>
44
+ </footer>
45
+ </div>
46
+
47
+ <script src="{{ url_for('static', filename='js/app.js') }}"></script>
48
+ </body>
49
+ </html>
words.txt ADDED
@@ -0,0 +1,510 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 草莓
2
+ 吊灯
3
+ 番茄酱
4
+ 牙膏
5
+ 彩虹
6
+ 双层床
7
+ 桌游
8
+ 蜂窝
9
+ 柠檬
10
+ 威化饼干
11
+ 气泡
12
+ 哨子
13
+ 雪球
14
+ 耳机
15
+ 烟花
16
+ 冰屋
17
+ 香蕉皮
18
+ 割草机
19
+ 纸杯蛋糕
20
+ 睡袋
21
+
22
+
23
+ 操场
24
+ 奇异果
25
+ 南瓜
26
+ 龙虾
27
+ 机器人
28
+ 棒棒糖
29
+ 沙堡
30
+ 城堡
31
+ 磁铁
32
+ 拖鞋
33
+ 链锯
34
+ 扩音器
35
+ 马戏团
36
+ 美人鱼
37
+ 电脑
38
+ 小型货车
39
+ 婴儿床
40
+ 蝌蚪
41
+
42
+ 音乐
43
+ 帐篷
44
+ 哑铃
45
+ 望远镜
46
+ 黄鳝
47
+ 护士
48
+ 火车
49
+ 猫头鹰
50
+ 三轮车
51
+ 旗帜
52
+ 芭蕾舞
53
+ 钢琴
54
+ 垃圾
55
+ 公园
56
+ 海盗
57
+ 滑雪
58
+ 棒球
59
+ 女王
60
+ 高尔夫球
61
+ 热狗
62
+
63
+ iPad
64
+ 青蛙
65
+ 床垫
66
+ 蛋糕
67
+ 电池
68
+ 牛仔
69
+ 老师
70
+ 蝙蝠侠
71
+ 海绵宝宝
72
+ 超人
73
+ 蜘蛛侠
74
+ 吸血鬼
75
+ 生日
76
+ 安全带
77
+ 烤鸭
78
+ 考拉
79
+ 面具
80
+ 香水
81
+ 照片
82
+ wifi
83
+ 满月
84
+ 僵尸
85
+ 游戏
86
+ 胡萝卜
87
+ 洋葱
88
+
89
+ 沙发
90
+ 电话
91
+ 鼻子
92
+
93
+ 斑马
94
+ 蚊子
95
+ 蜜蜂
96
+
97
+ 潜艇
98
+
99
+ 生日蛋糕
100
+ 自行车
101
+ 茶壶
102
+ 膝盖
103
+ 菠萝
104
+
105
+ 水池
106
+ 坚果
107
+ 苹果
108
+ 香蕉
109
+ 大象
110
+
111
+ 天使
112
+ 眼球
113
+ 婴儿
114
+ 胡须
115
+ 飞碟
116
+ 回收
117
+ 圣经
118
+ 长颈鹿
119
+ 比基尼
120
+ 眼镜
121
+ 雪花
122
+ 高跟鞋
123
+ 楼梯
124
+
125
+ 蛋卷冰淇淋
126
+ 海星
127
+ 熊蜂
128
+ 蝴蝶
129
+ 瓢虫
130
+ 太阳
131
+ 相机
132
+
133
+
134
+ 狮子
135
+ 吐司
136
+ 教会
137
+ 邮箱
138
+ 牙刷
139
+ 蜡笔
140
+ 夜晚
141
+ 海豚
142
+ 卡车
143
+
144
+ 奥运会
145
+ 排球
146
+ 埃菲尔铁塔
147
+ 花生
148
+
149
+ 口红
150
+
151
+ 公共汽车
152
+ 车祸
153
+ 棒糖
154
+ 电锯
155
+ 滚雪球
156
+ 指甲
157
+ 锤子
158
+ 马戏团帐篷
159
+ 自由女神像
160
+ 大本钟
161
+ 圆锥形帐篷
162
+ 壶铃
163
+ 北极
164
+ 南极
165
+ 鳗鱼
166
+ 摩天轮
167
+ 原子
168
+ 奶嘴
169
+ 短裙
170
+ 垃圾邮件
171
+ 吉他
172
+ 后座
173
+ 高脚椅
174
+
175
+ 扇子
176
+
177
+ 椅子
178
+ 书架
179
+ 图片
180
+ 壁橱
181
+ 衣柜
182
+ 枕头
183
+ 毯子
184
+ 垃圾桶
185
+ 电视
186
+ 洗手间
187
+ 洗衣机
188
+ 烘干机
189
+ 淋浴
190
+ 浴缸
191
+ 卫生纸
192
+ 剃刀
193
+ 毛巾
194
+ 钩子
195
+ 洗发水
196
+ 桌子
197
+ 长椅
198
+ 花瓶
199
+ 火炉
200
+ 冰箱
201
+ 电饭锅
202
+ 洗碗机
203
+ 时间表
204
+ 日历
205
+ 梳子
206
+ 衣服
207
+ 杯子
208
+ 窗帘
209
+
210
+ 屏幕
211
+ 床单
212
+ 手帕
213
+ 手提包
214
+ 夹子
215
+ 剪刀
216
+
217
+
218
+ 筷子
219
+ 瓶子
220
+ 老鼠
221
+ 金属丝
222
+ 笔记本电脑
223
+ 钥匙
224
+ 充电器
225
+ USB
226
+ 处理
227
+ 别针
228
+ 抽屉
229
+ 手电筒
230
+ 红绿灯
231
+ 药丸
232
+ 药品
233
+ 歌曲
234
+
235
+
236
+ 长笛
237
+ 喇叭
238
+ 大提琴
239
+ 竖琴
240
+ 戒指
241
+ 钻石
242
+ 树叶
243
+ 地球
244
+ 阴影
245
+ 嘴唇
246
+ 罗盘
247
+
248
+
249
+ 钓竿
250
+ 医院
251
+ 酒店
252
+ 工厂
253
+ 摩托车
254
+
255
+ 监狱
256
+ 囚犯
257
+
258
+
259
+
260
+ 斧头
261
+
262
+ 衬衫
263
+ 裤子
264
+ 内裤
265
+ 地平线
266
+ 垂直的
267
+
268
+ 钱包
269
+ 西瓜
270
+ 香肠
271
+ 皮卡丘
272
+ 土豆
273
+ 小麦
274
+ 可可
275
+ 手套
276
+ 飞机
277
+ 盆地
278
+
279
+ 糖果
280
+
281
+
282
+
283
+ 键盘
284
+ 黑色的
285
+ 馅饼
286
+
287
+ 裙子
288
+
289
+ 啤酒
290
+ 岩石
291
+ 牙齿
292
+
293
+ 眼睛
294
+ 耳朵
295
+ 骨头
296
+ 线
297
+ 头发
298
+
299
+ 游泳
300
+ 射击
301
+ 地图
302
+ 拥抱
303
+ 铅笔
304
+
305
+ 可乐
306
+ 烤箱
307
+
308
+ 手指
309
+ 卡片
310
+
311
+ 学校
312
+ 沙拉
313
+ 面包
314
+ 花朵
315
+ 窗户
316
+
317
+
318
+
319
+
320
+
321
+ 咖啡
322
+ 牛奶
323
+ 垃圾箱
324
+ 摇篮
325
+ 消防队员
326
+ 警察
327
+ 医生
328
+ 司机
329
+ 银行
330
+ 美国
331
+ 中国
332
+ 日本
333
+ 德国
334
+ 英国
335
+
336
+
337
+ T恤
338
+ 叶子
339
+
340
+ 步枪
341
+ 霰弹枪
342
+
343
+
344
+ 颅骨
345
+ 蘑菇
346
+ 橙子
347
+
348
+ 雷达
349
+ 收音机
350
+ 嗓音
351
+ 黑洞
352
+ 炒鸡蛋
353
+ 熏肉
354
+ 暴风雪
355
+ 海啸
356
+ 跳绳
357
+ 袋鼠
358
+ 队长
359
+ 妖精
360
+ 日食
361
+
362
+ 空间
363
+ 听诊器
364
+ 游轮
365
+ 战列舰
366
+ 喷射
367
+ 机械
368
+ 牙医
369
+
370
+ 舞蹈
371
+ 妈妈
372
+ 爸爸
373
+ Facebook
374
+ 推特
375
+ 连裤袜
376
+ 游客
377
+ 导游
378
+ 平坦的
379
+ 纸盘子
380
+ 框架
381
+ 无线上网
382
+ 狼人
383
+ 向导
384
+ 巫婆
385
+ 朝圣
386
+ 美洲原住民
387
+ 飞行员
388
+ 鹦鹉
389
+ 视频游戏
390
+ 哑剧
391
+ 冰雹
392
+ 密码
393
+ 激光
394
+ 神话
395
+ 素食主义者
396
+ 丛林
397
+ 农民
398
+ 镜子
399
+ 下载
400
+ 滑雪镜
401
+ 尖叫
402
+ 说谎
403
+ 橡皮
404
+ 闪光
405
+ 蚱蜢
406
+ 理发师
407
+ 维他命
408
+ 服务员
409
+ 伐木工人
410
+ 救生员
411
+ 矿工
412
+ 图书管理员
413
+ 法官
414
+ 宇航员
415
+ 渔夫
416
+ 贝克
417
+ 栅栏
418
+ 狗窝
419
+ 微波
420
+ 动物园
421
+ 公交车站
422
+ 火车站
423
+ 水塔
424
+
425
+ 腰带
426
+ 领结
427
+ 连帽衫
428
+ 手表
429
+ 制服
430
+
431
+ 前额
432
+ 牙龈
433
+ 食指
434
+
435
+ 指关节
436
+ 鼻孔
437
+ 耳垂
438
+ 瞳孔
439
+ 虹膜
440
+ 订书机
441
+ 薪水
442
+ 计算器
443
+ 蜘蛛
444
+ 果冻
445
+ 海蜇
446
+ 冲浪板
447
+ 松鼠
448
+ 独角兽
449
+ 吊床
450
+ 金字塔
451
+ 木乃伊
452
+ 鸡皮疙瘩
453
+ 鸭嘴兽
454
+ 金鱼
455
+ 蜘蛛网
456
+ 海藻
457
+ 齿
458
+
459
+ 家具
460
+ 蜡烛
461
+
462
+
463
+ 发胶
464
+ 围巾
465
+ 时光机器
466
+ 忍者
467
+ 投手
468
+
469
+ 呼啦圈
470
+ 灭虫器
471
+ 假发
472
+ 饼形图
473
+ 水枪
474
+ 白天
475
+ 夜间
476
+ 长城
477
+ 吸血蝙蝠
478
+ X射线
479
+ 打哈欠
480
+ 无形的
481
+ 吸尘器
482
+
483
+
484
+ 天线
485
+ 小行星
486
+ 墙纸
487
+ 压力
488
+ 假期
489
+ 欢呼
490
+ 书呆子
491
+ 人工智能
492
+ 研究
493
+ 免疫系统
494
+ 仙女
495
+ 马蜂窝
496
+ 比萨斜塔
497
+ 北极光
498
+ 灰尘
499
+ 打嗝
500
+ 鼻涕泡
501
+ 青春痘
502
+ 水痘
503
+ 兔子
504
+ 蚂蚁
505
+ 花瓣
506
+ 海洋
507
+
508
+ 水果
509
+
510
+ 直升机