diff --git a/.github/linters/my_java_checkstyle.xml b/.github/linters/my_java_checkstyle.xml
new file mode 100644
index 00000000..82def407
--- /dev/null
+++ b/.github/linters/my_java_checkstyle.xml
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml
new file mode 100644
index 00000000..ab9755d1
--- /dev/null
+++ b/.github/workflows/linter.yml
@@ -0,0 +1,41 @@
+---
+name: Lint
+
+on: # yamllint disable-line rule:truthy
+ push: null
+ pull_request: null
+
+jobs:
+ build:
+ name: Lint
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: read
+ packages: read
+ # To report GitHub Actions status checks
+ statuses: write
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ # super-linter needs the full git history to get the
+ # list of files that changed across commits
+ fetch-depth: 0
+
+ - name: Super-linter
+ uses: super-linter/super-linter@v5.7.2 # x-release-please-version
+ env:
+ DEFAULT_BRANCH: main
+ # To report GitHub Actions status checks
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # Exclude forked files from checks
+ FILTER_REGEX_INCLUDE: .*src/.*
+ # Ignore .gitignore files
+ IGNORE_GITIGNORED_FILES: true
+
+ # Custom Java Checkstyle configuration
+ JAVA_FILE_NAME: my_java_checkstyle.xml
+ # Disable google-java-format
+ VALIDATE_GOOGLE_JAVA_FORMAT: false
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000..3e006c60
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,10 @@
+{
+ "overrides": [
+ {
+ "files": "*.md",
+ "options": {
+ "tabWidth": 1
+ }
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..b315c3eb
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "dotnet.defaultSolution": "RPG_Game/RPG_Game.sln",
+
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+
+ "[python]": {
+ "editor.defaultFormatter": "ms-python.black-formatter",
+ "editor.formatOnSave": true
+ }
+}
diff --git a/PYTHONREQUIREMENTS.md b/PYTHONREQUIREMENTS.md
new file mode 100644
index 00000000..ef8bc12a
--- /dev/null
+++ b/PYTHONREQUIREMENTS.md
@@ -0,0 +1,16 @@
+## To install all Python dependencies for any specific project:
+
+1. cd into the project folder.
+2. Ensure that the folder contains a `requirements.txt` file. This file contains all the necessary dependencies for this project to function.
+3. If this file is not found, look at `Creating a requirements.txt file for your project`. Else, go to step 4.
+4. Run the command `pip3 install -r requirements.txt` to generate the dependency file.
+
+
+
+## Creating a requirements.txt file for your project:
+
+1. We will use two libraries to help us generate the `requirements.txt` file - pipreqs and pip-tools.
+2. If not already installed, run `pip3 install pipreqs` and `pip3 install pip-tools`.
+3. cd into the project folder.
+4. Run the command `pipreqs --savepath=requirements.in && pip-compile`.
+5. Both `requirements.in` and `requirements.txt` files should be created.
\ No newline at end of file
diff --git a/README.md b/README.md
index 9f3119b7..28dd1082 100644
--- a/README.md
+++ b/README.md
@@ -155,7 +155,7 @@ To get started, simply fork this repo. Please refer to [CONTRIBUTING.md](CONTRIB
## C#:
-- [Learn C# By Building a Simple RPG Game](http://scottlilly.com/learn-c-by-building-a-simple-rpg-index/)
+- [Learn C# By Building a Simple RPG Game](http://scottlilly.com/learn-c-by-building-a-simple-rpg-index/) :white_check_mark:
- [Create a Rogue-like game in C#](https://roguesharp.wordpress.com/)
- [Create a Blank App with C# and Xamarin (work in progress)](https://www.intertech.com/Blog/xamarin-tutorial-part-1-create-a-blank-app/)
- [Build iOS Photo Library App with Xamarin and Visual Studio](https://www.raywenderlich.com/134049/building-ios-apps-with-xamarin-and-visual-studio)
@@ -371,7 +371,7 @@ To get started, simply fork this repo. Please refer to [CONTRIBUTING.md](CONTRIB
- [Mining Twitter Data with Python](https://marcobonzanini.com/2015/03/02/mining-twitter-data-with-python-part-1/)
- [Scrape a Website with Scrapy and MongoDB](https://realpython.com/blog/python/web-scraping-with-scrapy-and-mongodb/)
- [How To Scrape With Python and Selenium WebDriver](http://www.byperth.com/2018/04/25/guide-web-scraping-101-what-you-need-to-know-and-how-to-scrape-with-python-selenium-webdriver/)
-- [Which Movie Should I Watch using BeautifulSoup](https://medium.com/@nishantsahoo.in/which-movie-should-i-watch-5c83a3c0f5b1)
+- [Which Movie Should I Watch using BeautifulSoup](https://medium.com/@nishantsahoo.in/which-movie-should-i-watch-5c83a3c0f5b1) :white_check_mark:
### Web Applications:
@@ -401,7 +401,7 @@ To get started, simply fork this repo. Please refer to [CONTRIBUTING.md](CONTRIB
- [How to Make a Reddit Bot - YouTube](https://www.youtube.com/watch?v=krTUf7BpTc0) (video)
- [Build a Facebook Messenger Bot](https://blog.hartleybrody.com/fb-messenger-bot/)
- [Making a Reddit + Facebook Messenger Bot](https://pythontips.com/2017/04/13/making-a-reddit-facebook-messenger-bot/)
-- How To Create a Telegram Bot Using Python
+- How To Create a Telegram Bot Using Python :white_check_mark:
- [Part 1](https://khashtamov.com/en/how-to-create-a-telegram-bot-using-python/)
- [Part 2](https://khashtamov.com/en/how-to-deploy-telegram-bot-django/)
- [Create a Twitter Bot In Python](https://medium.freecodecamp.org/creating-a-twitter-bot-in-python-with-tweepy-ac524157a607)
diff --git a/src/Crafting_Interpreters/.gitignore b/src/Crafting_Interpreters/.gitignore
new file mode 100644
index 00000000..38105ec2
--- /dev/null
+++ b/src/Crafting_Interpreters/.gitignore
@@ -0,0 +1,24 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+replay_pid*
\ No newline at end of file
diff --git a/src/Crafting_Interpreters/Makefile b/src/Crafting_Interpreters/Makefile
new file mode 100644
index 00000000..e4f3bc93
--- /dev/null
+++ b/src/Crafting_Interpreters/Makefile
@@ -0,0 +1,25 @@
+JFLAGS = -g
+JC = javac
+
+.SUFFIXES: .java .class
+
+.java.class:
+ $(JC) $(JFLAGS) $*.java
+
+# This uses the line continuation character (\) for readability.
+# Compile .java files in dependency order.
+CLASSES = \
+ lox/TokenType.java \
+ lox/Token.java \
+ lox/Scanner.java \
+ lox/Lox.java \
+
+default: classes
+
+classes: $(CLASSES:.java=.class)
+
+clean:
+ $(RM) lox/*.class
+
+run:
+ java lox/Lox
\ No newline at end of file
diff --git a/src/Crafting_Interpreters/README.md b/src/Crafting_Interpreters/README.md
new file mode 100644
index 00000000..5f3a0d40
--- /dev/null
+++ b/src/Crafting_Interpreters/README.md
@@ -0,0 +1,15 @@
+# Build an Interpreter
+
+Date Started: 07/01/2024
+
+Date Completed:
+
+This is a mini-project to learn about how compilers work by attempting to implement one in Java programming language. Here is the [link](https://www.craftinginterpreters.com/contents.html) to the guide that I followed. Stopped at 5.1.1.
+
+Dependencies:
+
+-
+
+References:
+
+-
diff --git a/src/Crafting_Interpreters/lox/Lox.java b/src/Crafting_Interpreters/lox/Lox.java
new file mode 100644
index 00000000..b19dc0f4
--- /dev/null
+++ b/src/Crafting_Interpreters/lox/Lox.java
@@ -0,0 +1,98 @@
+package lox;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+
+public final class Lox {
+ /**
+ * A flag to indicate if an error occurred
+ * during the execution of the program.
+ */
+ private static boolean hadError = false;
+
+ /**
+ * The exit code to indicate a usage error.
+ */
+ private static final int EX_USAGE = 64;
+
+ /**
+ * The exit code to indicate a data error.
+ */
+ private static final int EX_DATAERR = 65;
+
+
+ /**
+ * Private constructor to prevent instantiation of this class.
+ */
+ private Lox() { }
+
+ /**
+ * The entry point of the program.
+ *
+ * @param args The command line arguments.
+ * @throws IOException If an I/O error occurs.
+ */
+ public static void main(String[] args) throws IOException {
+ if (args.length > 1) {
+ System.out.println("Usage: jlox [script]");
+ System.exit(EX_USAGE);
+ } else if (args.length == 1) {
+ runFile(args[0]);
+ } else {
+ runPrompt();
+ }
+ }
+
+ private static void runFile(String path) throws IOException {
+ byte[] bytes = Files.readAllBytes(Paths.get(path));
+ run(new String(bytes, Charset.defaultCharset()));
+
+ // Indicate an error in the exit code.
+ if (hadError) System.exit(EX_DATAERR);
+ }
+
+ private static void runPrompt() throws IOException {
+ InputStreamReader input = new InputStreamReader(System.in);
+ BufferedReader reader = new BufferedReader(input);
+
+ for (;;) {
+ System.out.print("> ");
+ String line = reader.readLine();
+ if (line == null) break;
+ run(line);
+ hadError = false;
+ }
+ }
+
+ private static void run(String source) {
+ Scanner scanner = new Scanner(source);
+ List tokens = scanner.scanTokens();
+
+ // For now, just print the tokens.
+ for (Token token : tokens) {
+ System.out.println(token);
+ }
+ }
+
+ /**
+ * Report an error.
+ *
+ * @param line The line number where the error occurred.
+ * @param message The error message.
+ */
+ public static void error(int line, String message) {
+ report(line, "", message);
+ }
+
+ private static void report(int line, String where, String message) {
+ String msg = String.format("[line %d] Error%s: %s",
+ line, where, message);
+ System.err.println(msg);
+ hadError = true;
+ }
+}
diff --git a/src/Crafting_Interpreters/lox/Scanner.java b/src/Crafting_Interpreters/lox/Scanner.java
new file mode 100644
index 00000000..ccdc6e88
--- /dev/null
+++ b/src/Crafting_Interpreters/lox/Scanner.java
@@ -0,0 +1,236 @@
+package lox;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static lox.TokenType.*;
+
+class Scanner {
+
+ /**
+ * A map of KEYWORDS to their respective token types.
+ */
+ private static final Map KEYWORDS;
+
+ static {
+ KEYWORDS = new HashMap<>();
+ KEYWORDS.put("and", AND);
+ KEYWORDS.put("class", CLASS);
+ KEYWORDS.put("else", ELSE);
+ KEYWORDS.put("false", FALSE);
+ KEYWORDS.put("for", FOR);
+ KEYWORDS.put("fun", FUN);
+ KEYWORDS.put("if", IF);
+ KEYWORDS.put("nil", NIL);
+ KEYWORDS.put("or", OR);
+ KEYWORDS.put("print", PRINT);
+ KEYWORDS.put("return", RETURN);
+ KEYWORDS.put("super", SUPER);
+ KEYWORDS.put("this", THIS);
+ KEYWORDS.put("true", TRUE);
+ KEYWORDS.put("var", VAR);
+ KEYWORDS.put("while", WHILE);
+ }
+
+ /**
+ * The source code to scan.
+ */
+ private final String source;
+
+ /**
+ * The list of tokens found in the source code.
+ */
+ private final List tokens = new ArrayList<>();
+
+ /**
+ * The index of the first character in the current lexeme.
+ */
+ private int start = 0;
+
+ /**
+ * The index of the current character being considered.
+ */
+ private int current = 0;
+
+ /**
+ * The current line number in the source code.
+ */
+ private int line = 1;
+
+
+ /**
+ * Create a new scanner for the specified source code.
+ *
+ * @param source The source code to scan.
+ */
+ Scanner(String source) {
+ this.source = source;
+ }
+
+ /**
+ * Scan the source code and return the list of tokens found.
+ *
+ * @return The list of tokens found in the source code.
+ */
+ List scanTokens() {
+ while (!isAtEnd()) {
+ // We are at the beginning of the next lexeme.
+ start = current;
+ scanToken();
+ }
+
+ tokens.add(new Token(EOF, "", null, line));
+ return tokens;
+ }
+
+ private void scanToken() {
+ char c = advance();
+ switch (c) {
+ case '(': addToken(LEFT_PAREN); break;
+ case ')': addToken(RIGHT_PAREN); break;
+ case '{': addToken(LEFT_BRACE); break;
+ case '}': addToken(RIGHT_BRACE); break;
+ case ',': addToken(COMMA); break;
+ case '.': addToken(DOT); break;
+ case '-': addToken(MINUS); break;
+ case '+': addToken(PLUS); break;
+ case ';': addToken(SEMICOLON); break;
+ case '*': addToken(STAR); break;
+ case '!':
+ addToken(match('=') ? BANG_EQUAL : BANG);
+ break;
+ case '=':
+ addToken(match('=') ? EQUAL_EQUAL : EQUAL);
+ break;
+ case '<':
+ addToken(match('=') ? LESS_EQUAL : LESS);
+ break;
+ case '>':
+ addToken(match('=') ? GREATER_EQUAL : GREATER);
+ break;
+ case '/':
+ if (match('/')) {
+ // A comment goes until the end of the line.
+ while (peek() != '\n' && !isAtEnd()) advance();
+ } else {
+ addToken(SLASH);
+ }
+ break;
+ case ' ':
+ case '\r':
+ case '\t':
+ // Ignore whitespace.
+ break;
+
+ case '\n':
+ line++;
+ break;
+
+ case '"': string(); break;
+
+ default:
+ if (isDigit(c)) {
+ number();
+ } else if (isAlpha(c)) {
+ identifier();
+ } else {
+ Lox.error(line, "Unexpected character.");
+ }
+ break;
+ }
+ }
+
+ private void string() {
+ while (peek() != '"' && !isAtEnd()) {
+ if (peek() == '\n') line++;
+ advance();
+ }
+
+ // Unterminated string.
+ if (isAtEnd()) {
+ Lox.error(line, "Unterminated string.");
+ return;
+ }
+
+ // The closing ".
+ advance();
+
+ // Trim the surrounding quotes.
+ String value = source.substring(start + 1, current - 1);
+ addToken(STRING, value);
+ }
+
+ private void number() {
+ while (isDigit(peek())) advance();
+
+ // Look for a fractional part.
+ if (peek() == '.' && isDigit(peekNext())) {
+ // Consume the "."
+ advance();
+
+ while (isDigit(peek())) advance();
+ }
+
+ addToken(NUMBER, Double.parseDouble(source.substring(start, current)));
+ }
+
+ private void identifier() {
+ while (isAlphaNumeric(peek())) advance();
+
+ String text = source.substring(start, current);
+ TokenType type = KEYWORDS.get(text);
+ if (type == null) type = IDENTIFIER;
+ addToken(type);
+ }
+
+ private boolean match(char expected) {
+ if (isAtEnd()) return false;
+ if (source.charAt(current) != expected) return false;
+
+ current++;
+ return true;
+ }
+
+ private char peek() {
+ if (isAtEnd()) return '\0';
+ return source.charAt(current);
+ }
+
+ private char peekNext() {
+ if (current + 1 >= source.length()) return '\0';
+ return source.charAt(current + 1);
+ }
+
+ private boolean isDigit(char c) {
+ return c >= '0' && c <= '9';
+ }
+
+ private boolean isAlpha(char c) {
+ return (c >= 'a' && c <= 'z')
+ || (c >= 'A' && c <= 'Z')
+ || c == '_';
+ }
+
+ private boolean isAlphaNumeric(char c) {
+ return isAlpha(c) || isDigit(c);
+ }
+
+ private boolean isAtEnd() {
+ return current >= source.length();
+ }
+
+ private char advance() {
+ return source.charAt(current++);
+ }
+
+ private void addToken(TokenType type) {
+ addToken(type, null);
+ }
+
+ private void addToken(TokenType type, Object literal) {
+ String text = source.substring(start, current);
+ tokens.add(new Token(type, text, literal, line));
+ }
+}
diff --git a/src/Crafting_Interpreters/lox/Token.java b/src/Crafting_Interpreters/lox/Token.java
new file mode 100644
index 00000000..ed419500
--- /dev/null
+++ b/src/Crafting_Interpreters/lox/Token.java
@@ -0,0 +1,49 @@
+package lox;
+
+class Token {
+ /**
+ * The type of the token.
+ */
+ private final TokenType type;
+
+ /**
+ * The lexeme of the token.
+ */
+ private final String lexeme;
+
+ /**
+ * The literal value of the token.
+ */
+ private final Object literal;
+
+ /**
+ * The line number in the source code where the token appears.
+ */
+ private final int line;
+
+
+ /**
+ * Create a new token.
+ *
+ * @param type The type of the token.
+ * @param lexeme The lexeme of the token.
+ * @param literal The literal value of the token.
+ * @param line The line number in the source code where the token appears.
+ */
+ Token(TokenType type, String lexeme, Object literal, int line) {
+ this.type = type;
+ this.lexeme = lexeme;
+ this.literal = literal;
+ this.line = line;
+ }
+
+ /**
+ * Return a string representation of the token.
+ *
+ * @return A string representation of the token.
+ */
+ @Override
+ public String toString() {
+ return type + " " + lexeme + " " + literal;
+ }
+}
diff --git a/src/Crafting_Interpreters/lox/TokenType.java b/src/Crafting_Interpreters/lox/TokenType.java
new file mode 100644
index 00000000..92dd3fcc
--- /dev/null
+++ b/src/Crafting_Interpreters/lox/TokenType.java
@@ -0,0 +1,23 @@
+package lox;
+
+@SuppressWarnings("checkstyle:JavadocVariable")
+enum TokenType {
+ // Single-character tokens.
+ LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE,
+ COMMA, DOT, MINUS, PLUS, SEMICOLON, SLASH, STAR,
+
+ // One or two character tokens.
+ BANG, BANG_EQUAL,
+ EQUAL, EQUAL_EQUAL,
+ GREATER, GREATER_EQUAL,
+ LESS, LESS_EQUAL,
+
+ // Literals.
+ IDENTIFIER, STRING, NUMBER,
+
+ // Keywords.
+ AND, CLASS, ELSE, FALSE, FUN, FOR, IF, NIL, OR,
+ PRINT, RETURN, SUPER, THIS, TRUE, VAR, WHILE,
+
+ EOF
+}
diff --git a/src/IMDB_Movie_Scraper/.gitignore b/src/IMDB_Movie_Scraper/.gitignore
new file mode 100644
index 00000000..6769e21d
--- /dev/null
+++ b/src/IMDB_Movie_Scraper/.gitignore
@@ -0,0 +1,160 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
\ No newline at end of file
diff --git a/src/IMDB_Movie_Scraper/README.md b/src/IMDB_Movie_Scraper/README.md
new file mode 100644
index 00000000..528e1367
--- /dev/null
+++ b/src/IMDB_Movie_Scraper/README.md
@@ -0,0 +1,17 @@
+# Which Movie Should I Watch using BeautifulSoup
+
+Date Started: 22/05/2023
+
+Date Completed: 28/05/2023
+
+This is a mini-project to learn about web scraping using BeautifulSoup. Here is the [link](https://medium.com/@nishantsahoo/which-movie-should-i-watch-5c83a3c0f5b1) to the guide that I followed.
+
+Dependencies:
+
+- lxml 4.9.2
+- beautifulsoup4 4.12.2
+- Python 3.9.13
+
+References:
+
+- [BeautifulSoup4 API](https://www.crummy.com/software/BeautifulSoup/bs4/doc/)
diff --git a/src/IMDB_Movie_Scraper/requirements.in b/src/IMDB_Movie_Scraper/requirements.in
new file mode 100644
index 00000000..68f64703
--- /dev/null
+++ b/src/IMDB_Movie_Scraper/requirements.in
@@ -0,0 +1 @@
+beautifulsoup4==4.12.2
diff --git a/src/IMDB_Movie_Scraper/requirements.txt b/src/IMDB_Movie_Scraper/requirements.txt
new file mode 100644
index 00000000..93f8641c
--- /dev/null
+++ b/src/IMDB_Movie_Scraper/requirements.txt
@@ -0,0 +1,10 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# pip-compile
+#
+beautifulsoup4==4.12.2
+ # via -r requirements.in
+soupsieve==2.4.1
+ # via beautifulsoup4
diff --git a/src/IMDB_Movie_Scraper/scraper.py b/src/IMDB_Movie_Scraper/scraper.py
new file mode 100644
index 00000000..2c4d708b
--- /dev/null
+++ b/src/IMDB_Movie_Scraper/scraper.py
@@ -0,0 +1,48 @@
+from urllib.request import urlopen
+
+from bs4 import BeautifulSoup
+
+BASE_URL = "https://www.imdb.com"
+DEFAULT_YEAR = 2017
+
+# Getting user input
+while True:
+ year = input("Enter a year to search for: ")
+
+ if year == "":
+ year = str(DEFAULT_YEAR)
+ print("Setting default year to {}...".format(year))
+ break
+
+ if len(year) == 4 and year.isdigit():
+ break
+
+ print("Year must be 4 digits long.")
+
+
+# Sending a GET request to specified URL and getting the response as a HTTPResponse object
+url = BASE_URL + "/search/title/?release_date=" + year
+http_response = urlopen(url)
+markup = http_response.read()
+
+# Parser
+soup = BeautifulSoup(markup, "lxml")
+movie_list = soup.find_all("div", class_="lister-item mode-advanced")
+
+
+def get_movie_info(movie):
+ return {
+ "title": movie.find("h3", class_="lister-item-header").a.text,
+ "rating": movie.find(
+ "div", class_="inline-block ratings-imdb-rating"
+ ).text.strip()
+ if movie.find("div", class_="inline-block ratings-imdb-rating")
+ else "No rating",
+ "path": movie.find("h3", class_="lister-item-header").a["href"],
+ }
+
+
+movie_list = list(map(get_movie_info, movie_list))
+
+for idx, movie in enumerate(movie_list, 1):
+ print(f'{idx}. {movie["title"]}, {movie["rating"]}\n{BASE_URL + movie["path"]}')
diff --git a/src/MH_Script/.env.example b/src/MH_Script/.env.example
new file mode 100644
index 00000000..00853cb9
--- /dev/null
+++ b/src/MH_Script/.env.example
@@ -0,0 +1,2 @@
+MH_PASSWORD=my_secure_password
+MH_USERNAME=my_username
diff --git a/src/MH_Script/.gitignore b/src/MH_Script/.gitignore
new file mode 100644
index 00000000..5e45bbd6
--- /dev/null
+++ b/src/MH_Script/.gitignore
@@ -0,0 +1,177 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+
+# Selenium env
+selenium/
\ No newline at end of file
diff --git a/src/MH_Script/README.md b/src/MH_Script/README.md
new file mode 100644
index 00000000..47b59eea
--- /dev/null
+++ b/src/MH_Script/README.md
@@ -0,0 +1,49 @@
+# MH Script
+
+Date Started: 08/03/2025
+
+Date Completed: 10/03/2025
+
+This is a project that attempts to create a Mousehunt script that automatically gifts 25 gifts and 20 raffle tickets to the top most active friends.
+
+I am running this script in _Windows command-line_ on _Google Chrome_.
+
+**NOTE: Currently linked to Windows Task Scheduler with properties:**
+
+- Run whether user is logged on or not
+- Run task as soon as possible after a scheduled start is missed
+- Trigger at _9:00am_ daily
+- Starts the `run_mh_script.bat` program
+
+Dependencies:
+
+- selenium 4.29.0
+- webdriver-manager 4.0.2
+- selenium-stealth 1.0.6
+
+References:
+
+- [https://stackoverflow.com/questions/68289474/selenium-headless-how-to-bypass-cloudflare-detection-using-selenium](https://stackoverflow.com/questions/68289474/selenium-headless-how-to-bypass-cloudflare-detection-using-selenium)
+
+## Setup Instructions
+
+From root directory, perform these commands:
+
+```bash
+cd src/MH_Script
+
+python -m venv venv # OR
+python3 -m venv venv
+
+venv\Scripts\Activate # Windows
+source venv/bin/activate # Mac/Linux/bash
+
+pip install -r requirements.txt
+```
+
+Then, add `.env` file according to `.env.example` and run
+
+```bash
+python main.py # OR
+python3 main.py
+```
diff --git a/src/MH_Script/helper.py b/src/MH_Script/helper.py
new file mode 100644
index 00000000..da5c4744
--- /dev/null
+++ b/src/MH_Script/helper.py
@@ -0,0 +1,47 @@
+from logger import log, log_success
+from selenium import webdriver
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def wait_for(driver: webdriver.Chrome, condition, locator=None):
+ """Generic wait function to handle different conditions."""
+ return WebDriverWait(driver, 10).until(condition(locator) if locator else condition)
+
+
+def wait_and_click(driver: webdriver.Chrome, locator):
+ """Waits for an element (or WebElement) to be clickable and clicks it."""
+ button = wait_for(driver, EC.element_to_be_clickable, locator)
+ button.click()
+
+
+def wait_and_find_elements(driver: webdriver.Chrome, locator):
+ """
+ Waits for all elements matching the given locator to be present in the DOM.
+ """
+ return wait_for(driver, EC.presence_of_all_elements_located, locator)
+
+
+def wait_and_find_element(driver: webdriver.Chrome, locator):
+ """
+ Waits for the first element matching the given locator to be present in the DOM.
+ """
+ return wait_for(driver, EC.presence_of_element_located, locator)
+
+
+def click_all_gift_and_ticket(driver: webdriver.Chrome):
+ """Clicks all enabled gift and ticket buttons."""
+ buttons = {
+ "gift": (By.CLASS_NAME, "sendGift"),
+ "ticket": (By.CLASS_NAME, "sendTicket"),
+ }
+
+ log("Clicking all gift and ticket buttons...")
+
+ for key, locator in buttons.items():
+ for button in wait_and_find_elements(driver, locator):
+ if "disabled" not in button.get_attribute("class"):
+ wait_and_click(driver, button)
+
+ log_success("All gift and ticket buttons clicked.")
diff --git a/src/MH_Script/logger.py b/src/MH_Script/logger.py
new file mode 100644
index 00000000..e1d7b0eb
--- /dev/null
+++ b/src/MH_Script/logger.py
@@ -0,0 +1,81 @@
+import datetime
+import logging
+import os
+
+
+class Logger:
+ _instance = None
+ _initialized = False
+
+ @classmethod
+ def get_instance(cls, log_dir="log"):
+ """
+ Get or create the singleton logger instance.
+ """
+ if cls._instance is None:
+ cls._instance = cls(log_dir)
+ return cls._instance
+
+ def __init__(self, log_dir="log"):
+ """
+ Initialize the logger only once.
+ """
+ # Only initialize once, even if constructor is called multiple times
+ if not Logger._initialized:
+ # Create log directory if it doesn't exist
+ if not os.path.exists(log_dir):
+ os.makedirs(log_dir)
+
+ # Generate a unique filename based on current timestamp
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+ self.log_file = os.path.join(log_dir, f"app_{timestamp}.log")
+
+ # Configure the logger
+ self.logger = logging.getLogger("my_logger")
+ self.logger.setLevel(logging.INFO)
+ self.logger.handlers = [] # Clear any existing handlers
+
+ # Create file handler and set format
+ file_handler = logging.FileHandler(self.log_file)
+ formatter = logging.Formatter("%(message)s")
+ file_handler.setFormatter(formatter)
+
+ # Add handler to logger
+ self.logger.addHandler(file_handler)
+
+ # Log initial message
+ self.log_success(f"Logger initialized with log file: {self.log_file}")
+
+ # Mark as initialized
+ Logger._initialized = True
+
+ def log(self, status, message):
+ """Log a message with timestamp and status."""
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ log_entry = f"{timestamp} - {status} - {message}"
+ self.logger.info(log_entry)
+
+ def log_success(self, message):
+ """Log a success message."""
+ self.log("SUCCESS", message)
+
+ def log_error(self, message):
+ """Log an error message."""
+ self.log("ERROR", message)
+
+ def log_info(self, message):
+ """Log an info message."""
+ self.log("INFO", message)
+
+
+# Convenience static methods for easy access
+def log_success(message):
+ Logger.get_instance().log_success(message)
+
+
+def log_error(message):
+ Logger.get_instance().log_error(message)
+
+
+def log(func):
+ return Logger.get_instance().log_info(func)
diff --git a/src/MH_Script/main.py b/src/MH_Script/main.py
new file mode 100644
index 00000000..422a5a20
--- /dev/null
+++ b/src/MH_Script/main.py
@@ -0,0 +1,85 @@
+from helper import (
+ click_all_gift_and_ticket,
+ wait_and_click,
+ wait_and_find_element,
+ wait_and_find_elements,
+)
+from logger import log, log_error, log_success
+from selenium import webdriver
+from selenium.webdriver.chrome.options import Options
+from selenium.webdriver.chrome.service import Service as ChromeService
+from selenium.webdriver.common.by import By
+from selenium.webdriver.common.keys import Keys
+from selenium_stealth import stealth
+from util import get_password, get_username, init_options
+from webdriver_manager.chrome import ChromeDriverManager
+
+# Set up Chrome options
+options = Options()
+init_options(options)
+
+# Initialize WebDriver with automatic driver management
+driver = webdriver.Chrome(
+ service=ChromeService(ChromeDriverManager().install()), options=options
+)
+
+stealth(
+ driver,
+ languages=["en-US", "en"],
+ vendor="Google Inc.",
+ platform="Win32",
+ webgl_vendor="Intel Inc.",
+ renderer="Intel Iris OpenGL Engine",
+ fix_hairline=True,
+)
+
+log("Starting script...")
+
+try:
+ # Open a website
+ driver.get("https://www.mousehuntgame.com/")
+
+ if wait_and_find_elements(driver, (By.CLASS_NAME, "loginFormContainer")):
+ log("Starting login process...")
+
+ # If in first login menu, click on "START NEW GAME"
+ login_text = "START NEW GAME"
+ xpath_expr = (
+ "//*[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', "
+ "'abcdefghijklmnopqrstuvwxyz'), '{}')]".format(login_text.lower())
+ )
+ if wait_and_find_elements(driver, (By.XPATH, xpath_expr)):
+ wait_and_click(driver, (By.CLASS_NAME, "loginPage-signInButton"))
+
+ username_field = wait_and_find_element(driver, (By.NAME, "username"))
+ password_field = wait_and_find_element(driver, (By.NAME, "password"))
+
+ username_field.send_keys(get_username())
+ password_field.send_keys(get_password())
+
+ # Submit the form (Either by pressing Enter or clicking a login button)
+ password_field.send_keys(Keys.RETURN) # Press Enter
+
+ log_success("Login process completed.")
+
+ log("Starting main script...")
+
+ wait_and_click(driver, (By.CLASS_NAME, "friends"))
+
+ click_all_gift_and_ticket(driver)
+
+ wait_and_click(driver, (By.CLASS_NAME, "pagerView-nextPageLink"))
+
+ click_all_gift_and_ticket(driver)
+
+ log_success("Main script completed.")
+
+ log_success("Script completed successfully.")
+
+except Exception as e:
+ print(f"Error occurred: {e}")
+ log_error(f"Error occurred: {e}")
+
+finally:
+ # Close the browser window after actions are complete
+ driver.quit()
diff --git a/src/MH_Script/requirements.txt b/src/MH_Script/requirements.txt
new file mode 100644
index 00000000..3a1ec57d
--- /dev/null
+++ b/src/MH_Script/requirements.txt
@@ -0,0 +1,24 @@
+attrs==25.1.0
+certifi==2025.1.31
+cffi==1.17.1
+charset-normalizer==3.4.1
+exceptiongroup==1.2.2
+h11==0.14.0
+idna==3.10
+outcome==1.3.0.post0
+packaging==24.2
+pycparser==2.22
+PySocks==1.7.1
+python-dotenv==1.0.1
+requests==2.32.3
+selenium==4.29.0
+selenium-stealth==1.0.6
+sniffio==1.3.1
+sortedcontainers==2.4.0
+trio==0.29.0
+trio-websocket==0.12.2
+typing_extensions==4.12.2
+urllib3==2.3.0
+webdriver-manager==4.0.2
+websocket-client==1.8.0
+wsproto==1.2.0
diff --git a/src/MH_Script/run_mh_script.bat b/src/MH_Script/run_mh_script.bat
new file mode 100644
index 00000000..8b9029d7
--- /dev/null
+++ b/src/MH_Script/run_mh_script.bat
@@ -0,0 +1,14 @@
+@echo off
+REM TO BE RUN BY TASKSCHEDULER
+cd /d "%~dp0"
+
+python -m venv venv
+call venv\Scripts\activate.bat
+pip install -r requirements.txt
+python main.py
+call venv\Scripts\deactivate.bat
+
+REM OPEN LOG FOLDER IN EXPLORER
+explorer.exe /e,"%~dp0log"
+
+exit
\ No newline at end of file
diff --git a/src/MH_Script/util.py b/src/MH_Script/util.py
new file mode 100644
index 00000000..e95e1943
--- /dev/null
+++ b/src/MH_Script/util.py
@@ -0,0 +1,72 @@
+import os
+import shutil
+import subprocess
+
+from dotenv import load_dotenv
+from logger import log, log_success
+from selenium.webdriver.chrome.options import Options
+
+load_dotenv()
+
+
+# Running on WSL
+def get_windows_username():
+ return (
+ subprocess.check_output('cmd.exe /c "echo %USERNAME%"', shell=True)
+ .decode()
+ .strip()
+ )
+
+
+def get_windows_cwd():
+ wsl_cwd = subprocess.check_output(["pwd"]).decode().strip()
+ win_cwd = subprocess.check_output(["wslpath", "-w", wsl_cwd]).decode().strip()
+ return win_cwd
+
+
+def get_username():
+ return os.getenv("MH_USERNAME")
+
+
+def get_password():
+ return os.getenv("MH_PASSWORD")
+
+
+def init_options(options: Options):
+ log("Initializing Chrome options...")
+
+ # Set the path to your Chrome user data directory
+ dir_path = os.path.join(os.getcwd(), "selenium")
+ profile_name = "Default"
+
+ # Delete the existing directory if it exists
+ if os.path.exists(dir_path):
+ shutil.rmtree(dir_path)
+
+ options.add_argument(f"user-data-dir={dir_path}") # Path to user data
+ options.add_argument(
+ f"--profile-directory={profile_name}"
+ ) # Specific profile folder
+
+ # Add other options as needed
+ options.add_argument("--headless=new") # Run Chrome in headless mode (without GUI)
+ options.add_argument("--disable-gpu") # Recommended for some environments
+ options.add_argument(
+ "--window-size=1920,1080"
+ ) # Set a virtual screen size for consistency
+ options.add_argument("--start-maximized") # Start Chrome maximized
+ options.add_argument(
+ "disable-infobars"
+ ) # Disable the "Chrome is being controlled by automated software" notification
+ options.add_argument("--disable-extensions") # Disable all extensions
+ options.add_argument(
+ "--no-sandbox"
+ ) # Run Chrome without sandboxing (for some environments)
+ options.add_argument(
+ "--disable-dev-shm-usage"
+ ) # Disable shared memory usage (for some environments)
+
+ # Specify the location of the Chrome binary (if needed)
+ options.binary_location = r"C:\Program Files\Google\Chrome\Application\chrome.exe" # Path to Chrome on Windows
+
+ log_success("Chrome options initialized.")
diff --git a/src/RPG_Game/.gitignore b/src/RPG_Game/.gitignore
new file mode 100644
index 00000000..8dd4607a
--- /dev/null
+++ b/src/RPG_Game/.gitignore
@@ -0,0 +1,398 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
\ No newline at end of file
diff --git a/src/RPG_Game/README.md b/src/RPG_Game/README.md
new file mode 100644
index 00000000..b5175286
--- /dev/null
+++ b/src/RPG_Game/README.md
@@ -0,0 +1,21 @@
+# Learn C# By Building a Simple RPG Game
+
+Date Started: 08/08/2023
+
+Date Completed: 07/01/2024
+
+This is a mini-project to learn about C# and also to create my own RPG Game. Here is the [link](https://soscsrpg.com/) to the guide that I followed.
+
+To launch the app:
+
+- cd to the `/RPG_Game_UI` directory
+- run the command `dotnet build` to build the application
+- then `dotnet run` to launch
+
+Dependencies:
+
+- dotnet 7.0.306
+
+References:
+
+
diff --git a/src/RPG_Game/RPG_Game.sln b/src/RPG_Game/RPG_Game.sln
new file mode 100644
index 00000000..056c9163
--- /dev/null
+++ b/src/RPG_Game/RPG_Game.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.002.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RPG_Game_Engine", "RPG_Game_Engine\RPG_Game_Engine.csproj", "{462D9413-3AB3-46A9-A7FF-CCFD5B124E0E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RPG_Game_UI", "RPG_Game_UI\RPG_Game_UI.csproj", "{D03C6C5C-EB70-440A-84FE-7F5A69AA5097}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {462D9413-3AB3-46A9-A7FF-CCFD5B124E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {462D9413-3AB3-46A9-A7FF-CCFD5B124E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {462D9413-3AB3-46A9-A7FF-CCFD5B124E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {462D9413-3AB3-46A9-A7FF-CCFD5B124E0E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D03C6C5C-EB70-440A-84FE-7F5A69AA5097}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D03C6C5C-EB70-440A-84FE-7F5A69AA5097}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D03C6C5C-EB70-440A-84FE-7F5A69AA5097}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D03C6C5C-EB70-440A-84FE-7F5A69AA5097}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {A464F9CF-ECB4-439D-AB13-55C112AEFB61}
+ EndGlobalSection
+EndGlobal
diff --git a/src/RPG_Game/RPG_Game_Engine/BaseNotification.cs b/src/RPG_Game/RPG_Game_Engine/BaseNotification.cs
new file mode 100644
index 00000000..99c63037
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/BaseNotification.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.ComponentModel;
+
+namespace Engine
+{
+ public class BaseNotification : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler? PropertyChanged;
+ protected void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Controllers/GameSession.cs b/src/RPG_Game/RPG_Game_Engine/Controllers/GameSession.cs
new file mode 100644
index 00000000..9572650f
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Controllers/GameSession.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Engine.Models;
+using Engine.Models.Monsters;
+using Engine.Factories;
+using Engine.EventArgs;
+using Engine.Models.Items.Misc;
+
+namespace Engine.Controllers
+{
+ public class GameSession : BaseNotification
+ {
+ #region Properties
+
+ private Location? _currentLocation;
+ private Monster? _currentMonster;
+ public Player CurrentPlayer { get; private set; }
+ public Location? CurrentLocation
+ {
+ get { return _currentLocation; }
+ set
+ {
+ _currentLocation = value;
+ OnPropertyChanged(nameof(CurrentLocation));
+ }
+ }
+ public Monster? CurrentMonster
+ {
+ get { return _currentMonster; }
+ set
+ {
+ _currentMonster = value;
+ OnPropertyChanged(nameof(CurrentMonster));
+ OnPropertyChanged(nameof(HasMonster));
+ OnPropertyChanged(nameof(IsInBattle));
+ }
+ }
+ public static ShopKeeper ShopKeeper => ShopKeeper.GetInstance();
+ public bool HasMonster => CurrentMonster != null;
+ public bool IsInBattle => HasMonster && CurrentMonster?.CurrentHitPoints > 0;
+ private World CurrentWorld { get; set; }
+
+ #endregion
+
+ public GameSession()
+ {
+ this.CurrentPlayer = new Player("Scott", "Fighter", 10, 0, 1, 100000);
+ this.CurrentWorld = WorldFactory.CreateWorld();
+
+ //temp
+ this.CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItemGroup(1001, 1));
+ this.CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItemGroup(1001, 2));
+ this.CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItemGroup(1002, 1));
+
+ if (!CurrentPlayer.Inventory.Weapons.Any())
+ {
+ this.CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItemGroup(1001, 1));
+ }
+ }
+
+ public void OnClick_Move()
+ {
+ this.CurrentLocation = this.CurrentWorld.GetRandomLocation(this.CurrentLocation);
+ this.CurrentMonster = MonsterFactory.GetRandomMonster();
+
+ GameMessage.RaiseMessage(this, $"You see a {this.CurrentMonster.Name} here!");
+ }
+
+ public void OnClick_Use()
+ {
+ if (this.CurrentPlayer.ItemInHand is null)
+ {
+ GameMessage.RaiseMessage(this, "You have nothing to use.");
+ return;
+ }
+
+ this.CurrentPlayer.ItemInHand.Item.Use(this);
+ }
+
+ public void OnClick_Sell(GameItemGroup item)
+ {
+ if (item.Item.Price is null)
+ {
+ return;
+ }
+
+ this.CurrentPlayer.Inventory.RemoveItem(item);
+ this.CurrentPlayer.Gold += (int)item.Item.Price;
+ GameMessage.RaiseMessage(this, $"You sold 1 {item.Item.Name} for {item.Item.Price} gold.");
+ }
+
+ public void OnClick_Buy(GameItemGroup item)
+ {
+ if (item.Item.Price is null)
+ {
+ return;
+ }
+
+ if (this.CurrentPlayer.Gold < (int)item.Item.Price)
+ {
+ GameMessage.RaiseMessage(this, $"You do not have enough gold to buy {item.Item.Name}.");
+ return;
+ }
+
+ this.CurrentPlayer.Inventory.AddItem(item);
+ this.CurrentPlayer.Gold -= (int)item.Item.Price;
+ GameMessage.RaiseMessage(this, $"You bought 1 {item.Item.Name} for {item.Item.Price} gold.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/EventArgs/GameMessage.cs b/src/RPG_Game/RPG_Game_Engine/EventArgs/GameMessage.cs
new file mode 100644
index 00000000..2c7fb3f3
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/EventArgs/GameMessage.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Engine.EventArgs
+{
+ public static class GameMessage
+ {
+ public static event EventHandler? OnMessageRaised;
+
+ internal static void RaiseMessage(object source, string message)
+ {
+ OnMessageRaised?.Invoke(source, new GameMessageEventArgs(message));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/EventArgs/GameMessageEventArgs.cs b/src/RPG_Game/RPG_Game_Engine/EventArgs/GameMessageEventArgs.cs
new file mode 100644
index 00000000..acf0d481
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/EventArgs/GameMessageEventArgs.cs
@@ -0,0 +1,12 @@
+namespace Engine.EventArgs
+{
+ public class GameMessageEventArgs : System.EventArgs
+ {
+ public string Message { get; private set; }
+
+ public GameMessageEventArgs(string message)
+ {
+ this.Message = message;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Factories/ItemFactory.cs b/src/RPG_Game/RPG_Game_Engine/Factories/ItemFactory.cs
new file mode 100644
index 00000000..443a7e11
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Factories/ItemFactory.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using Engine.Models.Items.Item;
+using Engine.Models.Items.Misc;
+
+namespace Engine.Factories
+{
+ internal static class ItemFactory
+ {
+ private readonly static Dictionary _gameItems;
+
+ static ItemFactory()
+ {
+ _gameItems = new Dictionary
+ {
+ {1001, new Weapon(
+ itemID: 1001,
+ name: "Pointy Stick",
+ minimumDamage: 1,
+ maximumDamage: 2
+ )},
+ {1002, new Weapon(
+ itemID: 1002,
+ name: "Rusty Sword",
+ minimumDamage: 2,
+ maximumDamage: 3,
+ price: 5
+ )},
+ {3001, new Consumable(
+ itemID: 3001,
+ name: "Healing Gauze",
+ price: 7
+ )},
+ {9001, new Craftable(
+ itemID: 9001,
+ name: "Snakeskin",
+ description: "The skin of a snake. Looks durable.",
+ price: 2
+ )},
+ {9002, new Craftable(
+ itemID: 9002,
+ name: "Snake Venom",
+ description: "One drop can kill.",
+ price: 10
+ )},
+ {9003, new Craftable(
+ itemID: 9003,
+ name: "Rat Tail",
+ description: "Looks gross.",
+ price: 1
+ )},
+ {9004, new Craftable(
+ itemID: 9004,
+ name: "Rat Meat",
+ description: "Edible, but not very tasty.",
+ price: 2
+ )},
+ {9005, new Craftable(
+ itemID: 9005,
+ name: "Spider Fang",
+ description: "Sharp and poisonous.",
+ price: 5
+ )},
+ {9006, new Craftable(
+ itemID: 9006,
+ name: "Spider Silk",
+ description: "Strong and flexible.",
+ price: 3
+ )},
+ };
+ }
+
+ internal static GameItem CreateGameItem(int itemID)
+ {
+ if (!_gameItems.ContainsKey(itemID))
+ {
+ throw new ArgumentException(string.Format("Item ID {0} does not exist", itemID));
+ }
+
+ return _gameItems[itemID].Clone();
+ }
+
+ internal static GameItemGroup CreateGameItemGroup(int itemID, int quantity)
+ {
+ GameItem item = CreateGameItem(itemID);
+ return new GameItemGroup(item, quantity);
+ }
+
+ internal static GameItemGroup CreateGameItemGroup(GameItem item, int quantity)
+ {
+ return new GameItemGroup(item, quantity);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Factories/MonsterFactory.cs b/src/RPG_Game/RPG_Game_Engine/Factories/MonsterFactory.cs
new file mode 100644
index 00000000..f685c4aa
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Factories/MonsterFactory.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using Engine.Models.Monsters;
+using Engine.Models.Items.Misc;
+using Engine.Models;
+
+namespace Engine.Factories
+{
+ internal static class MonsterFactory
+ {
+ private readonly static Dictionary _monsters;
+
+ static MonsterFactory()
+ {
+ _monsters = new Dictionary
+ {
+ {4001, new Monster(
+ monsterID: 4001,
+ name: "Snake",
+ imageName: "Snake",
+ maximumHitPoints: 8,
+ minimumDamage: 1,
+ maximumDamage: 2,
+ rewardExperiencePoints: 10,
+ rewardGold: 10,
+ lootTable: new(new() {
+ new DropChance(itemID: 9001, dropPercentage: 100, minimumQuantity: 1, maximumQuantity: 3),
+ new DropChance(itemID: 9002, dropPercentage: 25, minimumQuantity: 1, maximumQuantity: 1),
+ })
+ )},
+ {4002, new Monster(
+ monsterID: 4002,
+ name: "Rat",
+ imageName: "Rat",
+ maximumHitPoints: 5,
+ minimumDamage: 2,
+ maximumDamage: 2,
+ rewardExperiencePoints: 5,
+ rewardGold: 5,
+ lootTable: new(new() {
+ new DropChance(itemID: 9003, dropPercentage: 100, minimumQuantity: 1, maximumQuantity: 1),
+ new DropChance(itemID: 9004, dropPercentage: 75, minimumQuantity: 1, maximumQuantity: 1),
+ })
+ )},
+ {4003, new Monster(
+ monsterID: 4003,
+ name: "Spider",
+ imageName: "Spider",
+ maximumHitPoints: 10,
+ minimumDamage: 1,
+ maximumDamage: 5,
+ rewardExperiencePoints: 10,
+ rewardGold: 10,
+ lootTable: new(new() {
+ new DropChance(itemID: 9005, dropPercentage: 50, minimumQuantity: 1, maximumQuantity: 2),
+ new DropChance(itemID: 9006, dropPercentage: 100, minimumQuantity: 1, maximumQuantity: 1),
+ })
+ )},
+ };
+ }
+
+ internal static Monster CreateMonster(int monsterID)
+ {
+ if (!_monsters.ContainsKey(monsterID))
+ {
+ throw new ArgumentException(string.Format("Monster ID {0} does not exist", monsterID));
+ }
+
+ return _monsters[monsterID].Clone();
+ }
+
+ internal static Monster GetRandomMonster()
+ {
+ int randomMonsterKeyID = RandomNumberGenerator.NumberBetweenExclusive(_monsters.Count);
+
+ return CreateMonster(_monsters.Keys.ElementAt(randomMonsterKeyID));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Factories/WorldFactory.cs b/src/RPG_Game/RPG_Game_Engine/Factories/WorldFactory.cs
new file mode 100644
index 00000000..3b240759
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Factories/WorldFactory.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Engine.Models;
+
+namespace Engine.Factories
+{
+ internal static class WorldFactory
+ {
+ internal static World CreateWorld()
+ {
+ World newWorld = new();
+
+ newWorld.AddLocation("Farmer's Field",
+ "There are rows of corn growing here, with giant rats hiding between them.",
+ "FarmFields");
+ newWorld.AddLocation("Farmer's House",
+ "This is the house of your neighbor, Farmer Ted.",
+ "Farmhouse");
+ newWorld.AddLocation("Home",
+ "This is your home",
+ "Home");
+ newWorld.AddLocation("Town square",
+ "You see a fountain here.",
+ "TownSquare");
+ newWorld.AddLocation("Town Gate",
+ "There is a gate here, protecting the town from giant spiders.",
+ "TownGate");
+ newWorld.AddLocation("Spider Forest",
+ "The trees in this forest are covered with spider webs.",
+ "SpiderForest");
+ newWorld.AddLocation("Herbalist's hut",
+ "You see a small hut, with plants drying from the roof.",
+ "HerbalistsHut");
+ newWorld.AddLocation("Herbalist's garden",
+ "There are many plants here, with snakes hiding behind them.",
+ "HerbalistsGarden");
+
+ return newWorld;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/GameStatus.cs b/src/RPG_Game/RPG_Game_Engine/GameStatus.cs
new file mode 100644
index 00000000..612ddde5
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/GameStatus.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.ComponentModel;
+
+namespace Engine
+{
+ public static class GameStatus
+ {
+ #region Game Variables
+
+ public enum GameVariables
+ {
+ ATTACK_SUCCESS,
+ ATTACK_FAILURE,
+ INSUFFICIENT_ITEM_QUANTITY,
+ ZERO_ITEM_QUANTITY
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/FarmFields.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/FarmFields.png
new file mode 100644
index 00000000..0413abbd
Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/FarmFields.png differ
diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/Farmhouse.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/Farmhouse.png
new file mode 100644
index 00000000..d1c48543
Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/Farmhouse.png differ
diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/HerbalistsGarden.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/HerbalistsGarden.png
new file mode 100644
index 00000000..c9509ce4
Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/HerbalistsGarden.png differ
diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/HerbalistsHut.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/HerbalistsHut.png
new file mode 100644
index 00000000..46e64a67
Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/HerbalistsHut.png differ
diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/Home.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/Home.png
new file mode 100644
index 00000000..835fda6c
Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/Home.png differ
diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/SpiderForest.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/SpiderForest.png
new file mode 100644
index 00000000..c036289a
Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/SpiderForest.png differ
diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/TownGate.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/TownGate.png
new file mode 100644
index 00000000..90a599d1
Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/TownGate.png differ
diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/TownSquare.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/TownSquare.png
new file mode 100644
index 00000000..c780585c
Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/TownSquare.png differ
diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/Trader.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/Trader.png
new file mode 100644
index 00000000..bd45d62a
Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/Trader.png differ
diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Rat.png b/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Rat.png
new file mode 100644
index 00000000..fe441254
Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Rat.png differ
diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Snake.png b/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Snake.png
new file mode 100644
index 00000000..41719af0
Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Snake.png differ
diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Spider.png b/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Spider.png
new file mode 100644
index 00000000..96677491
Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Spider.png differ
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Inventory.cs b/src/RPG_Game/RPG_Game_Engine/Models/Inventory.cs
new file mode 100644
index 00000000..edc35997
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/Inventory.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Collections.ObjectModel;
+using Engine.Models.Items.Item;
+using Engine.Models.Items.Misc;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Engine.Factories;
+
+namespace Engine.Models
+{
+ public class Inventory : BaseNotification
+ {
+ private readonly ObservableCollection _items;
+
+ public ObservableCollection Items { get { return _items; } }
+
+ #region Display Properties
+ public List Weapons { get; private set; }
+ public List Craftables { get; private set; }
+ public List Consumables { get; private set; }
+ public List Usables { get; private set; }
+
+ #endregion
+
+ public Inventory()
+ {
+ _items = new ObservableCollection();
+ _items.CollectionChanged += UpdateDisplayProperties;
+ }
+
+ public void AddItem(GameItemGroup itemGroupToAdd)
+ {
+ GameItemGroup? itemGroup = _items.FirstOrDefault(item => item.Item.IsSameItem(itemGroupToAdd.Item));
+
+ if (itemGroup == null)
+ {
+ _items.Add(itemGroupToAdd);
+ }
+ else
+ {
+ _items[_items.IndexOf(itemGroup)].IncrementQuantity(itemGroupToAdd.Quantity);
+ }
+ }
+
+ public void RemoveItem(GameItemGroup itemGroupToRemove)
+ {
+ GameItemGroup itemGroup = _items.FirstOrDefault(item => item.Item.IsSameItem(itemGroupToRemove.Item))
+ ?? throw new ArgumentException(string.Format("Item {0} not found in inventory", itemGroupToRemove.Item.Name));
+
+ int newQuantity = _items[_items.IndexOf(itemGroup)].DecrementQuantity(itemGroupToRemove.Quantity);
+
+ if (newQuantity == 0)
+ {
+ _items.Remove(itemGroup);
+ }
+ }
+
+ private void UpdateDisplayProperties(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ this.Weapons = new(_items.Where(item => item.Item is Weapon));
+ this.Craftables = new(_items.Where(item => item.Item is Craftable));
+ this.Consumables = new(_items.Where(item => item.Item is Consumable));
+ this.Usables = new[] { this.Weapons, this.Consumables }.SelectMany(item => item).ToList();
+
+ OnPropertyChanged(nameof(Weapons));
+ OnPropertyChanged(nameof(Craftables));
+ OnPropertyChanged(nameof(Consumables));
+ OnPropertyChanged(nameof(Usables));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Consumable.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Consumable.cs
new file mode 100644
index 00000000..f8374bbf
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Consumable.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Engine.Controllers;
+using Engine.Factories;
+
+namespace Engine.Models.Items.Item
+{
+ public class Consumable : GameItem
+ {
+ public Consumable(int itemID, string name, int? price = null) : base(itemID, name, price) { }
+
+ public override Consumable Clone()
+ {
+ return new Consumable(this.ItemID, this.Name, this.Price);
+ }
+
+ private void Consume(Inventory inventory)
+ {
+ inventory.RemoveItem(ItemFactory.CreateGameItemGroup(this, 1));
+ }
+
+ public override void Use(GameSession gameSession)
+ {
+ gameSession.CurrentPlayer.CurrentHitPoints = Math.Min(gameSession.CurrentPlayer.CurrentHitPoints + 5, gameSession.CurrentPlayer.MaximumHitPoints);
+ this.Consume(gameSession.CurrentPlayer.Inventory);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Craftable.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Craftable.cs
new file mode 100644
index 00000000..16a15ef5
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Craftable.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Engine.Controllers;
+
+namespace Engine.Models.Items.Item
+{
+ public class Craftable : GameItem
+ {
+ public string Description { get; set; }
+
+ public Craftable(int itemID, string name, string description, int? price = null) : base(itemID, name, price)
+ {
+ this.Description = description;
+ }
+
+ public override Craftable Clone()
+ {
+ return new Craftable(this.ItemID, this.Name, this.Description, this.Price);
+ }
+
+ public override void Use(GameSession gameSession)
+ {
+ return;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/GameItem.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/GameItem.cs
new file mode 100644
index 00000000..faf89aac
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/GameItem.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Engine.Controllers;
+
+namespace Engine.Models.Items.Item
+{
+ public abstract class GameItem : ICloneable
+ {
+ public int ItemID { get; set; }
+ public string Name { get; set; }
+
+ // Untradable items will have a null price
+ public int? Price { get; set; }
+
+ public GameItem(int itemID, string name, int? price = null)
+ {
+ this.ItemID = itemID;
+ this.Name = name;
+ this.Price = price;
+ }
+
+ object ICloneable.Clone()
+ {
+ return Clone();
+ }
+
+ public abstract GameItem Clone();
+ public abstract void Use(GameSession gameSession);
+
+ public bool IsSameItem(GameItem otherItem)
+ {
+ return this.ItemID == otherItem.ItemID;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Weapon.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Weapon.cs
new file mode 100644
index 00000000..899b9082
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Weapon.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Engine.Controllers;
+using Engine.Models.Monsters;
+using Engine.EventArgs;
+
+namespace Engine.Models.Items.Item
+{
+ public class Weapon : GameItem
+ {
+ public int MinimumDamage { get; set; }
+ public int MaximumDamage { get; set; }
+
+ public Weapon(int itemID, string name, int minimumDamage, int maximumDamage, int? price = null) : base(itemID, name, price)
+ {
+ this.MinimumDamage = minimumDamage;
+ this.MaximumDamage = maximumDamage;
+ }
+
+ public override Weapon Clone()
+ {
+ return new Weapon(this.ItemID, this.Name, this.MinimumDamage, this.MaximumDamage, this.Price);
+ }
+
+ public int RollDamage()
+ {
+ return RandomNumberGenerator.NumberBetweenInclusive(this.MinimumDamage, this.MaximumDamage);
+ }
+
+ public override void Use(GameSession gameSession)
+ {
+ if (gameSession.CurrentPlayer.Attack(gameSession.CurrentMonster!) == GameStatus.GameVariables.ATTACK_FAILURE)
+ {
+ return;
+ }
+
+ if (gameSession.CurrentMonster!.CurrentHitPoints <= 0)
+ {
+ GameMessage.RaiseMessage(this, "");
+ GameMessage.RaiseMessage(this, $"You defeated the {gameSession.CurrentMonster.Name}!");
+
+ gameSession.CurrentMonster.ReleaseRewards(gameSession.CurrentPlayer);
+ gameSession.CurrentMonster = null;
+
+ return;
+ }
+
+ gameSession.CurrentMonster.Attack(gameSession.CurrentPlayer);
+
+ if (gameSession.CurrentPlayer.CurrentHitPoints <= 0)
+ {
+ GameMessage.RaiseMessage(this, "");
+ GameMessage.RaiseMessage(this, $"You were defeated by the {gameSession.CurrentMonster.Name}!");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/DropChance.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/DropChance.cs
new file mode 100644
index 00000000..482dc026
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/DropChance.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Engine.Models.Items.Misc
+{
+ public class DropChance
+ {
+ public int ItemID { get; private set; }
+
+ // Out of 100
+ public int DropPercentage { get; private set; }
+ public int MinimumQuantity { get; private set; }
+ public int MaximumQuantity { get; private set; }
+
+ public DropChance(int itemID, int dropPercentage, int minimumQuantity, int maximumQuantity)
+ {
+ this.ItemID = itemID;
+ this.DropPercentage = dropPercentage;
+ this.MinimumQuantity = minimumQuantity;
+ this.MaximumQuantity = maximumQuantity;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/GameItemGroup.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/GameItemGroup.cs
new file mode 100644
index 00000000..ac4fe1fc
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/GameItemGroup.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Engine.Models.Items.Item;
+
+namespace Engine.Models.Items.Misc
+{
+ public class GameItemGroup : BaseNotification
+ {
+ private int _quantity;
+ public GameItem Item { get; set; }
+ public int Quantity
+ {
+ get { return _quantity; }
+ set
+ {
+ _quantity = value;
+ OnPropertyChanged(nameof(Quantity));
+ }
+ }
+
+ public GameItemGroup(GameItem item, int quantity)
+ {
+ this.Item = item;
+ this.Quantity = quantity;
+ }
+
+ public int IncrementQuantity(int quantity)
+ {
+ this.Quantity += quantity;
+ return this.Quantity;
+ }
+
+ public int DecrementQuantity(int quantity)
+ {
+ if (this.Quantity < quantity)
+ {
+ throw new ArgumentException(string.Format("Item {0} does not have enough quantity", this.Item.Name));
+ }
+
+ this.Quantity -= quantity;
+ return this.Quantity;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/LootTable.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/LootTable.cs
new file mode 100644
index 00000000..d238742d
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/LootTable.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using Engine.Factories;
+
+namespace Engine.Models.Items.Misc
+{
+ public class LootTable
+ {
+ private List DropChances { get; set; }
+
+ public LootTable(List dropChances)
+ {
+ DropChances = dropChances;
+ }
+
+ internal Inventory RollLoot()
+ {
+ Inventory inventory = new();
+
+ foreach (DropChance dropChance in DropChances)
+ {
+ if (RandomNumberGenerator.NumberBetweenInclusive(1, 100) <= dropChance.DropPercentage)
+ {
+ inventory.AddItem(ItemFactory.CreateGameItemGroup(dropChance.ItemID, RandomNumberGenerator.NumberBetweenInclusive(dropChance.MinimumQuantity, dropChance.MaximumQuantity)));
+ }
+ }
+
+ return inventory;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Location.cs b/src/RPG_Game/RPG_Game_Engine/Models/Location.cs
new file mode 100644
index 00000000..29e86bb5
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/Location.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Engine.Models
+{
+ public class Location
+ {
+ public string Name { get; private set; }
+ public string Description { get; private set; }
+ public string ImageName { get; private set; }
+
+ public Location(string name, string description, string imageName)
+ {
+ this.Name = name;
+ this.Description = description;
+ this.ImageName = string.Format("pack://application:,,,/RPG_Game_Engine;component/Images/Locations/{0}.png", imageName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Monsters/Monster.cs b/src/RPG_Game/RPG_Game_Engine/Models/Monsters/Monster.cs
new file mode 100644
index 00000000..437db226
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/Monsters/Monster.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading.Tasks;
+using Engine.Models.Items.Misc;
+using Engine.EventArgs;
+
+namespace Engine.Models.Monsters
+{
+ public class Monster : BaseNotification, ICloneable
+ {
+ private int _currentHitPoints;
+ public int MonsterID { get; private set; }
+ public string Name { get; private set; }
+ public string ImageName { get; private set; }
+ public int MaximumHitPoints { get; private set; }
+ public int CurrentHitPoints
+ {
+ get { return _currentHitPoints; }
+ set
+ {
+ _currentHitPoints = value;
+ OnPropertyChanged(nameof(CurrentHitPoints));
+ }
+ }
+ public int MinimumDamage { get; private set; }
+ public int MaximumDamage { get; private set; }
+ public int RewardExperiencePoints { get; private set; }
+ public int RewardGold { get; private set; }
+ private LootTable LootTable { get; set; }
+ private Inventory? Inventory { get; set; }
+
+ // Constructor for initializing a monster with a loot table
+ public Monster(int monsterID, string name, string imageName, int maximumHitPoints, int minimumDamage, int maximumDamage, int rewardExperiencePoints, int rewardGold, LootTable lootTable)
+ {
+ this.MonsterID = monsterID;
+ this.Name = name;
+ this.ImageName = string.Format("pack://application:,,,/RPG_Game_Engine;component/Images/Monsters/{0}.png", imageName);
+ this.MaximumHitPoints = maximumHitPoints;
+ this.CurrentHitPoints = maximumHitPoints;
+ this.MinimumDamage = minimumDamage;
+ this.MaximumDamage = maximumDamage;
+ this.RewardExperiencePoints = rewardExperiencePoints;
+ this.RewardGold = rewardGold;
+ this.LootTable = lootTable;
+ this.Inventory = null;
+ }
+
+ // Constructor for cloning a monster
+ private Monster(int monsterID, string name, int maximumHitPoints, int minimumDamage, int maximumDamage, int rewardExperiencePoints, int rewardGold, string imageName, LootTable lootTable)
+ {
+ this.MonsterID = monsterID;
+ this.Name = name;
+ this.ImageName = imageName;
+ this.MaximumHitPoints = maximumHitPoints;
+ this.CurrentHitPoints = maximumHitPoints;
+ this.MinimumDamage = minimumDamage;
+ this.MaximumDamage = maximumDamage;
+ this.RewardExperiencePoints = rewardExperiencePoints;
+ this.RewardGold = rewardGold;
+ this.LootTable = lootTable;
+ this.Inventory = LootTable.RollLoot();
+ }
+
+ public Monster Clone()
+ {
+ return new Monster(this.MonsterID, this.Name, this.MaximumHitPoints, this.MinimumDamage, this.MaximumDamage, this.RewardExperiencePoints, this.RewardGold, this.ImageName, this.LootTable);
+ }
+
+ object ICloneable.Clone()
+ {
+ return Clone();
+ }
+
+ internal void Attack(Player player)
+ {
+ int damageToPlayer = RandomNumberGenerator.NumberBetweenInclusive(this.MinimumDamage, this.MaximumDamage);
+
+ player.TakeDamage(this, damageToPlayer);
+ }
+
+ internal void TakeDamage(int damageToMonster)
+ {
+ if (damageToMonster == 0)
+ {
+ GameMessage.RaiseMessage(this, $"You missed the {this.Name}.");
+ }
+ else
+ {
+ this.CurrentHitPoints -= damageToMonster;
+ GameMessage.RaiseMessage(this, $"You hit the {this.Name} for {damageToMonster} points.");
+ }
+ }
+
+ internal void ReleaseRewards(Player player)
+ {
+ player.ExperiencePoints += this.RewardExperiencePoints;
+ GameMessage.RaiseMessage(this, $"You receive {this.RewardExperiencePoints} experience points.");
+
+ player.Gold += this.RewardGold;
+ GameMessage.RaiseMessage(this, $"You receive {this.RewardGold} gold.");
+
+ if (this.Inventory == null)
+ {
+ return;
+ }
+
+ foreach (GameItemGroup gameItemGroup in this.Inventory.Items)
+ {
+ player.Inventory.AddItem(gameItemGroup);
+ GameMessage.RaiseMessage(this, $"You receive {gameItemGroup.Quantity} {gameItemGroup.Item.Name}.");
+ }
+
+ this.Inventory = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Player.cs b/src/RPG_Game/RPG_Game_Engine/Models/Player.cs
new file mode 100644
index 00000000..9e2e78b8
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/Player.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Engine.Controllers;
+using Engine.EventArgs;
+using Engine.Models.Items.Item;
+using Engine.Models.Items.Misc;
+using Engine.Models.Monsters;
+
+namespace Engine.Models
+{
+ public class Player : BaseNotification
+ {
+ private string _characterClass;
+ private int _currentHitPoints;
+ private int _maximumHitPoints;
+ private int _experiencePoints;
+ private int _level;
+ private int _gold;
+
+ public string Name { get; private set; }
+ public string CharacterClass
+ {
+ get { return _characterClass; }
+ set
+ {
+ _characterClass = value;
+ OnPropertyChanged(nameof(CharacterClass));
+ }
+ }
+ public int CurrentHitPoints
+ {
+ get { return _currentHitPoints; }
+ set
+ {
+ _currentHitPoints = value;
+ OnPropertyChanged(nameof(CurrentHitPoints));
+ }
+ }
+ public int MaximumHitPoints
+ {
+ get { return _maximumHitPoints; }
+ set
+ {
+ _maximumHitPoints = value;
+ OnPropertyChanged(nameof(MaximumHitPoints));
+ }
+ }
+ public int ExperiencePoints
+ {
+ get { return _experiencePoints; }
+ set
+ {
+ _experiencePoints = value;
+ OnPropertyChanged(nameof(ExperiencePoints));
+ }
+ }
+ public int Level
+ {
+ get { return _level; }
+ set
+ {
+ _level = value;
+ OnPropertyChanged(nameof(Level));
+ }
+ }
+ public int Gold
+ {
+ get { return _gold; }
+ set
+ {
+ _gold = value;
+ OnPropertyChanged(nameof(Gold));
+ }
+ }
+
+ public Inventory Inventory { get; private set; }
+ public GameItemGroup? ItemInHand { get; set; }
+
+ public Player(string name, string characterClass, int hitPoints, int experiencePoints, int level, int gold)
+ {
+ this.Name = name;
+ this.CharacterClass = characterClass;
+ this.CurrentHitPoints = hitPoints;
+ this.MaximumHitPoints = hitPoints;
+ this.ExperiencePoints = experiencePoints;
+ this.Level = level;
+ this.Gold = gold;
+ this.Inventory = new Inventory();
+ }
+
+ internal GameStatus.GameVariables Attack(Monster monster)
+ {
+ if (this.ItemInHand == null || !(this.ItemInHand.Item is Weapon) || this.ItemInHand.Quantity == 0)
+ {
+ GameMessage.RaiseMessage(this, "You must select a weapon to attack.");
+ return GameStatus.GameVariables.ATTACK_FAILURE;
+ }
+
+ Weapon Weapon = (Weapon)this.ItemInHand.Item;
+
+ // Hit the monster
+ monster.TakeDamage(Weapon.RollDamage());
+
+ return GameStatus.GameVariables.ATTACK_SUCCESS;
+ }
+
+ internal void TakeDamage(Monster monster, int damageToPlayer)
+ {
+ if (damageToPlayer == 0)
+ {
+ GameMessage.RaiseMessage(this, $"The {monster.Name} attacks, but misses you.");
+ }
+ else
+ {
+ this.CurrentHitPoints -= damageToPlayer;
+ GameMessage.RaiseMessage(this, $"The {monster.Name} hit you for {damageToPlayer} points.");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/ShopKeeper.cs b/src/RPG_Game/RPG_Game_Engine/Models/ShopKeeper.cs
new file mode 100644
index 00000000..b239e553
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/ShopKeeper.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Engine.Factories;
+
+namespace Engine.Models
+{
+ public class ShopKeeper
+ {
+ #region Singleton Pattern
+ private static readonly ShopKeeper _shopKeeper = new();
+ public static ShopKeeper GetInstance()
+ {
+ return _shopKeeper;
+ }
+ #endregion
+
+ private readonly Inventory _inventory = new();
+ private ShopKeeper()
+ {
+ _inventory.AddItem(ItemFactory.CreateGameItemGroup(3001, 1));
+ _inventory.AddItem(ItemFactory.CreateGameItemGroup(9001, 1));
+ _inventory.AddItem(ItemFactory.CreateGameItemGroup(9002, 1));
+ _inventory.AddItem(ItemFactory.CreateGameItemGroup(9003, 1));
+ _inventory.AddItem(ItemFactory.CreateGameItemGroup(9004, 1));
+ _inventory.AddItem(ItemFactory.CreateGameItemGroup(9005, 1));
+ _inventory.AddItem(ItemFactory.CreateGameItemGroup(9006, 1));
+ }
+
+ public Inventory Inventory
+ {
+ get { return _inventory; }
+ set { }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/Models/World.cs b/src/RPG_Game/RPG_Game_Engine/Models/World.cs
new file mode 100644
index 00000000..3a6ac4fb
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/Models/World.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+namespace Engine.Models
+{
+ internal class World
+ {
+ private readonly List _locations = new();
+ private readonly Dictionary _special_locations = new();
+ internal World() { }
+
+ internal void AddLocation(string name, string description, string imageName)
+ {
+ _locations.Add(new Location(name, description, imageName));
+ }
+
+ internal void AddLocation(string key, string name, string description, string imageName)
+ {
+ _special_locations.Add(key, new Location(name, description, imageName));
+ }
+
+ internal Location GetRandomLocation(Location? prev)
+ {
+ while (true)
+ {
+ int index = RandomNumberGenerator.NumberBetweenExclusive(_locations.Count);
+ if (_locations[index] != prev)
+ {
+ return _locations[index];
+ }
+ }
+ }
+
+ internal Location GetSpecialLocation(string key)
+ {
+ if (!_special_locations.ContainsKey(key))
+ {
+ throw new KeyNotFoundException(string.Format("Key {0} not found in {1} dictionary", key, nameof(_special_locations)));
+ }
+
+ return _special_locations[key];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_Engine/RPG_Game_Engine.csproj b/src/RPG_Game/RPG_Game_Engine/RPG_Game_Engine.csproj
new file mode 100644
index 00000000..6d6c1689
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/RPG_Game_Engine.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net7.0-windows
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/RPG_Game/RPG_Game_Engine/RandomNumberGenerator.cs b/src/RPG_Game/RPG_Game_Engine/RandomNumberGenerator.cs
new file mode 100644
index 00000000..e1c9d332
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_Engine/RandomNumberGenerator.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace Engine
+{
+ internal class RandomNumberGenerator
+ {
+ private static Random _generator = new();
+
+ public static int NumberBetweenInclusive(int minimumValue, int maximumValue)
+ {
+ return _generator.Next(minimumValue, maximumValue + 1);
+ }
+
+ public static int NumberBetweenInclusive(int maximumValue)
+ {
+ return _generator.Next(0, maximumValue + 1);
+ }
+
+ public static int NumberBetweenExclusive(int minimumValue, int maximumValue)
+ {
+ return _generator.Next(minimumValue, maximumValue);
+ }
+
+ public static int NumberBetweenExclusive(int maximumValue)
+ {
+ return _generator.Next(0, maximumValue);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_UI/App.xaml b/src/RPG_Game/RPG_Game_UI/App.xaml
new file mode 100644
index 00000000..73f8ad7c
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_UI/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/src/RPG_Game/RPG_Game_UI/App.xaml.cs b/src/RPG_Game/RPG_Game_UI/App.xaml.cs
new file mode 100644
index 00000000..f8390c83
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_UI/App.xaml.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace RPG_Game_UI
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ }
+}
diff --git a/src/RPG_Game/RPG_Game_UI/AssemblyInfo.cs b/src/RPG_Game/RPG_Game_UI/AssemblyInfo.cs
new file mode 100644
index 00000000..8b5504ec
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_UI/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/src/RPG_Game/RPG_Game_UI/Converters/InvBooleanToVisibilityConverter.cs b/src/RPG_Game/RPG_Game_UI/Converters/InvBooleanToVisibilityConverter.cs
new file mode 100644
index 00000000..14fcd851
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_UI/Converters/InvBooleanToVisibilityConverter.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace RPG_Game_UI.Converters
+{
+ // copied from https://www.dotnetmirror.com/articles/wpf/107/wpf-visibility-converter-example-using-ivalueconverter
+ public class InvBooleanToVisibilityConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ bool visibility = false;
+
+ if (value is bool v)
+ {
+ visibility = v;
+ }
+
+ return visibility ? Visibility.Hidden : Visibility.Visible;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ Visibility visibility = (Visibility)value;
+
+ return visibility == Visibility.Hidden;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_UI/Converters/NullToVisibilityConverter.cs b/src/RPG_Game/RPG_Game_UI/Converters/NullToVisibilityConverter.cs
new file mode 100644
index 00000000..a069479a
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_UI/Converters/NullToVisibilityConverter.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace RPG_Game_UI.Converters
+{
+ // copied from https://stackoverflow.com/questions/21939667/nulltovisibilityconverter-make-visible-if-not-null
+ public class NullToVisibilityConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is null ? Visibility.Hidden : Visibility.Visible;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException(); // not needed
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RPG_Game/RPG_Game_UI/MainWindow.xaml b/src/RPG_Game/RPG_Game_UI/MainWindow.xaml
new file mode 100644
index 00000000..a90b43c8
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_UI/MainWindow.xaml
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/RPG_Game/RPG_Game_UI/MainWindow.xaml.cs b/src/RPG_Game/RPG_Game_UI/MainWindow.xaml.cs
new file mode 100644
index 00000000..a4495368
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_UI/MainWindow.xaml.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+using Engine.Controllers;
+using Engine.EventArgs;
+
+namespace RPG_Game_UI
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ private readonly GameSession _gameSession = new();
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ GameMessage.OnMessageRaised += OnGameMessageRaised;
+ DataContext = _gameSession;
+ }
+
+ private void OnClick_Move(object sender, RoutedEventArgs e)
+ {
+ _gameSession.OnClick_Move();
+ }
+
+ private void OnClick_DisplayShopWindow(object sender, RoutedEventArgs e)
+ {
+ ShopWindow shopWindow = new()
+ {
+ Owner = this,
+ DataContext = _gameSession
+ };
+ shopWindow.ShowDialog();
+ }
+
+ // TODO: change to general 'use' button, that either attacks with weapon or uses consumable
+ private void OnClick_Use(object sender, RoutedEventArgs e)
+ {
+ _gameSession.OnClick_Use();
+ }
+
+ private void OnGameMessageRaised(object sender, GameMessageEventArgs e)
+ {
+ GameMessages.Document.Blocks.Add(new Paragraph(new Run(e.Message)));
+ GameMessages.ScrollToEnd();
+ }
+ }
+}
diff --git a/src/RPG_Game/RPG_Game_UI/RPG_Game_UI.csproj b/src/RPG_Game/RPG_Game_UI/RPG_Game_UI.csproj
new file mode 100644
index 00000000..315e1e2f
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_UI/RPG_Game_UI.csproj
@@ -0,0 +1,14 @@
+
+
+
+ WinExe
+ net7.0-windows
+ enable
+ true
+
+
+
+
+
+
+
diff --git a/src/RPG_Game/RPG_Game_UI/ShopWindow.xaml b/src/RPG_Game/RPG_Game_UI/ShopWindow.xaml
new file mode 100644
index 00000000..72b1e7c6
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_UI/ShopWindow.xaml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/RPG_Game/RPG_Game_UI/ShopWindow.xaml.cs b/src/RPG_Game/RPG_Game_UI/ShopWindow.xaml.cs
new file mode 100644
index 00000000..5819311b
--- /dev/null
+++ b/src/RPG_Game/RPG_Game_UI/ShopWindow.xaml.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+using Engine.Controllers;
+using Engine.EventArgs;
+using Engine.Models.Items.Misc;
+
+namespace RPG_Game_UI
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class ShopWindow : Window
+ {
+ public GameSession Session => DataContext as GameSession;
+ public ShopWindow()
+ {
+ InitializeComponent();
+ }
+ private void OnClick_Sell(object sender, RoutedEventArgs e)
+ {
+ GameItemGroup? item = ((FrameworkElement)sender).DataContext as GameItemGroup;
+
+ if (item is null)
+ {
+ return;
+ }
+
+ Session.OnClick_Sell(new(item.Item, 1));
+ }
+ private void OnClick_Buy(object sender, RoutedEventArgs e)
+ {
+ GameItemGroup? item = ((FrameworkElement)sender).DataContext as GameItemGroup;
+
+ if (item is null)
+ {
+ return;
+ }
+
+ Session.OnClick_Buy(new(item.Item, 1));
+ }
+ private void OnClick_Close(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+ }
+}
diff --git a/src/TelegramGPT/.env.example b/src/TelegramGPT/.env.example
new file mode 100644
index 00000000..18c3a546
--- /dev/null
+++ b/src/TelegramGPT/.env.example
@@ -0,0 +1,2 @@
+OPENAI_API_KEY=YOUR_OPENAI_API_KEY_HERE
+TELEGRAM_TOKEN=YOUR_TELEGRAM_TOKEN_HERE
diff --git a/src/TelegramGPT/.gitignore b/src/TelegramGPT/.gitignore
new file mode 100644
index 00000000..a95fe14c
--- /dev/null
+++ b/src/TelegramGPT/.gitignore
@@ -0,0 +1,162 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+chatbot.json
\ No newline at end of file
diff --git a/src/TelegramGPT/README.md b/src/TelegramGPT/README.md
new file mode 100644
index 00000000..36477bcc
--- /dev/null
+++ b/src/TelegramGPT/README.md
@@ -0,0 +1,27 @@
+# Telegram Bot with ChatGPT API
+
+Date Started: 28/05/2023
+
+Date Completed: 03/08/2023
+
+This is a project that combines both the Telegram Chatbot API as well as OpenAI API. ~~These are the two-part guide for the Telegram Bot - [Create a Telegram Bot](https://khashtamov.com/en/how-to-create-a-telegram-bot-using-python/), [Deploy a Telegram Bot](https://khashtamov.com/en/how-to-deploy-telegram-bot-django/).~~
+
+NOTE: A `.env` file is required with the contents:
+
+```yaml
+TELEGRAM_TOKEN=...
+OPENAI_API_KEY=...
+```
+
+Dependencies:
+
+- python-telegram-bot 20.3
+- openai 0.27.8
+
+References:
+
+- ~~[Telegram Bot Features](https://core.telegram.org/bots/features)~~
+- ~~[Telegram Bot API](https://core.telegram.org/bots/api)~~
+- [python-telegram-bot](https://python-telegram-bot.org/) (my Bot API library of choice)
+- [Introduction to API](https://github.com/python-telegram-bot/python-telegram-bot/wiki/Introduction-to-the-API)
+- [Your First Bot](https://github.com/python-telegram-bot/python-telegram-bot/wiki/Extensions---Your-first-Bot)
diff --git a/src/TelegramGPT/handlers/chat_handler.py b/src/TelegramGPT/handlers/chat_handler.py
new file mode 100644
index 00000000..f165736f
--- /dev/null
+++ b/src/TelegramGPT/handlers/chat_handler.py
@@ -0,0 +1,90 @@
+import handlers.util_handler as utilh
+import utils.utils as utils
+from handlers.util_handler import decorator_help
+from telegram import Update
+from telegram.ext import ContextTypes
+from utils.chatbot import ChatBot
+
+
+async def _start_chat(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.START_CHAT_TEXT
+ )
+
+ if ChatBot.has_chatbot():
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.START_CHAT_LOAD_TEXT
+ )
+
+ return utils.CHAT
+
+
+@decorator_help
+async def _chat_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.CHAT_HELP_TEXT
+ )
+
+
+async def _chat_girl_1(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.START_CHAT_GIRL_1_TEXT
+ )
+
+ ChatBot.build(utils.CHAT_GIRL_1_PROMPT)
+ return utils.CHAT_MODE
+
+
+async def _chat_custom(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.START_CHAT_CUSTOM_TEXT
+ )
+
+ return utils.CHAT_CUSTOM_CONFIG
+
+
+async def _chat_custom_config(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.CHAT_CUSTOM_CONFIG_TEXT
+ )
+
+ ChatBot.build(update.effective_message.text)
+ return utils.CHAT_MODE
+
+
+async def _chat(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ reply = ChatBot.generate_response(update.effective_message.text)
+ await context.bot.send_message(chat_id=update.effective_chat.id, text=reply)
+
+
+async def _chat_cancel(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.CHAT_CANCEL_TEXT
+ )
+
+ return utils.CHAT_CANCEL
+
+
+# Use this to save the chatbot history, if any
+async def _chat_save(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.CHAT_SAVE_TEXT
+ )
+
+ ChatBot.save_chatbot()
+ return await utilh._cancel(update, context)
+
+
+# Use this to load the chatbot history, if any
+async def _chat_load(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ if ChatBot.has_chatbot():
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.CHAT_LOAD_TEXT
+ )
+
+ ChatBot.load_chatbot()
+ return utils.CHAT_MODE
+ else:
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.CHAT_LOAD_ERROR_TEXT
+ )
diff --git a/src/TelegramGPT/handlers/game_handler.py b/src/TelegramGPT/handlers/game_handler.py
new file mode 100644
index 00000000..7b1324fc
--- /dev/null
+++ b/src/TelegramGPT/handlers/game_handler.py
@@ -0,0 +1,22 @@
+import utils.utils as utils
+from handlers.util_handler import decorator_help
+from telegram import Update
+from telegram.ext import ContextTypes
+
+
+async def _start_game(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.START_GAME_TEXT
+ )
+
+ return utils.GAME
+
+
+@decorator_help
+async def _game_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ pass
+
+
+async def _game(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ text = "Game"
+ await context.bot.send_message(chat_id=update.effective_chat.id, text=text)
diff --git a/src/TelegramGPT/handlers/google_handler.py b/src/TelegramGPT/handlers/google_handler.py
new file mode 100644
index 00000000..0059384d
--- /dev/null
+++ b/src/TelegramGPT/handlers/google_handler.py
@@ -0,0 +1,25 @@
+import utils.utils as utils
+from handlers.util_handler import decorator_help
+from telegram import Update
+from telegram.ext import ContextTypes
+from utils.generate import query
+
+
+async def _start_google(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.START_GOOGLE_TEXT
+ )
+
+ return utils.GOOGLE
+
+
+@decorator_help
+async def _google_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.GOOGLE_HELP_TEXT
+ )
+
+
+async def _google(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ reply = query(update.effective_message.text)
+ await context.bot.send_message(chat_id=update.effective_chat.id, text=reply)
diff --git a/src/TelegramGPT/handlers/main_handler.py b/src/TelegramGPT/handlers/main_handler.py
new file mode 100644
index 00000000..34463db3
--- /dev/null
+++ b/src/TelegramGPT/handlers/main_handler.py
@@ -0,0 +1,66 @@
+import handlers.chat_handler as chath
+import handlers.game_handler as gameh
+import handlers.google_handler as googleh
+import handlers.util_handler as utilh
+import utils.utils as utils
+from telegram.ext import CommandHandler, ConversationHandler, MessageHandler, filters
+
+
+def getHandlers():
+ utils.init_states()
+
+ chat_handler = ConversationHandler(
+ entry_points=[
+ CommandHandler("girl1", chath._chat_girl_1),
+ CommandHandler("custom", chath._chat_custom),
+ CommandHandler("load", chath._chat_load),
+ ],
+ states={
+ utils.CHAT_CUSTOM_CONFIG: [
+ MessageHandler(
+ filters.TEXT & ~filters.COMMAND, chath._chat_custom_config
+ )
+ ],
+ utils.CHAT_MODE: [
+ MessageHandler(filters.TEXT & ~filters.COMMAND, chath._chat),
+ CommandHandler("cancel", chath._chat_cancel),
+ ],
+ utils.CHAT_CANCEL: [CommandHandler("save", chath._chat_save)],
+ },
+ fallbacks=[CommandHandler("cancel", utilh._cancel)],
+ map_to_parent={ConversationHandler.END: ConversationHandler.END},
+ )
+
+ entry_points = [
+ CommandHandler("google", googleh._start_google),
+ CommandHandler("chat", chath._start_chat),
+ CommandHandler("game", gameh._start_game),
+ ]
+ main_handler = ConversationHandler(
+ entry_points=entry_points,
+ states={
+ utils.GOOGLE: [
+ MessageHandler(filters.TEXT & ~filters.COMMAND, googleh._google),
+ CommandHandler("help", googleh._google_help),
+ ],
+ utils.CHAT: [chat_handler, CommandHandler("help", chath._chat_help)],
+ utils.GAME: [
+ MessageHandler(filters.TEXT & ~filters.COMMAND, gameh._game),
+ CommandHandler("help", gameh._game_help),
+ ],
+ },
+ fallbacks=[CommandHandler("cancel", utilh._cancel)] + entry_points,
+ )
+
+ ignore_handler = MessageHandler(filters.UpdateType.EDITED, utilh._ignore)
+ help_command_handler = CommandHandler("help", utilh._help)
+ invalid_command_handler = MessageHandler(filters.COMMAND, utilh._invalid_command)
+ invalid_input_handler = MessageHandler(filters.ALL, utilh._invalid_input)
+
+ return [
+ ignore_handler,
+ main_handler,
+ help_command_handler,
+ invalid_command_handler,
+ invalid_input_handler,
+ ]
diff --git a/src/TelegramGPT/handlers/util_handler.py b/src/TelegramGPT/handlers/util_handler.py
new file mode 100644
index 00000000..940800be
--- /dev/null
+++ b/src/TelegramGPT/handlers/util_handler.py
@@ -0,0 +1,43 @@
+import utils.utils as utils
+from telegram import Update
+from telegram.ext import ContextTypes, ConversationHandler
+
+
+async def _help(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.HELP_TEXT
+ )
+
+
+def decorator_help(func):
+ async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await func(update, context)
+ await _help(update, context)
+
+ return wrapper
+
+
+@decorator_help
+async def _invalid_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.INVALID_COMMAND_TEXT
+ )
+
+
+@decorator_help
+async def _invalid_input(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.INVALID_INPUT_TEXT
+ )
+
+
+async def _cancel(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ await context.bot.send_message(
+ chat_id=update.effective_chat.id, text=utils.CANCELLED_TEXT
+ )
+
+ return ConversationHandler.END
+
+
+async def _ignore(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ pass
diff --git a/src/TelegramGPT/main.py b/src/TelegramGPT/main.py
new file mode 100644
index 00000000..19716120
--- /dev/null
+++ b/src/TelegramGPT/main.py
@@ -0,0 +1,15 @@
+import logging
+
+from handlers.main_handler import getHandlers
+from telegram.ext import ApplicationBuilder
+from utils.utils import getTelegramToken
+
+logging.basicConfig(
+ format="%(asctime)s : %(name)s : %(levelname)s : %(message)s", level=logging.INFO
+)
+
+if __name__ == "__main__":
+ application = ApplicationBuilder().token(getTelegramToken()).build()
+ application.add_handlers(getHandlers())
+
+ application.run_polling()
diff --git a/src/TelegramGPT/requirements.in b/src/TelegramGPT/requirements.in
new file mode 100644
index 00000000..1845b02c
--- /dev/null
+++ b/src/TelegramGPT/requirements.in
@@ -0,0 +1,3 @@
+openai==0.27.8
+python-dotenv==1.0.0
+python-telegram-bot==20.4
diff --git a/src/TelegramGPT/requirements.txt b/src/TelegramGPT/requirements.txt
new file mode 100644
index 00000000..468d6451
--- /dev/null
+++ b/src/TelegramGPT/requirements.txt
@@ -0,0 +1,66 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# pip-compile
+#
+aiohttp==3.8.4
+ # via openai
+aiosignal==1.3.1
+ # via aiohttp
+anyio==3.7.1
+ # via httpcore
+async-timeout==4.0.2
+ # via aiohttp
+attrs==23.1.0
+ # via aiohttp
+certifi==2023.5.7
+ # via
+ # httpcore
+ # httpx
+ # requests
+charset-normalizer==3.2.0
+ # via
+ # aiohttp
+ # requests
+colorama==0.4.6
+ # via tqdm
+frozenlist==1.4.0
+ # via
+ # aiohttp
+ # aiosignal
+h11==0.14.0
+ # via httpcore
+httpcore==0.17.3
+ # via httpx
+httpx==0.24.1
+ # via python-telegram-bot
+idna==3.4
+ # via
+ # anyio
+ # httpx
+ # requests
+ # yarl
+multidict==6.0.4
+ # via
+ # aiohttp
+ # yarl
+openai==0.27.8
+ # via -r requirements.in
+python-dotenv==1.0.0
+ # via -r requirements.in
+python-telegram-bot==20.4
+ # via -r requirements.in
+requests==2.31.0
+ # via openai
+sniffio==1.3.0
+ # via
+ # anyio
+ # httpcore
+ # httpx
+tqdm==4.65.0
+ # via openai
+urllib3==2.0.3
+ # via requests
+yarl==1.9.2
+ # via aiohttp
diff --git a/src/TelegramGPT/utils/__init__.py b/src/TelegramGPT/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/TelegramGPT/utils/chatbot.py b/src/TelegramGPT/utils/chatbot.py
new file mode 100644
index 00000000..f62ea79f
--- /dev/null
+++ b/src/TelegramGPT/utils/chatbot.py
@@ -0,0 +1,89 @@
+# Static class that represents a class instance of the chatbot
+# Only one chatbot can be built at a time
+
+# Features:
+# 1. Create the chatbot (this simply creates a new instance of the chatbot)
+# 2. Build the chatbot with the given prompt
+# 3. Generate a response from the chatbot
+# 4. Chatbot remembers chat history
+# 5. Chatbot can be saved and loaded (to save on tokens)
+
+import json
+import os
+
+from utils.generate import chat
+
+save_file_name = "chatbot.json"
+
+
+class ChatBot:
+ chathistory = None
+
+ def __init__(self):
+ raise Exception("ChatBot is a static class and should not be instantiated")
+
+ @classmethod
+ def build(cls, prompt):
+ ChatBot.chathistory = []
+
+ ChatBot.chathistory.append(
+ {
+ "role": "system",
+ "content": prompt,
+ }
+ )
+
+ return ChatBot
+
+ @classmethod
+ def generate_response(cls, input):
+ if ChatBot.chathistory is None:
+ raise Exception("Chatbot has not been built yet")
+
+ ChatBot.chathistory.append(
+ {
+ "role": "user",
+ "content": input,
+ }
+ )
+
+ response = chat(ChatBot.chathistory)
+
+ ChatBot.chathistory.append(
+ {
+ "role": "assistant",
+ "content": response,
+ }
+ )
+
+ return response
+
+ @classmethod
+ def save_chatbot(cls):
+ if ChatBot.chathistory is None:
+ raise Exception("Chatbot has not been built yet")
+
+ try:
+ with open(save_file_name, "w") as f:
+ f.write(json.dumps(ChatBot.chathistory, indent=4))
+ except Exception as e:
+ raise Exception(e)
+
+ return ChatBot
+
+ @classmethod
+ def load_chatbot(cls):
+ if not ChatBot.has_chatbot():
+ raise Exception("Chatbot does not exist")
+
+ try:
+ with open(save_file_name, "r") as f:
+ ChatBot.chathistory = json.loads(f.read())
+ except Exception as e:
+ raise Exception(e)
+
+ return ChatBot
+
+ @classmethod
+ def has_chatbot(cls):
+ return os.path.isfile(save_file_name)
diff --git a/src/TelegramGPT/utils/generate.py b/src/TelegramGPT/utils/generate.py
new file mode 100644
index 00000000..66540710
--- /dev/null
+++ b/src/TelegramGPT/utils/generate.py
@@ -0,0 +1,31 @@
+import logging
+
+import openai
+from utils.utils import getOpenAIAPIKey
+
+log = logging.getLogger(__name__)
+
+openai.api_key = getOpenAIAPIKey()
+MAX_TOKENS = 100
+
+
+def query(prompt, model="text-davinci-003"):
+ response = openai.Completion.create(
+ model=model,
+ prompt=prompt,
+ max_tokens=MAX_TOKENS,
+ temperature=0.3,
+ )
+ log.info(response)
+ return response.choices[0].text.strip()
+
+
+def chat(messages, model="gpt-3.5-turbo"):
+ response = openai.ChatCompletion.create(
+ model=model,
+ messages=messages,
+ max_tokens=MAX_TOKENS,
+ temperature=0.8,
+ )
+ log.info(response)
+ return response.choices[0].message.content.strip()
diff --git a/src/TelegramGPT/utils/utils.py b/src/TelegramGPT/utils/utils.py
new file mode 100644
index 00000000..c243d8ef
--- /dev/null
+++ b/src/TelegramGPT/utils/utils.py
@@ -0,0 +1,86 @@
+import os
+
+from dotenv import load_dotenv
+
+load_dotenv()
+
+HELP_TEXT = """Valid commands:
+/help - Show this help message
+/google - Ask me a question
+/chat - Chat with me
+/game - Play a game with me"""
+INVALID_COMMAND_TEXT = "Invalid command!"
+INVALID_INPUT_TEXT = "Invalid input!"
+CANCELLED_TEXT = "Cancelled!"
+
+START_GOOGLE_TEXT = """Welcome to Google 2.0!
+I will try my best to answer all your questions here!
+
+Type /cancel to cancel."""
+GOOGLE_HELP_TEXT = """Start by typing a question!
+
+Eg. 'What is the meaning of life?' or 'Why is the sky blue?;
+
+Type /cancel to cancel."""
+
+START_CHAT_TEXT = """Welcome to Chat feature!
+
+/girl1 - basic girl chatbot
+/custom - custom chatbot
+
+Type /cancel to cancel."""
+CHAT_HELP_TEXT = """Start by striking up a conversation!
+
+Eg. 'How are you?' or 'What is your name?'
+
+Type /cancel to cancel."""
+START_CHAT_GIRL_1_TEXT = """You have selected preset Chat Girl 1. Happy chatting!
+
+Type /cancel to cancel."""
+CHAT_GIRL_1_PROMPT = "Act as if you are a friendly teenage girl."
+START_CHAT_CUSTOM_TEXT = """You have selected custom chatbot. Type your prompt below!
+
+Type /cancel to cancel."""
+CHAT_CUSTOM_CONFIG_TEXT = """Custom chatbot prompt initialized. Happy chatting!
+
+Type /cancel to cancel."""
+CHAT_CANCEL_TEXT = """Confirm cancel chat?
+Note that this will delete all chat history.
+
+Type /cancel to cancel or /save to save the chatbot history and cancel"""
+CHAT_SAVE_TEXT = "Chatbot history saved!"
+CHAT_LOAD_TEXT = "Chatbot history loaded!"
+START_CHAT_LOAD_TEXT = "Previous chatbot history available.\n\nType /load to load."
+CHAT_LOAD_ERROR_TEXT = "No previous chatbot history available."
+
+START_GAME_TEXT = "Welcome game"
+
+# States
+GOOGLE = 0
+CHAT = 1
+GAME = 2
+
+# Chat states
+CHAT_MODE = 0
+CHAT_CUSTOM_CONFIG = 1
+CHAT_CANCEL = 2
+
+
+def init_states():
+ # states = ["GOOGLE", "CHAT", "GAME"]
+ # chat_states = ["CHAT_MODE", "CHAT_CUSTOM_CONFIG", "CHAT_CANCEL"]
+
+ # for val, state in enumerate(states):
+ # globals()[state] = val
+
+ # for val, chat_state in enumerate(chat_states):
+ # globals()[chat_state] = val
+ pass
+
+
+def getTelegramToken():
+ return os.getenv("TELEGRAM_TOKEN")
+
+
+def getOpenAIAPIKey():
+ return os.getenv("OPENAI_API_KEY")