5
from pathlib import Path
6
from typing import Dict, List, Union
10
from common.inflect import engine
11
from requests import RequestException
12
from common import utils
13
from common.combined_classes import TOPIC_GROUPS
16
VIDEO_GAME_WORDS_COMPILED_PATTERN = re.compile(
17
r"\bvideo ?game|\bgaming\b|\bplay ?station|\bx ?box\b|\bplay(?:ed|ing|s).*\b(?:tablet|pc|computer)\b|"
18
r"\bgames? for (?:android|pc|computer|play ?station|x ?box|tablet|ipad)\b|\b(what|which)\b.+\bgame.+\bplay.+\?",
22
CHECK_DEFINITELY_GAME_COMPILED_PATTERN = re.compile(
23
VIDEO_GAME_WORDS_COMPILED_PATTERN.pattern + r"|\bgames?\b|\bplay(?:ed|ing|s)\b", re.IGNORECASE
27
logger = logging.getLogger(__name__)
29
sentry_sdk.init(os.getenv("SENTRY_DSN"))
32
inflect_engine = engine()
35
def about_games(annotated_utterance):
36
found_topics = utils.get_topics(annotated_utterance, probs=False, which="all")
37
if any([game_topic in found_topics for game_topic in TOPIC_GROUPS["games"]]):
39
elif re.findall(VIDEO_GAME_WORDS_COMPILED_PATTERN, annotated_utterance["text"]):
75
ROMAN_NUMBER_COMPILED_PATTERN = re.compile(
76
r"\b(?:M{1,4}(?:CM|CD|DC{0,3}|C{1,3})?(?:XC|XL|LX{0,3}|X{1,3})?(?:IX|IV|VI{0,3}|I{1,3})?|"
77
r"(?:CM|CD|DC{0,3}|C{1,3})(?:XC|XL|LX{0,3}|X{1,3})?(?:IX|IV|VI{0,3}|I{1,3})?|"
78
r"(?:XC|XL|LX{0,3}|X{1,3})(?:IX|IV|VI{0,3}|I{1,3})?|"
79
r"(?:IX|IV|VI{0,3}|I{1,3}))\b",
82
INTEGER_PATTERN = re.compile(r"[1-9][0-9]*", re.I)
83
NUMBER_COMPILED_PATTERN = re.compile(ROMAN_NUMBER_COMPILED_PATTERN.pattern + "|" + INTEGER_PATTERN.pattern, re.I)
88
for r in INT_TO_ROMAN.keys():
90
yield INT_TO_ROMAN[r] * x
95
return "".join([a for a in roman_num(num)])
102
if i + 1 < len(s) and s[i : i + 2] in ROMAN_TO_INT:
103
num += ROMAN_TO_INT[s[i : i + 2]]
106
num += ROMAN_TO_INT[s[i]]
111
def roman_number_replace(match_obj):
112
i = roman_to_int(match_obj.group(0).upper())
113
words = inflect_engine.number_to_words(i)
114
return f"(?:part )?(?:{i}|{words}|{match_obj.group(0)})"
117
def integer_replace(match_obj):
118
i = int(match_obj.group(0))
119
roman = write_roman(i)
120
words = inflect_engine.number_to_words(i)
121
return f"(?:part )?(?:{i}|{words}|{roman})"
124
def number_replace(match_obj):
125
if ROMAN_NUMBER_COMPILED_PATTERN.match(match_obj.group(0)):
126
return roman_number_replace(match_obj)
128
return integer_replace(match_obj)
131
ARTICLE_PATTERN = re.compile(r"(\ba |\ban |\bthe )", re.I)
132
COLON_PATTERN = re.compile(r":")
133
ARTICLE_COLON_PATTERN = re.compile(ARTICLE_PATTERN.pattern + "|" + COLON_PATTERN.pattern, re.I)
136
def article_colon_replacement(match_obj):
137
s = match_obj.group(0)
138
if ARTICLE_PATTERN.match(s):
139
return ARTICLE_PATTERN.sub(r"(?:\1)?", s)
141
return COLON_PATTERN.sub(r":?", s)
144
def compose_game_name_re(name):
145
first_number = NUMBER_COMPILED_PATTERN.search(name)
146
if first_number is None:
147
no_numbers_name = None
149
no_numbers_name = ARTICLE_COLON_PATTERN.sub(article_colon_replacement, name[: first_number.start()]).strip()
150
if not no_numbers_name:
151
no_numbers_name = None
153
before_colon_name = name.split(":")[0].strip()
154
if before_colon_name:
155
before_colon_name = ARTICLE_COLON_PATTERN.sub(article_colon_replacement, before_colon_name)
156
before_colon_name = NUMBER_COMPILED_PATTERN.sub(number_replace, before_colon_name)
158
before_colon_name = None
160
before_colon_name = None
161
pattern = ARTICLE_COLON_PATTERN.sub(article_colon_replacement, name)
162
pattern = NUMBER_COMPILED_PATTERN.sub(number_replace, pattern)
163
return pattern, before_colon_name, no_numbers_name
166
def compile_re_pattern_for_list_of_strings(list_of_game_names: List[Union[str, List[str]]]):
167
full_name_patterns = []
177
before_number_name_to_full_names: Dict[str, Dict[str, Union[str, List[int]]]] = {}
180
full_names_without_numbers = []
185
alternative_names = {}
187
before_colon_names = []
188
for i, game_names in enumerate(list_of_game_names):
189
if isinstance(game_names, list):
190
main_name = game_names[0]
191
for name in game_names[1:]:
192
if name not in alternative_names:
193
alternative_names[name] = [i]
195
alternative_names[name].append(i)
197
main_name = game_names
198
full, before_colon, before_number = compose_game_name_re(main_name)
199
full_name_patterns.append(full)
200
before_colon_names.append(before_colon)
201
if before_number is not None:
202
before_number_l = before_number.lower()
203
if before_number_l in before_number_name_to_full_names:
204
before_number_name_to_full_names[before_number_l]["full_indices"].append(i)
206
before_number_name_to_full_names[before_number_l] = {"not_lowered": before_number, "full_indices": [i]}
208
full_names_without_numbers.append(full)
209
for full in full_names_without_numbers:
211
if full in before_number_name_to_full_names:
212
del before_number_name_to_full_names[full]
213
for before_number_info in before_number_name_to_full_names.values():
214
for i in before_number_info["full_indices"]:
215
full_name_patterns[i] += r"\b|\b" + before_number_info["not_lowered"]
216
for alternative_name, full_name_indices in alternative_names.items():
217
alternative_name_pattern = compose_game_name_re(alternative_name)[0]
218
for i in full_name_indices:
219
full_name_patterns[i] += r"\b|\b" + alternative_name_pattern
220
for i, name in enumerate(before_colon_names):
222
full_name_patterns[i] += r"\b|\b" + name
223
regex = "|".join([r"(\b" + p + r"\b)" for p in full_name_patterns])
224
return re.compile(regex, flags=re.I)
227
def load_json(file_path):
228
with open(file_path) as f:
233
path = Path(__file__).parent / Path("games_with_at_least_1M_copies_sold.json")
234
GAMES_WITH_AT_LEAST_1M_COPIES_SOLD = load_json(path)
235
GAMES_WITH_AT_LEAST_1M_COPIES_SOLD_COMPILED_PATTERN = compile_re_pattern_for_list_of_strings(
236
GAMES_WITH_AT_LEAST_1M_COPIES_SOLD
240
def find_games_in_text(text):
242
for match_groups in GAMES_WITH_AT_LEAST_1M_COPIES_SOLD_COMPILED_PATTERN.findall(text):
244
for i, name in enumerate(match_groups):
246
orig = GAMES_WITH_AT_LEAST_1M_COPIES_SOLD[i]
247
if isinstance(orig, list):
249
logger.info(f"orig: {orig}")
250
match_names.append(orig)
252
found_names.append(match_names)
256
VIDEO_GAME_WORDS_COMPILED_PATTERN = re.compile(
257
r"(?:\bvideo ?game|\bgam(?:e|es|ing)\b|\bplay ?station|\bplaying\b|\bx ?box\b|"
258
r"\bplay(ed|ing|s).*\b(tablet|pc|computer)\b)",
262
VIDEO_GAME_QUESTION_COMPILED_PATTERN = re.compile(
263
r"(?:\bvideo ?game|\bgam(?:e|es|ing)\b|\bplay ?station|\bplaying\b|\bx ?box\b|"
264
r"\bplay(ed|ing|s).*\b(tablet|pc|computer)\b)[a-zA-Z \-]+\?",
269
genre_and_theme_groups = {
270
"action": {"genres": [2, 4, 5, 10, 11, 12, 24, 25, 31, 36], "themes": [1, 23, 39]},
271
"history": {"genres": [11, 15, 16], "themes": [22, 39]},
278
"You know, many fantasy video games are based on movies or books. "
279
"My favorite fantasy movie is the Lord of the Rings. What is your favorite fantasy movie?"
282
"I also like science fiction movies. My favorite is Ex Machina. What is your favorite sci-fi movie?"
285
"To be honest, horror video games are not my favorite. "
286
"How about movies? What is your favorite horror movie?"
288
"Thriller": ["I think this game is too scary for me. What cool thriller movie do you remember?"],
290
"theme_genre_group": {
291
"action": ["Action games are cool but how about movies? What is your favorite action movie?"],
292
"history": ["Changing topic slightly, what is your favorite historical movie?"],
300
"Sometimes I like to imagine fantasy worlds myself and just look at something drawn by video game artist. "
301
"Could you tell me what is your favorite fantasy book?"
304
"Video games are not the only way to touch fantastic worlds. What is your favorite sci-fi book?"
306
"Horror": ["I never really liked horror video games. Books are less scary. What is your favorite horror book?"],
307
"Thriller": ["I think this game is too scary for me. Do you read thriller books?"],
309
"theme_genre_group": {
311
"History games are cool! But what about books that describe times long gone? Do you like such books?"
317
special_links_to_movies = {"Harry Potter": ["By the way, what is your favorite Harry Potter movie?"]}
320
special_links_to_books = {"Harry Potter": ["By the way, what Harry Potter book did impress you most?"]}
323
harry_potter_part_names = [
324
"Harry Potter and the Sorcerer's Stone",
325
"Harry Potter and the Chamber of Secrets",
326
"Harry Potter and the Prisoner of Azkaban",
327
"Harry Potter and the Goblet of Fire",
328
"Harry Potter and the Order of the Phoenix",
329
"Harry Potter and the Half-Blood Prince",
330
"Harry Potter and the Deathly Hallows",
334
harry_potter_part_number_words = [
335
["first", "one", "all", "every", "philosopher", "sorcerer", "stone"],
336
["second", "two", "chamber", "secret"],
337
["third", "three", "prisoner", "azkaban"],
338
["four", "fourth", "goblet", "fire"],
339
["fifth", "five", "order", "phoenix"],
340
["sixth", "six", "half", "blood", "prince"],
341
["last", "seventh", "eighth", "deathly", "hallows", "harry", "potter"],
345
def get_harry_potter_part_name_if_special_link_was_used(human_utterance, prev_bot_utterance):
346
prev_bot_utterance_text = prev_bot_utterance.get("text", "").lower()
347
human_utterance_text = human_utterance.get("text", "").lower()
348
special_link_tos = special_links_to_movies["Harry Potter"] + special_links_to_books["Harry Potter"]
350
if any([u.lower() in prev_bot_utterance_text.lower() for u in special_link_tos]):
351
for i, hpnw in enumerate(harry_potter_part_number_words):
352
if any([w in human_utterance_text for w in hpnw]):
353
part_name = harry_potter_part_names[i]
358
def compose_list_of_links(link_dict):
360
for v in link_dict.values():
361
for vv in v.values():
366
def compose_list_of_special_links(link_dict):
368
for v in link_dict.values():
373
ALL_LINKS_TO_BOOKS = compose_list_of_links(links_to_books) + compose_list_of_special_links(special_links_to_books)
376
def skill_trigger_phrases():
377
return ["What video game are you playing in recent days?", "What is your favorite video game?"]
380
ANSWER_TO_GENERAL_WISH_TO_DISCUSS_VIDEO_GAMES_AND_QUESTION_WHAT_GAME_YOU_PLAY = (
381
"Wow, video games are cool. "
382
"If I didn't love to chat so much, I would definitely played video games at least half a day. "
383
"What game are you playing now?"
387
CAN_CONTINUE_PHRASES = [
388
"I love games, especially stats like top of the games released.",
389
"Got a list of the top released games, wanna discuss it?",
390
"Which of these time periods is of interest for you?",
391
"Got a list of the top released games, wanna discuss it?",
392
"I can talk about the most popular games for this or last year, last month, or even the last week",
393
"released games highly rated in this year. Do you want to learn more?",
394
"If you want to discuss it in details say I want to talk about it.",
395
"Do you want to learn more about it, or shall we move on?",
396
"Have you played it before?",
397
"You can always talk to me about other popular games.",
398
"Do you want to chat about the best games of the past year, this year, last month or week?",
399
"Let me know if we should talk about the next one or discuss this one",
400
"Talking about it or going on?",
401
"Discussing it or moving on?",
402
"Chatting about it or the next one?",
403
"How would you rate the desire to play it again",
404
"Your rating is way lower than one given by the rest of the players.",
405
"My memory failed me and I can't recall anything else about the games.",
406
"one of my hobbies is keeping fresh stats about the top video games.",
407
"How would you rate the desire to play",
408
"I'd love to talk about other things but my developer forgot to add them to my memory banks.",
409
"I was talking about games, do you want to continue?",
410
"I can tell you about some music for gaming, should I continue?",
414
CAN_NOT_CONTINUE_PHRASES = [
415
"Do you think that pets can use gadgets the same way as humans?",
416
"play with my cat different games, such as run and fetch",
417
"I played with my cat a game",
418
"play with my dog different game",
419
"game that I like to play with my cat",
420
"playing with a pet makes a lot of fun",
424
def get_igdb_client_token(client_id, client_secret):
425
payload = {"client_id": client_id, "client_secret": client_secret, "grant_type": "client_credentials"}
426
url = "https://id.twitch.tv/oauth2/token?"
429
token_data = requests.post(url, params=payload, timeout=timeout)
430
except RequestException as e:
431
logger.warning(f"Request to {url} failed. `dff_gaming_skill` failed to get access to igdb.com. {e}")
434
token_data_json = token_data.json()
435
access_token = token_data_json.get("access_token")
436
if access_token is None:
438
f"Could not get access token for CLIENT_ID={client_id} and CLIENT_SECRET={client_secret}. "
439
f"`dff_gaming_skill` failed to get access to igdb.com\n"
440
f"payload={payload}\nurl={url}\ntimeout={timeout}\nresponse status code: {token_data.status_code}"
445
class BearerAuth(requests.auth.AuthBase):
446
def __init__(self, token):
449
def __call__(self, r):
450
r.headers["Authorization"] = "Bearer " + self.token
454
def get_igdb_post_kwargs(client_token, client_id):
456
"auth": BearerAuth(client_token),
457
"headers": {"Client-ID": client_id, "Accept": "application/json", "Content-Type": "text/plain"},