File size: 13,632 Bytes
f60623c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c2eafe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f60623c
7c2eafe
 
 
f60623c
7c2eafe
 
 
f60623c
 
 
 
 
 
 
 
7c2eafe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f60623c
7c2eafe
 
f60623c
 
7c2eafe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f60623c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c2eafe
 
f60623c
 
 
 
 
 
7c2eafe
 
f60623c
 
7c2eafe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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
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
# """
# Semantrix Game Module

# This module defines the Semantrix class, which implements a word guessing game using word embeddings. The game can be configured to use either a Word2Vec model or a SentenceTransformer model for word embeddings. The game supports multiple languages and difficulty levels.

# Classes:
#     Semantrix: A class that implements the Semantrix word guessing game.
#     Semantrix.DictWrapper: A helper class to wrap configuration dictionaries.

# Functions:
#     __init__(self, lang=0, model_type="SentenceTransformer"): Initializes the Semantrix game with the specified language and model type.
#     prepare_game(self, difficulty): Prepares the game with the selected difficulty level.
#     gen_rank(self, repeated): Generates the ranking file based on the scores.
#     play_game(self, word): Plays the game with the selected word and returns feedback.
#     curiosity(self): Generates a curiosity hint about the secret word once the game is over.

# Attributes:
#     model (KeyedVectors): The word embeddings model.
#     config_file_path (str): Path to the configuration file.
#     secret_file_path (str): Path to the secret words file.
#     data_path (str): Path to the data directory.
#     Config_full (dict): Full configuration data.
#     secret (dict): Secret words data.
#     lang (int): Language of the game (0 for Spanish, 1 for English).
#     model_type (str): Type of the model ("word2vec" or "SentenceTransformer").
#     Config (DictWrapper): Configuration data for the selected language.
#     secret_dict (dict): Secret words for the selected language.
#     secret_list (list): List of secret words for the selected difficulty.
#     words (list): List of words guessed by the player.
#     scores (list): List of scores for the guessed words.
#     win (bool): Indicates if the player has won the game.
#     n (int): Number of hints given.
#     recent_hint (int): Counter for recent hints.
#     f_dev_avg (float): Moving average of the tendency slope.
#     last_hint (int): Index of the last hint given.
#     difficulty (int): Difficulty level of the game.
# """

import json
import random
from datetime import datetime
import numpy as np
from gensim.models import KeyedVectors
from hints import curiosity, hint
from tracking import (
    calculate_moving_average,
    calculate_tendency_slope,
)
from sentence_transformers import SentenceTransformer
import warnings

warnings.filterwarnings(action="ignore", category=UserWarning, module="gensim")


