Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nerlim #1

Merged
merged 11 commits into from
Feb 15, 2024
Merged
3 changes: 3 additions & 0 deletions tools/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@

### Data loading ###

# scale for experience awarded to the user
XP_PER_LEVEL: int = 10

# Create the user data json if it does not exist
if not os.path.exists(PATH_USER_DATA):
default_user_data = {
Expand Down
188 changes: 166 additions & 22 deletions tools/linconym.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
)
from tools.constants import (
ENGLISH_WORDS_DICTS,
GAMEPLAY_DICT
GAMEPLAY_DICT,
USER_DATA,
XP_PER_LEVEL
)
from tools.basic_tools import (
dichotomy,
Expand All @@ -25,7 +27,7 @@
#################


def is_in_english_words(word: str):
def is_in_english_words(word: str) -> bool:
"""
Indicates whether a word belongs to the english words dictionnary or not.

Expand All @@ -43,7 +45,7 @@ def is_in_english_words(word: str):
return dichotomy(word, ENGLISH_WORDS_DICTS["375k"]) is not None


def count_different_letters(word1: str, word2: str):
def count_different_letters(word1: str, word2: str) -> int:
"""
Count the number of different letters between two words.

Expand Down Expand Up @@ -78,7 +80,7 @@ def count_different_letters(word1: str, word2: str):
return nb_different_letters


def count_common_letters(word1: str, word2: str):
def count_common_letters(word1: str, word2: str) -> int:
res = len(word1) - count_different_letters(word1, word2)
# TEMP
res += (len(word2) - len(word1)) // 2
Expand All @@ -87,7 +89,7 @@ def count_common_letters(word1: str, word2: str):
return res


def is_valid(new_word: str, current_word: str, skip_in_english_words: bool = False):
def is_valid(new_word: str, current_word: str, skip_in_english_words: bool = False) -> bool:
"""
Determine whether a word is valid or not to be submitted.

Expand All @@ -97,6 +99,8 @@ def is_valid(new_word: str, current_word: str, skip_in_english_words: bool = Fal
New word to check.
current_word : str
Current word.
skip_in_english_words : bool = False
If True, a word doesn't need to be in any dictionary to be valid (i.e., in_english_words() is not called here).

Returns
-------
Expand Down Expand Up @@ -147,7 +151,7 @@ def find_all_next_words(current_word: str, english_words):
return next_words


def convert_position_to_wordlist(position: tuple, position_to_word_id, words_found):
def convert_position_to_wordlist(position: str, position_to_word_id, words_found):
wordlist = []
for i in range(1, len(position) + 1):
word = words_found[position_to_word_id[position[:i]]]
Expand Down Expand Up @@ -292,23 +296,68 @@ def fill_gameplay_dict_with_solutions():
class Game():
"""
Class to manage all actions and variables during a game of Linconym.
Its member variables represent a tree of words, the root of which is the start word of the game.
Its member variables also include a cursor "pointing" to one node in particular: new words submitted by the user
may be added as children of the node "pointed to" by this cursor if they are valid successors.
"""

def __init__(self, start_word: str, end_word: str) -> None:
self.start_word = start_word
self.end_word = end_word
self.current_position = (0,)
self.position_to_word_id = {self.current_position: 0}
self.words_found = [start_word]
self.current_word = start_word
def __init__(self, start_word: str, end_word: str, quest_word: str = None, act_name: str = "Act1", lvl_name: str = "1") -> None:
self.start_word: str = start_word
self.end_word: str = end_word
self.quest_word: str = quest_word
self.act_name = act_name
self.lvl_name = lvl_name
self.current_position: str = "0" # Positions are strings of comma-separated integers which indicate a path in the nodes
self.position_to_word_id = {self.current_position: 0} # A dictionary to map a (str) position to the index of its word in the words_found list[str]
self.words_found: list[str] = [start_word]
self.current_word: str = start_word

def get_word(self, position: str) -> str:
"""
Get the word at given position.

Parameters
----------
position : str
Position to use.

Returns
-------
str
Word at given position.
"""

return self.words_found[self.position_to_word_id[position]]

def get_word_path(self, position: str) -> list[str]:
"""
Get a list of the words forming the path to the given position.

Parameters
----------
position : str
Position to use.

def get_nb_next_words(self, position: tuple):
Returns
-------
list[str]
List of words leading to given position.
"""

word_path: list[str] = []
for i in range(len(position)):
previous_pos: str = position[:(i+1)]
LupaDevStudio marked this conversation as resolved.
Show resolved Hide resolved
word_path += [self.get_word(previous_pos)]
return word_path

def get_nb_next_words(self, position: str) -> int:
"""
Get the number of words deriving directly from the given position
Get the number of words deriving directly from the given position.
In tree jargon, get the number of children of the node corresponding to the input position.

Parameters
----------
position : tuple
position : str
Position to use.

Returns
Expand All @@ -328,20 +377,41 @@ def get_nb_next_words(self, position: tuple):

return nb_next_words

def change_position(self, new_position: tuple):
def change_position(self, new_position: str) -> None:
"""
Change the position of the cursor from one word to another
Change the position of the cursor from one word (node) to another.

Parameters
----------
new_position : tuple
new_position : str
New position to set.
"""

self.current_position = new_position
self.current_word = self.words_found[self.position_to_word_id[self.current_position]]
self.current_word = self.get_word(self.current_position)

def is_valid_and_new_in_path(self, new_word: str, skip_dictionary_check: bool = False) -> bool:
LupaDevStudio marked this conversation as resolved.
Show resolved Hide resolved
"""
Checks whether a new word is absent from the path leading to the current word and is a valid successor to the current word.

def submit_word(self, new_word: str):
Parameters
----------
new_word: str
New word to be checked.
skip_dictionary_check: bool = False
If True, new_word can be valid even if it isn't in any dictionary.

Returns
-------
bool
True if new_word isn't in the path leading to the current word and is valid, False otherwise.
"""

word_is_valid: bool = is_valid(new_word, self.current_word, skip_dictionary_check)
word_is_new_in_path: bool = not(new_word in self.get_word_path(self.current_position))
LupaDevStudio marked this conversation as resolved.
Show resolved Hide resolved
return word_is_valid and word_is_new_in_path

def submit_word(self, new_word: str) -> None:
"""
Add a new word to the history if it is valid.

Expand All @@ -350,7 +420,8 @@ def submit_word(self, new_word: str):
new_word : str
New word to verify and add.
"""
if is_valid(new_word):

if self.is_valid_and_new_in_path(new_word):

# Compute the new position
new_word_id = self.get_nb_next_words(self.current_position)
Expand All @@ -369,6 +440,79 @@ def submit_word(self, new_word: str):

else:
print("Word not valid")
LupaDevStudio marked this conversation as resolved.
Show resolved Hide resolved

def award_stars_xp(self, solution_10k_exists: bool, nb_words_10k: int, nb_words_300k: int) -> None:
# Only receive stars and xp if the level was completed
if (self.current_word == self.end_word):
solution_found: list[str] = self.get_word_path(self.current_position)

# Stars
nb_stars: int = 1 # first star is awarded for finishing the level
nb_words_found: int = len(solution_found)
if (solution_10k_exists): # easy levels have a solution in the 10k dictionary
LupaDevStudio marked this conversation as resolved.
Show resolved Hide resolved
if (nb_words_found >= (1.25)*nb_words_10k):
nb_stars += 1 # second star for doing as well as the 10k dictionary + 25%
if (nb_words_found >= nb_words_10k):
nb_stars += 1 # third star for doing as well as the 10k dictionary
else: # harder levels don't have a solution in the 10k dictionary, and their stars are defined differently
if (nb_words_found >= (1.30)*nb_words_300k):
nb_stars += 1 # second star for doing as well as the 300k dictionary + 30%
if (nb_words_found >= (1.10)*nb_words_300k):
nb_stars += 1 # third star for doing as well as the 300k dictionary + 10%

# xp: get a percentage of a certain constant amount depending on proximity to the 300k dictionary solution...
xp_fraction: float = nb_words_300k / nb_words_found
if (xp_fraction > 1):
xp_fraction = 1.0
# ... and get a bonus for passing through the quest word
quest_word_done: bool = ((self.quest_word != None) and (self.quest_word in solution_found))

### save level progress (TEMP: only for classic mode, should probably make subclasses for classic and daily mode)
# check that the current act has save data
if (not(self.act_name in USER_DATA.classic_mode)):
USER_DATA.classic_mode[self.act_name] = {}
# check that current level has save data
if (not(self.lvl_name in USER_DATA.classic_mode[self.act_name])):
USER_DATA.classic_mode[self.act_name][self.lvl_name] = {}
# save stars
STARS_KEY: str = "nb_stars"
if (STARS_KEY in USER_DATA.classic_mode[self.act_name][self.lvl_name]):
if (USER_DATA.classic_mode[self.act_name][self.lvl_name][STARS_KEY] < nb_stars):
USER_DATA.classic_mode[self.act_name][self.lvl_name][STARS_KEY] = nb_stars
else:
USER_DATA.classic_mode[self.act_name][self.lvl_name][STARS_KEY] = nb_stars
# save number of words
NB_WORDS_KEY: str = "best_solution_nb_words"
nb_words_previous_best: int = 0
if (NB_WORDS_KEY in USER_DATA.classic_mode[self.act_name][self.lvl_name]):
LupaDevStudio marked this conversation as resolved.
Show resolved Hide resolved
nb_words_previous_best = USER_DATA.classic_mode[self.act_name][self.lvl_name][NB_WORDS_KEY]
if (nb_words_previous_best < nb_words_found):
USER_DATA.classic_mode[self.act_name][self.lvl_name][NB_WORDS_KEY] = nb_words_found
else:
USER_DATA.classic_mode[self.act_name][self.lvl_name][NB_WORDS_KEY] = nb_words_found
previous_xp_fraction: float = 0.0
if (nb_words_previous_best > 0):
previous_xp_fraction = nb_words_300k / nb_words_previous_best
# save quest word
QUEST_WORD_KEY: str = "quest_word_done"
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il faudra ajouter ce fameux quest word dans le json contenant les données des levels.

award_quest_word_xp: bool = False
if (QUEST_WORD_KEY in USER_DATA.classic_mode[self.act_name][self.lvl_name]):
award_quest_word_xp = not(USER_DATA.classic_mode[self.act_name][self.lvl_name][QUEST_WORD_KEY]) and quest_word_done
USER_DATA.classic_mode[self.act_name][self.lvl_name][QUEST_WORD_KEY] = USER_DATA.classic_mode[self.act_name][self.lvl_name][QUEST_WORD_KEY] or quest_word_done
else:
award_quest_word_xp = quest_word_done
USER_DATA.classic_mode[self.act_name][self.lvl_name][QUEST_WORD_KEY] = quest_word_done

# award newly acquired xp
XP_KEY: str = "experience"
USER_DATA.user_profile[XP_KEY] += int((xp_fraction - previous_xp_fraction) * XP_PER_LEVEL)
if (award_quest_word_xp):
USER_DATA.user_profile[XP_KEY] += XP_PER_LEVEL

# save changes
USER_DATA.save_changes()

return


if __name__ == "__main__":
Expand Down
Loading