# Define the class Semantrix
class Semantrix:

    # Define the paths for the configuration files and the data
    config_file_path = "config/lang.json"
    secret_file_path = "config/secret.json"
    data_path = "data/"

    # Define the class DictWrapper to store the configuration data
    class DictWrapper:
        def __init__(self, data_dict):
            self.__dict__.update(data_dict)

    # Define the constructor of the class which loads the configuration files and initializes the class variables depending on the language parameter and the model type
    def __init__(self, lang=0, model_type="SentenceTransformer"):

        # Load the configuration files
        with open(self.config_file_path, "r") as file:
            self.Config_full = json.load(file)

        # Load the secret file where the secret words are stored
        with open(self.secret_file_path, "r") as file:
            self.secret = json.load(file)

        # Set the language of the game
        self.lang = lang

        # Set the model type
        self.model_type = model_type

        if self.lang == 1:
            self.model = KeyedVectors.load("config/w2v_models/eng_w2v_model", mmap="r")
            self.Config = self.DictWrapper(self.Config_full["ENG"]["Game"])
            self.secret_dict = self.secret["ENG"]
        else:
            self.model = KeyedVectors.load("config/w2v_models/esp_w2v_model", mmap="r")
            self.Config = self.DictWrapper(self.Config_full["SPA"]["Game"])
            self.secret_dict = self.secret["SPA"]

        # Create empty KeyedVectors model with predefined size where the embeddings will be stored if Sentence Transformer used
        if self.model_type == "SentenceTransformer":
            self.model_trans = KeyedVectors(768)

        self.model_st = SentenceTransformer(
            "sentence-transformers/paraphrase-multilingual-mpnet-base-v2"
        )

        # Create the ranking file
        with open(self.data_path + "ranking.txt", "w+") as file:
            file.write("---------------------------")

    # Define the function to prepare the game with the selected difficulty
    def prepare_game(self, difficulty):

        # Set the secret list depending on the difficulty
        self.secret_list = (
            self.secret_dict["basic"]
            if difficulty <= 2
            else self.secret_dict["advanced"]
        )

        # Select a random secret word from the secret list
        self.secret = self.secret_list.pop(random.randint(0, len(self.secret_list) - 1))
        self.secret = self.secret.lower()

        # Store the secret word in the words list
        self.words = [self.Config.secret_word]

        # Store the score in the scores list
        self.scores = [10]

        # Store the embedding of the secret word in the embeddings dictionary
        if self.model_type == "SentenceTransformer":
            # Add the secret word to the KeyedVectors model if the model type is SentenceTransformer
            # If the model type is word2vec, the secret word is already in the model
            if self.secret not in self.model_trans.key_to_index.keys():
                self.model_trans.add_vector(
                    self.secret,
                    self.model_st.encode(self.secret, convert_to_tensor=True).tolist(),
                )

        # Initialize the game variables
        self.win = False
        self.n = 0
        self.recent_hint = 0
        self.f_dev_avg = 0
        self.last_hint = -1
        self.difficulty = difficulty

        # Set the number of hints depending on the difficulty
        if self.difficulty == 1:
            self.n = 3

    # Define the function to generate the ranking file
    def gen_rank(self, repeated):
        ascending_indices = np.argsort(self.scores)
        descending_indices = list(ascending_indices[::-1])
        ranking_data = []
        k = len(self.words) - 1
        if repeated != -1:
            k = repeated

        ranking_data.append(["#" + str(k), self.words[k], self.scores[k]])

        ranking_data.append("---------------------------")
        for i in descending_indices:
            if i == 0:
                continue
            ranking_data.append(["#" + str(i), self.words[i], self.scores[i]])

        with open(self.data_path + "ranking.txt", "w+") as file:
            for item in ranking_data:
                file.write("%s\n" % item)

    # Define the function to play the game with the selected word
    def play_game(self, word):

        # Convert the word to lowercase
        word = word.lower()

        # Check if the user wants to give up
        if word == "give_up":
            text = (
                "[lose]"
                + self.Config.Feedback_9
                + self.secret
                + "\n\n"
                + self.Config.Feedback_10
            )
            return text

        # Check if the word is repeated
        if word in self.words:
            repeated = self.words.index(word)
        else:
            repeated = -1
            self.words.append(word)

        # Check if the word is in the model already
        if word not in self.model.key_to_index.keys():
            # If the word is not in the model, remove it from the words list and provide feedback
            self.words.pop(len(self.words) - 1)
            feedback = (
                "I don't know that word. Try again."
                if self.lang == 1
                else "No conozco esa palabra. Inténtalo de nuevo."
            )
            feedback += (
                "[rank]" + open(self.data_path + "ranking.txt", "r").read()
                if len(self.words) > 1
                else "\n\n"
            )
            return feedback

        # Add the word to the KeyedVectors model if the model type is SentenceTransformer and the word is not already in the model
        if (
            self.model_type == "SentenceTransformer"
            and word not in self.model_trans.key_to_index.keys()
        ):
            self.model_trans.add_vector(
                word, self.model_st.encode(word, convert_to_tensor=True).tolist()
            )

        # Calculate the score of the word, apply logarithmic scaling, interpolate the score to a range from 0 to 10, and round it to two decimal places
        if self.model_type == "word2vec":
            similarity = self.model.similarity(self.secret, word)
        else:
            similarity = self.model_trans.similarity(self.secret, word)

        log_similarity = np.log10((similarity if similarity > 0 else 0) * 10)
        score = round(
            np.interp(
                log_similarity,
                [0, np.log10(10)],
                [0, 10],
            ),
            2,
        )

        # Remove the word from the score list if it is repeated
        if repeated == -1:
            self.scores.append(score)

        # Generate the feedback message depending on the score
        if score <= 2.5:
            feedback = self.Config.Feedback_0 + str(score)
        elif score > 2.5 and score <= 4.0:
            feedback = self.Config.Feedback_1 + str(score)
        elif score > 4.0 and score <= 6.0:
            feedback = self.Config.Feedback_2 + str(score)
        elif score > 6.0 and score <= 7.5:
            feedback = self.Config.Feedback_3 + str(score)
        elif score > 7.5 and score <= 8.0:
            feedback = self.Config.Feedback_4 + str(score)
        elif score > 8.0 and score < 10.0:
            feedback = self.Config.Feedback_5 + str(score)
        # If the score is 10, the user wins the game
        else:
            self.win = True
            feedback = "[win]" + self.Config.Feedback_8
            self.words[0] = self.secret
            self.words.pop(len(self.words) - 1)
            self.scores.pop(len(self.scores) - 1)

        # Generate the feedback message depending on the score and the previous score
        if score > self.scores[len(self.scores) - 2] and self.win == False:
            feedback += "\n" + self.Config.Feedback_6
        elif score < self.scores[len(self.scores) - 2] and self.win == False:
            feedback += "\n" + self.Config.Feedback_7

        ## Hint generation
        # If the difficulty is not 4, calculate the moving average of the scores and the tendency slope
        if self.difficulty != 4:
            mov_avg = calculate_moving_average(self.scores[1:], 5)

            # If the moving average has more than one element and the user has not won yet, calculate the tendency slope and the moving average of the tendency slope
            if len(mov_avg) > 1 and self.win == False:
                f_dev = calculate_tendency_slope(mov_avg)
                f_dev_avg = calculate_moving_average(f_dev, 3)

                # If the tendency slope is negative and the hint has not been given recently (at least three rounds earlier), generate a hint
                if f_dev_avg[len(f_dev_avg) - 1] < 0 and self.recent_hint == 0:

                    # Generate a random hint intro from the hint list
                    i = random.randint(0, len(self.Config.hint_intro) - 1)
                    feedback += "\n\n[hint]" + self.Config.hint_intro[i]

                    # Generate a dynamic hint
                    hint_text, self.n, self.last_hint = hint(
                        self.secret,
                        self.n,
                        self.model_st,
                        self.last_hint,
                        self.lang,
                        (
                            self.DictWrapper(self.Config_full["ENG"]["Hint"])
                            if self.lang == 1
                            else self.DictWrapper(self.Config_full["SPA"]["Hint"])
                        ),
                    )
                    feedback += "\n" + hint_text
                    self.recent_hint = 3

            if self.recent_hint != 0:
                self.recent_hint -= 1

        # Generate the ranking file
        self.gen_rank(repeated)

        # Add the ranking file to the feedback message
        feedback += "[rank]" + open(self.data_path + "ranking.txt", "r").read()

        # Save the ranking file with the plays of the user if the user wins
        if self.win:

            with open(self.data_path + "ranking.txt", "r") as original_file:
                file_content = original_file.readlines()

            new_file_name = self.secret + "_" + str(datetime.now()) + ".txt"

            with open(self.data_path + "plays/" + new_file_name, "w+") as new_file:
                new_file.writelines(file_content[2:])

        # Return the feedback message
        return feedback

    # Define the function to generate a curiosity hint once the game is over
    def curiosity(self):

        # Generate a curiosity aboyt the secret word
        feedback = curiosity(
            self.secret,
            (
                self.DictWrapper(self.Config_full["ENG"]["Hint"])
                if self.lang == 1
                else self.DictWrapper(self.Config_full["SPA"]["Hint"])
            ),
        )

        # Return the feedback message
        return feedback