Page MenuHomedesp's stash

No OneTemporary

This document is not UTF8. It was detected as EUC-JP and converted to UTF8 for display.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0bf028c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+ArithTrainer/trainer.conf
+**/users.data*
+**/bin/
\ No newline at end of file
diff --git a/ArithTrainer Tests/.classpath b/ArithTrainer Tests/.classpath
new file mode 100644
index 0000000..fc87796
--- /dev/null
+++ b/ArithTrainer Tests/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+ <classpathentry kind="src" path="test"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/ArithTrainer"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/ArithTrainer Tests/.project b/ArithTrainer Tests/.project
new file mode 100644
index 0000000..b912e69
--- /dev/null
+++ b/ArithTrainer Tests/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>ArithTrainer Tests</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/ArithTrainer Tests/.settings/org.eclipse.jdt.core.prefs b/ArithTrainer Tests/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..3a21537
--- /dev/null
+++ b/ArithTrainer Tests/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,11 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/ArithTrainer Tests/test/me/despawningbone/arithtrain/AllTests.java b/ArithTrainer Tests/test/me/despawningbone/arithtrain/AllTests.java
new file mode 100644
index 0000000..5ebbb0e
--- /dev/null
+++ b/ArithTrainer Tests/test/me/despawningbone/arithtrain/AllTests.java
@@ -0,0 +1,11 @@
+package me.despawningbone.arithtrain;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses({ ExpressionTest.class, UserTest.class })
+public class AllTests {
+
+}
diff --git a/ArithTrainer Tests/test/me/despawningbone/arithtrain/ExpressionTest.java b/ArithTrainer Tests/test/me/despawningbone/arithtrain/ExpressionTest.java
new file mode 100644
index 0000000..4428b8f
--- /dev/null
+++ b/ArithTrainer Tests/test/me/despawningbone/arithtrain/ExpressionTest.java
@@ -0,0 +1,71 @@
+package me.despawningbone.arithtrain;
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Test;
+
+public class ExpressionTest {
+
+ @Test
+ public void testEvaluation() { //will not test stepEval since it is vulnerable by design and needs evaluateWithSteps as a linter
+ //normal cases
+ assertEquals(String.valueOf(4.0), ExpressionGenerator.evaluateWithSteps("2+2").get(1)); //normal operation
+ assertEquals(String.valueOf(2.5), ExpressionGenerator.evaluateWithSteps("2+2/4").get(2)); //priority
+ assertEquals(String.valueOf(1-(2-3/(4+5d))-2), ExpressionGenerator.evaluateWithSteps("1-(2-3/(4+5))-2").get(5)); //parentheses
+ assertEquals(String.valueOf(2.0E7), ExpressionGenerator.evaluateWithSteps("1E10 / 500").get(1)); //E notation
+
+ assertEquals(Arrays.asList("5/(4%8)+8-34+43*2", "5/4.0+8-34+43*2" ,"1.25+8-34+43*2", "1.25+8-34+86.0", "9.25-34+86.0", "-24.75+86.0", "61.25"), ExpressionGenerator.evaluateWithSteps("5/(4%8)+8-34+43*2")); //steps
+
+ //edge cases
+ assertEquals(String.valueOf(0.0), ExpressionGenerator.evaluateWithSteps("1 / 1E1000").get(1));
+ assertEquals(String.valueOf(23.0), ExpressionGenerator.evaluateWithSteps("4 0+++2 8*- 1 1 % -1 2+-++-+--- 8 +- 1 ").get(5)); //space placements + positive/negative indicators
+ assertEquals(String.valueOf(210.0), ExpressionGenerator.evaluateWithSteps("7(((((5))))6)").get(2)); //redundant parentheses
+ assertEquals(Arrays.asList(""), ExpressionGenerator.evaluateWithSteps("")); //empty string
+ assertEquals(String.valueOf(-2323628.56), ExpressionGenerator.evaluateWithSteps("2374*-982+19005-11396+3044E-2").get(4)); //negative E notation
+
+ //invalid cases
+ testEvaluationThrows(() -> ExpressionGenerator.evaluateWithSteps("32-(432+13/3/2")); //imbalanced parentheses
+ testEvaluationThrows(() -> ExpressionGenerator.evaluateWithSteps("!3a9-(231)-439-3")); //invalid characters
+ testEvaluationThrows(() -> ExpressionGenerator.evaluateWithSteps("\\39-(231)-439-3")); //invalid characters
+ testEvaluationThrows(() -> ExpressionGenerator.evaluateWithSteps("(1-1)%0")); //division by zero
+ testEvaluationThrows(() -> ExpressionGenerator.evaluateWithSteps("1/0")); //division by zero
+ testEvaluationThrows(() -> ExpressionGenerator.evaluateWithSteps("1E100 * 1E1000 - 1")); //infinity
+ testEvaluationThrows(() -> ExpressionGenerator.evaluateWithSteps(null)); //null
+ }
+
+ private void testEvaluationThrows(Runnable run) {
+ try {
+ run.run();
+ } catch(IllegalArgumentException e) {
+ return;
+ }
+ fail();
+ }
+
+ @Test
+ public void testGenerator() {
+
+ Main.initOperators(); //need to manually invoke this since main class initConfig() is not invoked in test cases
+
+ ExpressionGenerator exp1 = new ExpressionGenerator(19, 5, -1000, 19343);
+
+ //normal cases
+ ExpressionGenerator exp2 = new ExpressionGenerator(10, 3, 0, 1000);
+ assertEquals(true, exp1.isHarder(exp2));
+
+ assertEquals("ExpGen{bracket=19,opCount=5,range=-1000-19343}", exp1.toString());
+
+ String exp = exp1.generate().get(0);
+ assertTrue(exp.length() - exp.replaceAll("(?<=[0-9)])[+\\-*/%]", "").length() == 5); //check how many actual operators are there, excluding negative indicators ("-"s not preceding numbers or parentheses, i.e. "+-" etc)
+
+ //volume test
+ for(int i = 0; i < 10000; i++) {
+ List<String> gen = exp1.generate();
+ assertTrue(ExpressionGenerator.evaluateWithSteps(gen.get(0)).indexOf(gen.get(gen.size() - 1)) != -1);
+ }
+ }
+
+}
diff --git a/ArithTrainer Tests/test/me/despawningbone/arithtrain/UserTest.java b/ArithTrainer Tests/test/me/despawningbone/arithtrain/UserTest.java
new file mode 100644
index 0000000..58ac809
--- /dev/null
+++ b/ArithTrainer Tests/test/me/despawningbone/arithtrain/UserTest.java
@@ -0,0 +1,89 @@
+package me.despawningbone.arithtrain;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.NoSuchElementException;
+
+import org.junit.Test;
+
+import junit.framework.TestCase;
+
+public class UserTest extends TestCase {
+
+ //no need to backup user.data since it will reside inside the test environment instead of usual locations that the trainer will be ran
+
+ @Override
+ public void setUp() {
+ File pfile = new File(System.getProperty("user.dir") + File.separator + "users.data");
+ try {
+ if (!pfile.createNewFile()) {
+ PrintWriter pw = new PrintWriter(pfile); //cleans the user database for test cases
+ pw.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ Main.initLevels(); //have to init default config since actual config isnt loaded
+ }
+
+ @Test
+ public void testUser() throws IOException {
+ //all registers will internally instantiate User, therefore no need to instantiate again to test if user exists
+ //normal cases
+ User user1 = User.register("te$t", "Te$t1", "Te$t1"); //ASCII symbols
+
+ //edge cases
+ User user2 = User.register("te|st", "Test1", "Test1"); //delimiter escaping
+ User user3 = User.register(" ̄ ̄ ̄ ̄", " ̄Tt1", " ̄Tt1"); //unicode characters
+
+ assertTrue(Files.readAllLines(new File(System.getProperty("user.dir") + File.separator + "users.data").toPath(), StandardCharsets.UTF_8).size() == 3);
+
+ assertEquals("te|st", user2.getName()); //check if delimiter escape succeeded
+ assertEquals(" ̄ ̄ ̄ ̄", user3.getName()); //check if unicode can be get with getName correctly
+
+ user1.updatePassword("Te$t1", "T$et1", "T$et1"); //uses same algorithm for matching passwords, so no need to double test
+ new User("te$t", "T$et1"); //check if updated by login
+
+ assertEquals(1.0, user3.getAccuracyRaw());
+
+ for(int i = 0; i < 100; i++) {
+ user1.updateScore(true);
+ user2.updateScore(false);
+ }
+ assertEquals(2, user1.getLevel()); //according to default config, score 100 should be in level 2
+ assertEquals(User.levels.floorEntry(100.0).getValue(), user1.getCurrentGenerator());
+ assertEquals(100, user1.getScore());
+ assertEquals(0.5, user1.getNextLevelPercent());
+ assertEquals(0.0, user2.getAccuracy());
+
+ user2.save();
+ assertTrue(Files.readAllLines(new File(System.getProperty("user.dir") + File.separator + "users.data").toPath(), StandardCharsets.UTF_8).get(1).endsWith("0|100")); //check if score update is reflected in database after save
+
+ //invalid cases
+ testUserThrows(() -> User.register("test", "Test1", "test1")); //non matching passwords
+ testUserThrows(() -> User.register("te$t", "Te$t1", "Te$t1")); //registered user
+ testUserThrows(() -> User.register("test", "test1", "test1")); //not strong enough password
+ testUserThrows(() -> User.register(null, null, null)); //null
+ testUserThrows(() -> User.register("", "Test1", "Test1")); //empty username; empty password doesnt need to be checked since it will violate password strength check anyways
+
+ testUserThrows(() -> new User("te$t", "Test1")); //wrong password
+ testUserThrows(() -> new User("test", "Test1")); //non-existent user
+ testUserThrows(() -> new User(null, null)); //null
+
+ }
+
+ private void testUserThrows(ModifRunnable run) throws IOException {
+ try {
+ run.run();
+ } catch(IllegalArgumentException | NoSuchElementException e) {
+ return;
+ }
+ fail();
+ }
+
+ @FunctionalInterface public interface ModifRunnable { void run() throws IOException; } //so that i dont need to handle IOExceptions internally but pass it to JUnit
+}
diff --git a/ArithTrainer/.classpath b/ArithTrainer/.classpath
new file mode 100644
index 0000000..cfabda2
--- /dev/null
+++ b/ArithTrainer/.classpath
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
+ <accessrules>
+ <accessrule kind="accessible" pattern="javafx/**"/>
+ </accessrules>
+ </classpathentry>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/ArithTrainer/.project b/ArithTrainer/.project
new file mode 100644
index 0000000..8b390f3
--- /dev/null
+++ b/ArithTrainer/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>ArithTrainer</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/ArithTrainer/.settings/org.eclipse.jdt.core.prefs b/ArithTrainer/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..a698e59
--- /dev/null
+++ b/ArithTrainer/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,12 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/ArithTrainer/src/me/despawningbone/arithtrain/ExpressionGenerator.java b/ArithTrainer/src/me/despawningbone/arithtrain/ExpressionGenerator.java
new file mode 100644
index 0000000..fcce0b5
--- /dev/null
+++ b/ArithTrainer/src/me/despawningbone/arithtrain/ExpressionGenerator.java
@@ -0,0 +1,205 @@
+package me.despawningbone.arithtrain;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.TreeMap;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The utility class for expression evaluation and generation.<br>
+ * The generator can be instantiated with different configurable values, allowing for flexible expression generation.<br>
+ * The evaluator can also be statically accessed in this class, since it does not have configurable values.
+ * <p>
+ * <i>Intended for use in the main arithmetic trainer, but third-party programs may also utilize this class as an expression generation and evaluation library if needed.</i>
+ * @author despawningbone
+ */
+
+public class ExpressionGenerator {
+
+ /**
+ * A weighted representation of the operators' weighted chance to appear in generation.
+ * <p>
+ * <b>Note: This is a universal value across all generators!</b>
+ */
+ public final static TreeMap<Integer, String> operators = new TreeMap<>();
+
+ public int bracketChance, operatorCount, origin, bound;
+
+ /**
+ * Instantiates a generator with specifications according to the configuration passed in the parameters.<br>
+ *
+ * @param bracketChance chance for brackets to appear (value should be from 0-100)
+ * @param operatorCount how many operators the generated expression will have
+ * @param origin lower bound of the generated numbers in the expressions (inclusive)
+ * @param bound upper bound of the generated numbers in the expressions (inclusive)
+ */
+ public ExpressionGenerator(int bracketChance, int operatorCount, int origin, int bound) {
+ this.bracketChance = bracketChance;
+ this.operatorCount = operatorCount;
+ this.origin = origin;
+ this.bound = bound;
+ }
+
+
+ /**
+ * Generates a direct {@link ExpressionGenerator#stepEval(List)} parsable expression along with the steps needed to solve according to the generator object's specifications. <br>
+ * Note that as real number operations can be extremely difficult to calculate mentally, the arithmetic trainer will only generate integers.
+ *
+ * @return a generated expression string
+ */
+ public List<String> generate() { //generates only integers
+ ThreadLocalRandom ran = ThreadLocalRandom.current();
+ int startingValue = ran.nextInt(origin, bound); //start it with a value so it wont be invalid
+ String exp = String.valueOf(startingValue);
+ for(int i = 0; i < operatorCount; i++) {
+ int opnd = ran.nextInt(origin, bound);
+ String operator = operators.floorEntry(ran.nextInt(100)).getValue(); //choose one randomly
+ if(ran.nextBoolean()) { //randomly choose to put in front or behind the old expression
+ exp += operator + opnd;
+ } else {
+ exp = opnd + operator + exp;
+ }
+ if(ran.nextInt(100) + 1 < bracketChance) { //add brackets
+ exp = "(" + exp + ")";
+ }
+ }
+
+ try {
+ List<String> steps = stepEval(new ArrayList<String>(Arrays.asList(exp))); //validate if whole expression is valid; might induce performance overhead but its the quickest way to do so
+ //no need evaluateWithSteps() coz the generated expression format can be directly parsed without reformatting
+ return steps;
+ } catch (IllegalArgumentException e) {
+ return generate(); //generate a new one until theres no divide by zero anymore
+ }
+ }
+
+ /**
+ * Compares the current expression generator with another and determines if the current generator has strictly more difficult specifications than the other.<br>
+ * The comparing criteria is as follows:
+ * <ul>
+ * <li>Higher bracket chance</li>
+ * <li>Higher operator count</li>
+ * <li>Larger numeric range</li>
+ * </ul><p>
+ * All of the above must be true in order for the current generator to be considered harder.<br>
+ * @param gen the generator to be compared with
+ * @return true if the current generator is harder, false otherwise
+ */
+ public boolean isHarder(ExpressionGenerator gen) {
+ return this.bracketChance >= gen.bracketChance && this.operatorCount >= gen.operatorCount && (this.bound - this.origin) > (gen.bound - gen.origin);
+ }
+
+
+
+ /**
+ * Returns a textual representation of the specifications defined in this generator object.<br>
+ * @return an array-like representation of the generator
+ */
+ @Override
+ public String toString() { //allow people to get the config values via toString()
+ return "ExpGen{bracket=" + this.bracketChance + ",opCount=" + this.operatorCount + ",range=" + this.origin + "-" + this.bound + "}";
+ }
+
+
+ /**
+ * Lints and evaluates a string expression, and returns the evaluated result with steps.<br>
+ *
+ * @param exp the expression to be evaluated
+ * @return an array storing the steps involved in evaluating the result
+ */
+ public static List<String> evaluateWithSteps(String exp) { //wrapper for the stepEval function to make it easier to understand externally, and also acts as an expression linter
+ if(exp == null || Pattern.compile("[^+\\-*/%().0-9E ]").matcher(exp).find()) { //checking for anything other than the operators supported
+ throw new IllegalArgumentException("Invalid expression!");
+ }
+
+ Pattern pattern = Pattern.compile("\\(([0-9.E]*)\\)|\\((-*[0-9.E]*)\\)"); //remove unnecessary brackets, eg 1-(-6) -> 1--6 so splitting later on will not break
+ exp = exp.replaceAll("\\s", "") //DONE remove all spaces
+ .replaceAll("\\)([0-9(])", ")*$1").replaceAll("([0-9)])\\(", "$1*("); //add back *s between brackets so it wont break, but the steps will include it
+ Matcher match = pattern.matcher(exp);
+ while(match.find()) { //to remove multiple brackets if present, eg ((x))
+ exp = match.replaceAll("$1$2");
+ match = pattern.matcher(exp);
+ }
+
+ return stepEval(new ArrayList<String>(Arrays.asList(exp
+ .replaceAll("(?<![0-9)])[+]+", "") //remove + for positive indicators
+ .replaceAll("(?<![0-9)])[-]((-{4})*)-", "")))); // make consecutive negative signs become one, excluding x--y because it can be x-(-x)
+ }
+
+
+ /**
+ * An internal recursive function that evaluates the last expression in the given list step by step until a single numeric value is yielded.<br>
+ * <p>
+ * This method is not expected to be used alone! As a result, it has the private modifier to prevent outside access.<br>
+ * It is strongly recommended that third-party programs should utilize {@link ExpressionGenerator#evaluateWithSteps(String)} instead of accessing this method through reflection.<br>
+ * @param steps a list containing the expression(s)
+ * @return a list containing the expressions including the newly evaluated one
+ */
+ private static List<String> stepEval(List<String> steps) {
+ String newestExp = steps.get(steps.size() - 1);
+
+ if(newestExp.matches("-{0,1}[0-9.]*(?:E-{0,1})?[0-9.]*")) return steps; //stopping anchor; only case where - sign should exist is at the first char or after E for negative
+
+ String[] splits = newestExp.split("(?<=[^0-9.E])(?=(?<![^0-9]-))(?=(?<!^-))|(?=[^0-9.E])(?<=[^E])"); //split with delimiter, aka expressions; second group defines - without preceding number as negative indicator instead of operator, and 3rd group checks the start of line; last group escapes negative E notations (e.g. E-2)
+ boolean isPrior = false, sameLevel = true; int index = splits.length - 1;
+ for(int i = 0; i < splits.length; i++) {
+ String split = splits[i];
+ if(split.matches("[^0-9.E]*")) { //if its an operand we dont need to do anything
+ if(split.equals("(")) {
+ isPrior = false; //because parentheses preceed any operators, so remove other priorities
+ sameLevel = false;
+ } else if(!isPrior && (split.equals("*") || split.equals("/") || split.equals("%"))) { //only 3 with elevated priorities
+ isPrior = true;
+ index = i;
+ } else if(!isPrior && (!sameLevel || !(splits[index].equals("+") || splits[index].equals("-"))) && (split.equals("+") || split.equals("-"))) { //as its not prioritized, only when there is no prioritized operator will it be evaluated; if it is in the same bracket, evaluate the first operator
+ index = i;
+ sameLevel = true;
+ } else if(split.equals(")")) {
+ isPrior = true; //because parentheses are of highest priority
+ }
+ }
+
+ }
+
+ /*
+ * start eval
+ */
+
+ String op = splits[index];
+ try {
+ double x = Double.parseDouble(splits[index - 1]);
+ double y = Double.parseDouble(splits[index + 1]);
+ double val = 0;
+ switch(op) {
+ case "+": val = x + y;
+ break;
+ case "-": val = x - y;
+ break;
+ case "*": val = x * y;
+ break;
+ case "/": val = x / y;
+ break;
+ case "%": val = x % y; //apparently in java its remainder, not modulus
+ break;
+ }
+ splits[index - 1] = ""; //wipe the old values
+ splits[index + 1] = "";
+ splits[index] = String.valueOf(val); //put the new value in
+ if(index + 2 < splits.length && index - 2 >= 0) {
+ if(splits[index + 2].equals(")") && splits[index - 2].equals("(")) { //throws ArrayIndexOutOfBoundsException theres inbalanced parentheses
+ splits[index + 2] = ""; //wipes used parentheses
+ splits[index - 2] = "";
+ }
+ }
+ } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
+ throw new IllegalArgumentException("Invalid expression!"); //this should only throw if there is 2 operators at the same place, result reaches infinity or undefined, or if there is unfinished operators at the ends
+ }
+
+ steps.add(String.join("", splits));
+ return stepEval(steps);
+ }
+
+}
diff --git a/ArithTrainer/src/me/despawningbone/arithtrain/Main.java b/ArithTrainer/src/me/despawningbone/arithtrain/Main.java
new file mode 100644
index 0000000..629aefb
--- /dev/null
+++ b/ArithTrainer/src/me/despawningbone/arithtrain/Main.java
@@ -0,0 +1,232 @@
+package me.despawningbone.arithtrain;
+
+import java.awt.GraphicsEnvironment;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Map.Entry;
+
+import javafx.animation.FadeTransition;
+import javafx.animation.PauseTransition;
+import javafx.application.Application;
+import javafx.scene.image.Image;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.paint.Color;
+import javafx.scene.text.Font;
+import javafx.scene.text.Text;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+
+/**
+ * The initializing class for the arithmetic trainer.
+ * @author despawningbone
+ *
+ */
+public class Main extends Application {
+ /**
+ * Global precision value for answers accepted for the trainer mode.
+ */
+ public static int precision = 2;
+
+ public static void main(String[] args) { //no need to document this function in javadoc as it is the universal entry point of java programs
+ System.setProperty("prism.lcdtext", "false"); //fix anti aliasing
+
+ initConfig();
+
+ File pfile = new File(System.getProperty("user.dir") + File.separator + "users.data");
+ try {
+ if (pfile.createNewFile()) {
+ System.out.println("Cannot find user database, created a new one.");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ launch(args);
+ }
+
+ /**
+ * Initializes the welcome menu and the UIHelper for subsequent GUI pages.<br>
+ * See the parent method for more information.
+ * @param main the main window stage to operate on
+ */
+ @Override
+ public void start(Stage main) throws Exception {
+ Font.loadFont(Main.class.getClassLoader().getResourceAsStream("resources/GothamBook.ttf"), 10);
+ Font.loadFont(Main.class.getClassLoader().getResourceAsStream("resources/GothamBold.ttf"), 10);
+ GraphicsEnvironment g = GraphicsEnvironment.getLocalGraphicsEnvironment(); //resize according to screen
+ int height = g.getDefaultScreenDevice().getDisplayMode().getHeight() / 2;
+ int width = g.getDefaultScreenDevice().getDisplayMode().getWidth() / 2;
+ if(height <= width) width = height / 9 * 16; //bind width to height with 16:9 so that 4:3 and 21:9 wont break the program
+ else height = width / 16 * 9; //bind to height if height is longer than width to avoid going out of bounds
+
+ UIHelper helper = new UIHelper(main, width, height);
+ main.setTitle("Arithmetic Trainer");
+ main.setResizable(false); //to avoid issues with resizing; since the window is already proportional
+ main.getIcons().add(new Image("resources/globe.png"));
+
+ main.setHeight(height);
+ main.setWidth(width);
+
+ //welcome
+ BorderPane welcome = new BorderPane();
+ Text t = new Text("Welcome to the Arithmetic Trainer");
+ t.setStyle("-fx-font-size: 3em; -fx-font-weight: bold;");
+ t.setFill(Color.WHITE);
+ welcome.setStyle("-fx-background-image: url(\"resources/bg-darken.png\"); -fx-background-size: cover;");
+ welcome.setCenter(t);
+ helper.display(welcome, true);
+ main.show();
+
+ //main menu
+
+
+ //transition from welcome to main menu
+ PauseTransition pause = new PauseTransition(Duration.millis(800));
+ pause.setOnFinished(p -> {
+ FadeTransition ft = new FadeTransition(Duration.millis(1200), t);
+ ft.setFromValue(1.0);
+ ft.setToValue(0.0);
+ ft.play();
+ ft.setOnFinished(f -> helper.showMainMenu());
+ });
+ pause.play();
+ }
+
+
+ /**
+ * Initializes the configuration, parsing the values into respective variables and generating one if one is not present.
+ */
+ public static void initConfig() {
+ try {
+ File pfile = new File(System.getProperty("user.dir") + File.separator + "trainer.conf");
+ if (pfile.createNewFile()) { //create config
+ BufferedWriter writer = new BufferedWriter(new FileWriter(pfile));
+ writer.append("######################\n");
+ writer.append("# Arithmetic Trainer \n");
+ writer.append("# By despawningbone \n");
+ writer.append("# v1.0 \n");
+ writer.append("######################\n");
+ writer.append("\n");
+ writer.append("# At least how many decimal places should the precision be\n");
+ writer.append("# for the answers inputted to be accepted? (Integer only)\n");
+ writer.append("# This excludes answers that doesnt need to be rounded,\n");
+ writer.append("# And does not alter the results in evaluation mode.\n");
+ writer.append("Precision=2\n");
+ writer.append("\n");
+ writer.append("# What percentage will each operator appear?\n");
+ writer.append("# They should add up to 100.\n");
+ writer.append("# [+, -, *, /, %]\n");
+ writer.append("OperatorWeight=35, 25, 17, 13, 10\n");
+ writer.append("\n");
+ writer.append("# How many levels should there be?\n");
+ writer.append("# Each new line represent a level, with the top one being the first.\n");
+ writer.append("# [requirement, bracketChance, operatorCount, lowerRange, upperRange]\n");
+ writer.append("# where requirement = score * accuracy,\n");
+ writer.append("# bracketChance = chance for brackets to appear (0-100),\n");
+ writer.append("# operatorCount = how many operators the generated expression will have,\n");
+ writer.append("# lowerRange & upperRange = how large the operand range will be (lower - upper inclusive).\n");
+ writer.append("# only when the next level is harder and contains only Integer values (except for requirement) will it be valid.\n");
+ writer.append("# The first level must start from 0.\n");
+ writer.append("Levels:\n");
+ writer.append("0, 0, 3, 0, 9\n");
+ writer.append("50, 10, 4, -10, 20\n");
+ writer.append("150, 20, 5, -20, 50\n");
+ writer.close();
+ System.out.println("Cannot find configuration, created a new one."); //use debug?
+ initLevels();
+ initOperators();
+ } else { //parse config
+ boolean levelsConfig = false;
+ for(String line : Files.readAllLines(Paths.get(pfile.getPath()))) {
+ if(line.isEmpty() || line.startsWith("#")) { //either comments or new line
+ continue;
+ } else {
+ if(line.contains("=")) { //the 2 options
+ String[] split = line.split("=");
+ switch(split[0].toLowerCase()) {
+ case "precision":
+ try {
+ precision = Integer.parseInt(split[1].trim());
+ if(precision < 0) {
+ precision = 2;
+ throw new NumberFormatException(); //pass to exception handler
+ }
+ } catch (NumberFormatException e) {
+ System.out.println("Invalid precision value! Using default... (2 d.p.)");
+ }
+ break;
+ case "operatorweight":
+ try {
+ int total = 0;
+ String[] vals = split[1].split(", ?");
+ String[] ops = {"+", "-", "*", "/", "%"};
+ for(int i = 0; i < 5; i++) {
+ int weight = Integer.parseInt(vals[i]);
+ ExpressionGenerator.operators.put(total, ops[i]);
+ total += weight;
+ }
+ if(total != 100) {
+ throw new NumberFormatException();
+ }
+ } catch (NumberFormatException e) {
+ System.out.println("Invalid operator weights! Using default... (35, 25, 17, 13, 10)");
+ initOperators();
+ }
+ break;
+ }
+ } else if(line.equalsIgnoreCase("Levels:")) { //start of levels config section
+ levelsConfig = true;
+ } else if(levelsConfig) { //levels config section
+ try {
+ String[] vals = line.split(", ?");
+ double req = Double.parseDouble(vals[0]);
+ ExpressionGenerator newGen = new ExpressionGenerator(Integer.parseInt(vals[1]), Integer.parseInt(vals[2]), Integer.parseInt(vals[3]), Integer.parseInt(vals[4]));
+ Entry<Double, ExpressionGenerator> lowerGen = User.levels.floorEntry(req);
+ Entry<Double, ExpressionGenerator> higherGen = User.levels.ceilingEntry(req);
+ if((lowerGen == null || newGen.isHarder(lowerGen.getValue())) && (higherGen == null || higherGen.getValue().isHarder(newGen))) { //verify if the level makes sense with the requirement
+ User.levels.put(req, newGen);
+ } else {
+ throw new NumberFormatException();
+ }
+ } catch (NumberFormatException e) {
+ System.out.println("Invalid levels! Using default...");
+ initLevels();
+ }
+ }
+ }
+ }
+ if(User.levels.isEmpty() || User.levels.firstKey() != 0) {
+ System.out.println("Invalid levels! Using default...");
+ User.levels.clear();
+ initLevels();
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Initializes the operator weights with default value.
+ */
+ static void initOperators() {
+ int[] vals = {0, 35, 60, 77, 90};
+ String[] ops = {"+", "-", "*", "/", "%"};
+ for(int i = 0; i < 5; i++) {
+ ExpressionGenerator.operators.put(vals[i], ops[i]);
+ }
+ }
+
+ /**
+ * Initializes the levels with default value.
+ */
+ static void initLevels() {
+ User.levels.put((double) 0, new ExpressionGenerator(0, 3, 0 ,9)); //initialize levels
+ User.levels.put((double) 50, new ExpressionGenerator(10, 4, -10, 20));
+ User.levels.put((double) 150, new ExpressionGenerator(20, 5, -20, 50));
+ }
+}
diff --git a/ArithTrainer/src/me/despawningbone/arithtrain/UIHelper.java b/ArithTrainer/src/me/despawningbone/arithtrain/UIHelper.java
new file mode 100644
index 0000000..5698e2a
--- /dev/null
+++ b/ArithTrainer/src/me/despawningbone/arithtrain/UIHelper.java
@@ -0,0 +1,493 @@
+package me.despawningbone.arithtrain;
+
+import java.io.IOException;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.util.Collections;
+import java.util.List;
+import java.util.NoSuchElementException;
+import javafx.animation.PauseTransition;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.PasswordField;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.control.TextArea;
+import javafx.scene.control.TextField;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontWeight;
+import javafx.scene.text.Text;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+import me.despawningbone.arithtrain.ExpressionGenerator;
+import me.despawningbone.arithtrain.Main;
+import me.despawningbone.arithtrain.User;
+
+
+/**
+ * The UIHelper class for providing GUI pages and elements.<br>
+ * It has been designed so that multiple stages can utilize the UIHelper with multiple UIHelper instances.
+ *
+ * @author despawningbone
+ */
+@SuppressWarnings({"unchecked", "rawtypes"}) //had to do this to drastically reduce code lines; besides i wont actually use the events in the functions anyways
+public class UIHelper {
+ private Stage m;
+ private int h;
+ private int w;
+
+ /**
+ * Instantiates the UIHelper with the respective window stage, and its size.
+ * @param main the window stage the UIHelper should operate on
+ * @param width the width of the window stage, in pixels
+ * @param height the height of the window stage, in pixels
+ */
+ public UIHelper(Stage main, int width, int height) {
+ m = main;
+ h = height;
+ w = width;
+ }
+
+ /**
+ * Displays a page according to the node given, adding suitable uniformed styling in the process.
+ * @param node the page node to be displayed
+ * @param darkenBg whether the background should be darkened or not
+ */
+ public void display(Parent node, boolean darkenBg) {
+ Scene scene = new Scene(node, (double) w, (double) h);
+ scene.getStylesheets().add("resources/main.css");
+ node.setStyle("-fx-background-image: url(\"resources/bg" + (darkenBg ? "-darken.png" : ".jpg")
+ + "\"); -fx-background-size: cover;");
+ this.m.setScene(scene);
+ }
+
+ /**
+ * A helper method for retrieving the 2 main menu buttons.
+ * @param op the name of the button to be retrieved
+ * @return the button object
+ */
+ public Button getMenuButton(String op) { //wrapper
+ return this.getPicButton(op, (event) -> { //no need to unrelease it
+ if (op.equals("train")) {
+ this.showLogin(false);
+ } else if (op.equals("solve")) {
+ this.showSolver();
+ }
+
+ }, 0.2D, false);
+ }
+
+ /**
+ * Retrieves a generic PNG-based button from the program's resources directory, and assigns generic reaction listeners to the button.
+ * @param op the name of the button to be retrieved
+ * @param func the function to be executed on mouse press (or enter if the defaultButton is true)
+ * @param scaleFactor how large the button should appear
+ * @param defaultButton whether the button should be triggered on pressing enter anywhere on the GUI.
+ * @return the retrieved button
+ */
+ private Button getPicButton(String op, EventHandler func, double scaleFactor, boolean defaultButton) {
+ Button b = new Button();
+ b.setGraphic(this.getImage(op, "normal", scaleFactor));
+ b.setStyle("-fx-background-color: transparent;");
+ b.setOnMouseEntered((event) -> {
+ b.setGraphic(this.getImage(op, "hover", scaleFactor));
+ });
+ b.setOnDragEntered((event) -> {
+ b.setGraphic(this.getImage(op, "hover", scaleFactor));
+ });
+ b.setOnMouseExited((event) -> {
+ b.setGraphic(this.getImage(op, "normal", scaleFactor));
+ });
+ b.setOnDragExited((event) -> {
+ b.setGraphic(this.getImage(op, "normal", scaleFactor));
+ });
+ b.setOnMousePressed((event) -> {
+ b.setGraphic(this.getImage(op, "pressed", scaleFactor));
+ });
+ b.setOnMouseReleased(func);
+ b.setOnMouseClicked((event) -> {
+ b.setGraphic(this.getImage(op, "hover", scaleFactor));
+ });
+ if (defaultButton) {
+ b.setDefaultButton(true);
+ b.setOnAction(func);
+ }
+
+ return b;
+ }
+
+ /**
+ * Constructs a generic CSS-based button, and assigns generic reaction listeners to the button.
+ * @param name the name of the CSS button
+ * @param style the CSS styling for the main button
+ * @param pressedStyle the CSS styling for when the button is pressed
+ * @param hoverStyle the CSS styling for when the button is hovered
+ * @param func the function to be executed on mouse press (or enter if the defaultButton is true)
+ * @param defaultButton whether the button should be triggered on pressing enter anywhere on the GUI.
+ * @return the retrieved button
+ */
+ private Button getCSSButton(String name, String style, String pressedStyle, String hoverStyle, EventHandler func,
+ boolean defaultButton) {
+ Button b = new Button(name);
+ b.setStyle(style);
+ b.setOnMouseEntered((event) -> {
+ b.setStyle(hoverStyle);
+ });
+ b.setOnDragEntered((event) -> {
+ b.setStyle(hoverStyle);
+ });
+ b.setOnMouseExited((event) -> {
+ b.setStyle(style);
+ });
+ b.setOnDragExited((event) -> {
+ b.setStyle(style);
+ });
+ b.setOnMousePressed((event) -> {
+ b.setStyle(pressedStyle);
+ });
+ b.setOnMouseReleased(func);
+ b.setOnMouseClicked((event) -> {
+ b.setStyle(hoverStyle);
+ });
+ if (defaultButton) {
+ b.setDefaultButton(true);
+ b.setOnAction(func);
+ }
+
+ return b;
+ }
+
+ /**
+ * Gets an image from the program's resource directory with pre-defined names.
+ * @param op the name of the image
+ * @param type the type of the image (hover, normal, pressed etc)
+ * @param scaleFactor how large the image should appear
+ * @return the retrieved image
+ */
+ private ImageView getImage(String op, String type, double scaleFactor) {
+ ImageView img = new ImageView(new Image("resources/" + op + "_" + type + ".png"));
+ img.setFitHeight((double) h * scaleFactor);
+ img.setPreserveRatio(true);
+ return img;
+ }
+
+ /**
+ * Constructs a uniform footer for GUI pages that needs a back button.
+ * @param func a custom function to be executed on pressing the back button, or null for returning to the main menu.
+ * @return the footer as a StackPane
+ */
+ public StackPane getFooter(EventHandler func) {
+ StackPane footer = new StackPane();
+ Rectangle box = new Rectangle(0.0D, 0.0D, (double) w, (double) (h / 8)); //h isnt / 10 to get all of available space
+ box.setStyle("-fx-fill: rgba(0, 0, 0, 0.5);");
+ Button back = this.getPicButton("back", func == null ? (event) -> {
+ this.showMainMenu();
+ } : func, 0.1D, false);
+ back.setCancelButton(true);
+ back.setOnAction(func == null ? (event) -> {
+ this.showMainMenu();
+ } : func);
+ footer.getChildren().add(box);
+ footer.getChildren().add(back);
+ footer.setPrefSize((double) w, (double) (h / 10));
+ footer.setAlignment(Pos.CENTER_LEFT);
+ StackPane.setAlignment(footer, Pos.BOTTOM_CENTER);
+ return footer;
+ }
+
+ /**
+ * Constructs a text field with the icon specified, according to the size instructed.
+ * @param name the name of the text field, this will be shown when the text field is not focused
+ * @param img the icon name in the resources directory of the program
+ * @param width the specified field width
+ * @param height the specified field height
+ * @return the constructed TextField
+ */
+ public TextField getTextField(String name, String img, double width, double height) {
+ Object f = img != null && img.equals("pw") ? new PasswordField() : new TextField(); //exception for passwords
+ ((TextField) f).setPromptText(name);
+ ((TextField) f).setMaxSize(width, height);
+ if (img != null) {
+ ((TextField) f).setPadding(new Insets(0.0D, 0.0D, 0.0D, (double) (w / 30)));
+ }
+
+ ((TextField) f).setStyle("-fx-background-insets: -0.5em;"
+ + (img == null ? ""
+ : " -fx-background-image:url(\"resources/" + img
+ + ".png\"); -fx-background-size: 1.8em; -fx-background-repeat: no-repeat; -fx-background-position: left center;")
+ + "-fx-background-radius: 5em; -fx-background-color: #fff; -fx-font-size: 1.8em;");
+ return (TextField) f;
+ }
+
+ //END HELPER METHODS
+ //START PAGES
+
+ /**
+ * Shows the main menu page.
+ */
+ public void showMainMenu() {
+ VBox menu = new VBox();
+ menu.getChildren().add(this.getMenuButton("train"));
+ menu.getChildren().add(this.getMenuButton("solve"));
+ menu.setAlignment(Pos.CENTER);
+ this.display(menu, false);
+ }
+
+ /**
+ * Shows the login or the register page.
+ * @param reg true to show the register page, and false for the login page.
+ */
+ public void showLogin(boolean reg) {
+ BorderPane pane = new BorderPane();
+ VBox box = new VBox();
+ box.setStyle(
+ "-fx-background-color: linear-gradient(to bottom right, rgba(50, 120, 134, 0.7), rgba(53, 204, 147, 0.9)); -fx-background-radius: 1.5em; -fx-padding: 2em;");
+ box.setAlignment(Pos.CENTER);
+ box.setSpacing((double) (h / 20));
+ HBox header = new HBox();
+ header.setMaxSize((double) (w / 2), (double) (h / 10));
+ header.setAlignment(Pos.CENTER_LEFT);
+ header.setSpacing((double) (w / 60));
+ Text name = new Text(reg ? "Register" : "Sign In");
+ name.setStyle("-fx-font-weight: bold; -fx-font-size: 2.5em; -fx-fill: #fff");
+ name.setFont(Font.font((String) null, FontWeight.EXTRA_BOLD, 25.0D));
+ TextField user = this.getTextField("Username", "user", (double) (w / 3), (double) (h / 10));
+ TextField password = this.getTextField("Password", "pw", (double) (w / 3), (double) (h / 10));
+ TextField reEnter = this.getTextField("Confirm Password", "pw", (double) (w / 3), (double) (h / 10));
+ HBox buttons = new HBox();
+ Button signIn = this.getCSSButton(reg ? "Register" : "Sign In",
+ "-fx-background-color: rgba(20, 20, 20, 0.7); -fx-text-fill: #fff; -fx-background-radius: 0.5em;",
+ "-fx-background-color: rgba(20, 20, 20, 0.85); -fx-text-fill: #fff; -fx-background-radius: 0.5em;",
+ "-fx-background-color: rgba(20, 20, 20, 0.55); -fx-text-fill: #fff; -fx-background-radius: 0.5em;",
+ (event) -> {
+ try {
+ if (reg) {
+ this.showTrainer(User.register(user.getText(), password.getText(), reEnter.getText()));
+ } else {
+ this.showTrainer(new User(user.getText(), password.getText()));
+ }
+ } catch (IllegalArgumentException | NoSuchElementException | IOException e) {
+ Text error = new Text(e instanceof IOException ? "Something went wrong: " + e.getMessage()
+ : e.getMessage());
+ error.setStyle("-fx-font-size: 1em; -fx-fill: #d00");
+ error.setWrappingWidth((double) w / 3.5D);
+ if (header.getChildren().size() == 1) {
+ header.getChildren().add(error);
+ } else {
+ header.getChildren().set(1, error);
+ }
+ }
+
+ }, true);
+ signIn.setPrefSize((double) (w / 8), (double) (h / 15));
+ signIn.setMinSize((double) (w / 8), (double) (h / 15));
+ header.getChildren().add(name);
+ box.getChildren().add(header);
+ box.getChildren().add(user);
+ box.getChildren().add(password);
+ buttons.getChildren().add(signIn);
+ buttons.setAlignment(Pos.CENTER);
+ buttons.setSpacing((double) (w / 12));
+ if (!reg) {
+ Button regButton = this.getCSSButton("Create Account",
+ "-fx-background-color: transparent; -fx-text-fill: #fff;",
+ "-fx-background-color: transparent; -fx-text-fill: #aaa; -fx-underline: true",
+ "-fx-background-color: transparent; -fx-text-fill: #fff; -fx-underline: true", (event) -> {
+ this.showLogin(true);
+ }, false);
+ regButton.setPrefSize((double) (w / 8), (double) (h / 10));
+ buttons.getChildren().add(regButton);
+ } else {
+ box.getChildren().add(reEnter);
+ }
+
+ box.getChildren().add(buttons);
+ box.setMaxSize((double) (w / 2), (double) (h / 2));
+ pane.setCenter(box);
+ pane.setBottom(reg ? this.getFooter((event) -> {
+ this.showLogin(false);
+ }) : this.getFooter((EventHandler) null));
+ this.display(pane, true);
+ }
+
+
+ private String currentResult;
+ /**
+ * Shows the trainer page.
+ * @param user the logged in user that the trainer page should operate in context of
+ */
+ public void showTrainer(User user) {
+ BorderPane pane = new BorderPane();
+ pane.setPadding(new Insets((double) (h / 30), 0.0D, 0.0D, 0.0D));
+ HBox top = new HBox();
+ Text level = new Text("Level");
+ level.setStyle("-fx-font-weight: bold; -fx-font-size: 2em; -fx-fill: #fff");
+ StackPane progress = new StackPane();
+ progress.setMaxSize((double) w * 0.8D, (double) (h / 10));
+ ProgressBar bar = new ProgressBar();
+ bar.setProgress(user.getNextLevelPercent());
+ bar.setPrefSize((double) w * 0.8D, (double) (h / 10));
+ bar.layout();
+ int initLv = user.getLevel();
+ Label fromLevel = new Label(String.valueOf(initLv));
+ fromLevel.setPrefSize((double) (h / 10), (double) (h / 10));
+ Label toLevel = new Label(String.valueOf(initLv + 1));
+ toLevel.setMaxSize((double) (h / 10), (double) (h / 10));
+ progress.getChildren().add(bar);
+ progress.getChildren().add(fromLevel);
+ toLevel.setAlignment(Pos.CENTER);
+ toLevel.setStyle(
+ "-fx-background-radius: 5em; -fx-background-color: #fff; -fx-font-size: 2em; -fx-background-insets: 1em;");
+ fromLevel.setAlignment(Pos.CENTER);
+ fromLevel.setStyle(
+ "-fx-background-radius: 5em; -fx-background-color: #fff; -fx-font-size: 2em; -fx-background-insets: 1em;");
+ progress.getChildren().add(toLevel);
+ StackPane.setAlignment(fromLevel, Pos.CENTER_LEFT);
+ StackPane.setAlignment(toLevel, Pos.CENTER_RIGHT);
+ DecimalFormat df = new DecimalFormat("#" + (Main.precision > 0 ? "." + String.join("", Collections.nCopies(Main.precision, "#")) : ""));
+ df.setRoundingMode(RoundingMode.HALF_UP);
+ PauseTransition wait = new PauseTransition(Duration.seconds(1.0D));
+ VBox center = new VBox();
+ StackPane exp = new StackPane();
+ List<String> gen = user.getCurrentGenerator().generate();
+ Label expContent = new Label(gen.get(0));
+ currentResult = df.format(Double.parseDouble(gen.get(gen.size() - 1)));
+ exp.getStyleClass().add("exp");
+ exp.setMaxSize((double) (w / 2), (double) (h / 4));
+ exp.setFocusTraversable(false);
+ expContent.setPrefSize((double) (w / 2), (double) (h / 4));
+ String expStyle = "-fx-padding: 0.5em; -fx-font-weight: bold; -fx-text-fill: #fff; -fx-alignment: center;"
+ + "-fx-background-radius: 1.2em; -fx-background-color:linear-gradient(to bottom right, rgba(36, 224, 216, 0.6), rgba(62, 106, 169, 0.8));";
+ expContent.setStyle(expStyle + " -fx-font-size: 3em;");
+ exp.getChildren().add(expContent);
+ exp.setAlignment(Pos.CENTER);
+ HBox hB = new HBox();
+ hB.setPrefSize((double) w, (double) (h / 10));
+ hB.setPadding(new Insets(0.0D, 0.0D, 0.0D, (double) (w / 80))); // to offset the inflated textfield
+ TextField input = this.getTextField("Answer", (String) null, (double) (w / 2), (double) (h / 12));
+ input.setPrefSize((double) w, (double) (h / 10));
+ StackPane footer = this.getFooter((event) -> {
+ try {
+ user.save();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ this.showMainMenu();
+ });
+ Label score = new Label("Current Score: " + user.getScore());
+ score.setPadding(new Insets(0.0D, (double) (w / 50), 0.0D, 0.0D));
+ score.setStyle("-fx-font-weight: bold; -fx-font-size: 2.5em; -fx-text-fill: #fff;");
+ footer.getChildren().add(score);
+ StackPane.setAlignment(score, Pos.CENTER_RIGHT);
+ Button send = this.getPicButton("send", (event) -> {
+ int lv = Integer.parseInt(fromLevel.getText());
+ System.out.println("Expression generated: " + expContent.getText() + "; Expected: " + currentResult + ", Answer:" + input.getText());
+ int newLv = 0;
+ try {
+ if (!currentResult.equals(df.format(Double.parseDouble(input.getText())))) { //match with configured precision or preciser
+ throw new NumberFormatException(); //pass to exception handler
+ }
+ expContent.setText("Correct!");
+ newLv = user.updateScore(true);
+ score.setText("Current Score: " + user.getScore());
+ } catch (NumberFormatException e) {
+ newLv = user.updateScore(false);
+ expContent.setText("Incorrect! The answer was: " + currentResult);
+ expContent.setStyle(expStyle + " -fx-font-size: 2em;");
+ }
+
+ input.setText("");
+ ((Button) event.getSource()).setDisable(true); //so people cant spam it when its in result mode; have to cast coz even tho send is definitely initialized it thinks it isnt
+ bar.setProgress(user.getNextLevelPercent());
+ if (lv != newLv) {
+ fromLevel.setText(String.valueOf(newLv));
+ toLevel.setText(String.valueOf(newLv + 1));
+ if (lv < newLv) {
+ expContent.setText("Leveled up!");
+ } else if (lv > newLv) {
+ expContent.setText("Oops! You dropped a level.");
+ }
+ }
+ wait.setOnFinished((e) -> {
+ ((Button) event.getSource()).setDisable(false);
+ List<String> newGen = user.getCurrentGenerator().generate();
+ expContent.setText(newGen.get(0));
+ currentResult = df.format(Double.parseDouble(newGen.get(newGen.size() - 1)));
+ expContent.setStyle(expStyle + " -fx-font-size: 3em;");
+ });
+ wait.play();
+ }, 0.1D, true);
+ hB.getChildren().add(input);
+ hB.getChildren().add(send);
+ hB.setSpacing((double) (w / 60));
+ hB.setAlignment(Pos.CENTER);
+ center.getChildren().add(exp);
+ center.getChildren().add(hB);
+ center.setSpacing((double) (h / 12));
+ center.setMaxSize((double) (w / 2), (double) (h / 2));
+ center.setAlignment(Pos.CENTER);
+ top.getChildren().add(level);
+ top.getChildren().add(progress);
+ top.setSpacing((double) (w / 40));
+ top.setAlignment(Pos.CENTER);
+ pane.setTop(top);
+ pane.setCenter(center);
+ pane.setBottom(footer);
+ this.display(pane, true);
+ }
+
+ /**
+ * Shows the solver page.
+ */
+ public void showSolver() {
+ BorderPane pane = new BorderPane();
+ VBox vB = new VBox();
+ vB.setSpacing((double) (h / 20));
+ vB.setStyle("-fx-padding: 4em;");
+ HBox hB = new HBox();
+ hB.setPrefSize((double) w, (double) (h / 10));
+ TextField input = this.getTextField("Enter your expression here", (String) null, (double) w * 0.79D,
+ (double) (h / 10));
+ input.setPrefSize((double) w, (double) (h / 10));
+ TextArea result = new TextArea();
+ result.setEditable(false); //so no input is done in the steps field
+ result.setPrefSize((double) w, (double) h);
+ result.setFocusTraversable(false);
+ String resultStyle = "-fx-focus-color: transparent; -fx-faint-focus-color: transparent; -fx-border-color: transparent;-fx-padding: 0.5em; -fx-font-size: 1.5em; -fx-font-weight: 400;-fx-opacity: 0.8; -fx-background-radius: 1.2em; -fx-background-color:";
+ result.setStyle(resultStyle + "rgba(255, 255, 255, 0.7)");
+ Button send = this.getPicButton("send", (event) -> {
+ String steps;
+ try {
+ steps = String.join("\n= ", ExpressionGenerator.evaluateWithSteps(input.getText()));
+ result.setStyle(resultStyle + "rgba(255, 255, 255, 0.7)");
+ } catch (IllegalArgumentException e) {
+ //shake the result box?
+ result.setStyle(resultStyle + "rgba(255, 146, 146, 0.7);");
+ steps = e.getMessage();
+ }
+
+ result.setText(steps);
+ }, 0.1D, true);
+ hB.setSpacing((double) (w / 40));
+ hB.getChildren().add(input);
+ hB.getChildren().add(send);
+ hB.setAlignment(Pos.CENTER);
+ vB.getChildren().add(hB);
+ vB.getChildren().add(result);
+ pane.setCenter(vB);
+ pane.setBottom(this.getFooter((EventHandler) null));
+ this.display(pane, true);
+ }
+}
\ No newline at end of file
diff --git a/ArithTrainer/src/me/despawningbone/arithtrain/User.java b/ArithTrainer/src/me/despawningbone/arithtrain/User.java
new file mode 100644
index 0000000..33357c0
--- /dev/null
+++ b/ArithTrainer/src/me/despawningbone/arithtrain/User.java
@@ -0,0 +1,308 @@
+package me.despawningbone.arithtrain;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.NoSuchElementException;
+import java.util.Scanner;
+import java.util.TreeMap;
+
+
+/**
+ * The utility class for handling user account storage and operations, including score calculations and leveling.<br>
+ * Third-party programs are expected to utilize this class to interact with the user database instead of their own codes.
+ *
+ * @author despawningbone
+ */
+public class User { //TODO test whole class; make it not reflection accessible and encrypt the database to prevent cheating/password editing?
+
+ /**
+ * A map storing the levels in the form of {@link ExpressionGenerator} objects.<br>
+ * This allows modular addition of levels from external sources, namely the configuration.
+ * A TreeMap is utilized due to the strong ordering nature of levels and the ease of setting level thresholds.
+ */
+ public static TreeMap<Double, ExpressionGenerator> levels = new TreeMap<>();
+
+ /**
+ * A static method for creating a user object upon register.<br>
+ * <i>Instantiating through {@link User#User(String, String)} should be done instead if the user already exists.</i>
+ * @param name the username of the new user
+ * @param pw the password of the new user, see {@link User#makePassword(String, String)}
+ * @param confirm a matching password for confirmation
+ * @return a new user object
+ * @throws IOException if the user database is not accessible
+ */
+ public static User register(String name, String pw, String confirm) throws IOException {
+ if(name == null || name.isEmpty()) throw new IllegalArgumentException("The username cannot be empty!");
+ if(searchData(name) != null) throw new IllegalArgumentException("This username is already taken!");
+ File pfile = new File(System.getProperty("user.dir") + File.separator + "users.data");
+ FileOutputStream out = new FileOutputStream(pfile, true);
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
+ writer.write(name.replaceAll("\\|", "\\\\|").toLowerCase() + "|" //so that the delimiter is unique even if people use it in usernames; case insensitive
+ + makePassword(pw, confirm) + "|" //encrypt password with salted MD5; using $ as delimiter because hex strings will not contain it
+ + "0|" //score
+ + "0\n" ); //answered (to compute accuracy)
+ writer.close();
+ out.close();
+ return new User(name, pw); //redundant login; but still a good check
+ }
+
+ /**
+ * Generates a salted and hashed password with MD5 for storage in the database.
+ * <p>
+ * Third-party programs are not expected to utilize this method, but they should access this method through reflection if they have to edit the database manually.
+ * @param pw the password which should include both upper case and lower case letters, along with at least one number
+ * @param confirm a matching password for confirmation
+ * @return the hashed string in hexadecimal form
+ */
+ private static String makePassword(String pw, String confirm) {
+ if(!pw.matches("(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).*")) throw new IllegalArgumentException("The password entered is not strong enough!\nIt needs to have at least 1 lower case letter, 1 upper case letter and 1 number.");
+ if(pw.equals(confirm)) {
+ String salt = generateSalt(8); //TODO make salt length configurable?
+ return salt + "$" + hash(pw + salt);
+ } else {
+ throw new IllegalArgumentException("Passwords does not match!");
+ }
+ }
+
+ /**
+ * Compares the hashed password with the password provided.
+ * @param pw the plaintext password inputted
+ * @param hash the hashed password stored
+ * @return true if identical, false otherwise
+ */
+ private static boolean matchPassword(String pw, String hash) {
+ String[] split = hash.split("\\$");
+ return hash(pw + split[0]).equals(split[1]);
+ }
+
+ /**
+ * A helper method for applying MD5 hashing on any unicode passwords.
+ * @param pw the password to be hashed
+ * @return the hashed password in hexadecimal form
+ */
+ private static String hash(String pw) {
+ try {
+
+ return new BigInteger(1, MessageDigest.getInstance("MD5").digest(pw.getBytes("UTF-8"))).toString(16);
+ } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
+ e.printStackTrace(); //should not be possible to have an exception here
+ return null;
+ }
+ }
+
+ private static final char[] alphanumeric = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();
+
+ /**
+ * Generates a variable length alphanumeric salt for use in hashing along with {@link User#hash(String)}.
+ * @param saltLength the desired character length of the salt
+ * @return the salt
+ */
+ private static String generateSalt(int saltLength) {
+ SecureRandom ran = new SecureRandom();
+ char[] chars = new char[saltLength];
+ for(int i = 0; i < saltLength; i++) {
+ chars[i] = alphanumeric[ran.nextInt(62)];
+ }
+ return new String(chars);
+ }
+
+ /**
+ * Searches the user database and returns the user data.
+ * @param name the username of the user to be searched
+ * @return a string array containing the user account details
+ * @throws IOException if the user database is not accessible
+ */
+ private static String[] searchData(String name) throws IOException {
+ if(name == null) return null;
+ name = name.replaceAll("\\|", "\\\\|");
+ Scanner scanner = new Scanner(new File(System.getProperty("user.dir") + File.separator + "users.data"), "UTF-8");
+ while(scanner.hasNextLine()) { //linear search; not putting the file in memory with Files.readLines() because it can be really big
+ String entry = scanner.nextLine();
+ String[] split = entry.split("(?<=[^\\\\])\\|");
+ if(split[0].equalsIgnoreCase(name)) {
+ scanner.close();
+ return split;
+ }
+ }
+ scanner.close();
+ return null;
+ }
+
+ //END STATIC METHODS
+
+ private long score;
+ private long answered;
+ private String name;
+ private ExpressionGenerator gen;
+ private int level;
+
+ /**
+ * Attempts to log a user in, instantiating a {@link User} object on success.
+ * @param name the username of the user to be logged in
+ * @param pw the password of the user to be logged in
+ * @throws IOException if the user database is not accessible
+ */
+ public User(String name, String pw) throws IOException { //perform login
+ String[] userData = searchData(name);
+ if(userData == null) {
+ throw new NoSuchElementException("This user does not exist!");
+ }
+
+ if(!matchPassword(pw, userData[1])) throw new IllegalArgumentException("Wrong password!");
+
+ this.score = Long.parseLong(userData[2]);
+ this.answered = Long.parseLong(userData[3]);
+ this.name = name;
+ this.gen = levels.floorEntry(score * getAccuracyRaw()).getValue();
+ this.level = getLevel();
+ }
+
+ /**
+ * Gets the user's name.
+ * @return the username string
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Gets the display accuracy in percentage form.
+ * @return the display accuracy
+ */
+ public double getAccuracy() {
+ return getAccuracyRaw() * 100;
+ }
+
+ /**
+ * Computes the raw accuracy of the user (from 0-1).
+ * @return the raw accuracy
+ */
+ public double getAccuracyRaw() {
+ return answered == 0 ? 1 : ((double) score / answered);
+ }
+
+ /**
+ * Returns the correct answer count of the user.
+ * @return the user's score
+ */
+ public long getScore() {
+ return score;
+ }
+
+ /**
+ * Returns the {@link ExpressionGenerator} object of the level the user is currently in.
+ * @return the expression generator
+ */
+ public ExpressionGenerator getCurrentGenerator() {
+ return gen;
+ }
+
+ /**
+ * Computes the current percentage from reaching the next level,<br>
+ * and returns infinity if there is no level after the current one.
+ * @return the percentage
+ */
+ public double getNextLevelPercent() {
+ if(levels.ceilingKey(score * getAccuracyRaw()) != null) {
+ return (score * getAccuracyRaw() - levels.floorKey(score * getAccuracyRaw())) / (levels.ceilingKey(score * getAccuracyRaw()) - levels.floorKey(score * getAccuracyRaw())); //so its the actual range not the whole range
+ } else {
+ return Double.POSITIVE_INFINITY;
+ }
+ }
+
+ /**
+ * Saves the user object into the user database.
+ * @throws IOException if the user database is not accessible
+ */
+ public void save() throws IOException { //NOTE: SAVING IS NOT THREAD SAFE!
+ editData( this.name.replaceAll("\\|", "\\\\|").toLowerCase() + "|" //so that the delimiter is unique even if people use it in usernames; case insensitive
+ + searchData(this.name)[1] + "|" //encrypt password with salted MD5; using $ as delimiter because hex strings will not contain it
+ + this.score + "|" //score
+ + this.answered); //answered (to compute accuracy)
+ }
+
+ /**
+ * Updates the user's password.<br>
+ * This function is currently unused in the main arithmetic trainer, however third-party programs are free to utilize this method.
+ * @param oldPw the original password of the user
+ * @param newPw the new password of the user, see {@link User#makePassword(String, String)}
+ * @param confirm a matching password for confirmation
+ * @throws IOException if the user database is not accessible
+ */
+ public void updatePassword(String oldPw, String newPw, String confirm) throws IOException {
+ String[] values = searchData(this.name); //searchData should never return null because this is a valid user object
+ if(matchPassword(oldPw, values[1])) {
+ values[1] = makePassword(newPw, confirm);
+ editData(String.join("|", values));
+ } else {
+ throw new IllegalArgumentException("Wrong password!");
+ }
+ }
+
+ /**
+ * Updates the user's score according to whether he answered correctly or not.
+ * @param success whether the user succeeded in answering the question correctly
+ * @return the level the user should be in after the update, can be higher, lower or unchanged.
+ */
+ public int updateScore(boolean success) { //update score based on success; also updates level if needed
+ if(success) score++;
+ answered++;
+ ExpressionGenerator newGen = levels.floorEntry(score * getAccuracyRaw()).getValue();
+ if(newGen != this.gen) {
+ this.gen = newGen;
+ this.level = getLevel();
+ }
+ return level; //tells back if the level changed; it can be lower or higher
+ }
+
+ /**
+ * An internal method for editing the user database directly, changing the entry to be updated according to the username provided in the update string.
+ * @param update the updated user entry for replacement
+ * @throws IOException if the user database is not accessible
+ */
+ private void editData(String update) throws IOException { //Files.readLines() is utilized because all methods need to put the file into memory anyways
+ Path path = Paths.get(System.getProperty("user.dir") + File.separator + "users.data");
+ List<String> lines = Files.readAllLines(Paths.get(System.getProperty("user.dir") + File.separator + "users.data"), StandardCharsets.UTF_8);
+ for(int i = 0; i < lines.size(); i++) {
+ String[] split = lines.get(i).split("(?<=[^\\\\])\\|");
+ if(split[0].equalsIgnoreCase(this.name.replaceAll("\\|", "\\\\|"))) {
+ lines.set(i, update);
+ Files.write(path, lines, StandardCharsets.UTF_8);
+ return;
+ }
+ }
+ throw new IllegalArgumentException("This user does not exist!");
+ }
+
+ /**
+ * Gets the current level of the user in integer form.
+ * @return the level
+ */
+ public int getLevel() {
+ Iterator<Entry<Double, ExpressionGenerator>> iterator = levels.entrySet().iterator();
+ int i = 1;
+ while(iterator.hasNext()) {
+ if(iterator.next().getValue() == this.gen) {
+ return i;
+ }
+ i++;
+ }
+ throw new IllegalStateException("Invalid level!"); //as score * acc will never go down to 0, and the lowest key is 0, reaching this means something has gone wrong
+ }
+
+}
diff --git a/ArithTrainer/src/resources/GothamBook.ttf b/ArithTrainer/src/resources/GothamBook.ttf
new file mode 100644
index 0000000..6cdde85
Binary files /dev/null and b/ArithTrainer/src/resources/GothamBook.ttf differ
diff --git a/ArithTrainer/src/resources/back_hover.png b/ArithTrainer/src/resources/back_hover.png
new file mode 100644
index 0000000..786efb5
Binary files /dev/null and b/ArithTrainer/src/resources/back_hover.png differ
diff --git a/ArithTrainer/src/resources/back_normal.png b/ArithTrainer/src/resources/back_normal.png
new file mode 100644
index 0000000..16b1bde
Binary files /dev/null and b/ArithTrainer/src/resources/back_normal.png differ
diff --git a/ArithTrainer/src/resources/back_pressed.png b/ArithTrainer/src/resources/back_pressed.png
new file mode 100644
index 0000000..786efb5
Binary files /dev/null and b/ArithTrainer/src/resources/back_pressed.png differ
diff --git a/ArithTrainer/src/resources/bg-darken.png b/ArithTrainer/src/resources/bg-darken.png
new file mode 100644
index 0000000..70ab172
Binary files /dev/null and b/ArithTrainer/src/resources/bg-darken.png differ
diff --git a/ArithTrainer/src/resources/bg.jpg b/ArithTrainer/src/resources/bg.jpg
new file mode 100644
index 0000000..68af42b
Binary files /dev/null and b/ArithTrainer/src/resources/bg.jpg differ
diff --git a/ArithTrainer/src/resources/globe.png b/ArithTrainer/src/resources/globe.png
new file mode 100644
index 0000000..027ad0a
Binary files /dev/null and b/ArithTrainer/src/resources/globe.png differ
diff --git a/ArithTrainer/src/resources/main.css b/ArithTrainer/src/resources/main.css
new file mode 100644
index 0000000..a41a5ee
--- /dev/null
+++ b/ArithTrainer/src/resources/main.css
@@ -0,0 +1,29 @@
+.root {
+ -fx-font-family: "Gotham Bold";
+}
+
+.text-area, .text-area .viewport, .text-area .content {
+ -fx-background-color: transparent;
+}
+
+.exp .scroll-pane .content .text {
+ -fx-text-alignment: center;
+}
+
+.progress-bar {
+ -fx-accent: #44E298;
+ -fx-control-inner-background: transparent;
+ -fx-text-box-border: transparent;
+ -fx-background-color: transparent;
+}
+
+.progress-bar > .bar {
+ -fx-background-radius: 5em;
+ -fx-text-box-border: transparent;
+}
+
+.progress-bar > .track {
+ -fx-text-box-border: transparent;
+ -fx-background-radius: 0.5em;
+ -fx-background-color: #00B0F0;
+}
\ No newline at end of file
diff --git a/ArithTrainer/src/resources/pw.png b/ArithTrainer/src/resources/pw.png
new file mode 100644
index 0000000..d296a99
Binary files /dev/null and b/ArithTrainer/src/resources/pw.png differ
diff --git a/ArithTrainer/src/resources/send_hover.png b/ArithTrainer/src/resources/send_hover.png
new file mode 100644
index 0000000..ef0aa4d
Binary files /dev/null and b/ArithTrainer/src/resources/send_hover.png differ
diff --git a/ArithTrainer/src/resources/send_normal.png b/ArithTrainer/src/resources/send_normal.png
new file mode 100644
index 0000000..ac333e1
Binary files /dev/null and b/ArithTrainer/src/resources/send_normal.png differ
diff --git a/ArithTrainer/src/resources/send_pressed.png b/ArithTrainer/src/resources/send_pressed.png
new file mode 100644
index 0000000..82c24ee
Binary files /dev/null and b/ArithTrainer/src/resources/send_pressed.png differ
diff --git a/ArithTrainer/src/resources/solve.png b/ArithTrainer/src/resources/solve.png
new file mode 100644
index 0000000..83182b1
Binary files /dev/null and b/ArithTrainer/src/resources/solve.png differ
diff --git a/ArithTrainer/src/resources/solve_hover.png b/ArithTrainer/src/resources/solve_hover.png
new file mode 100644
index 0000000..ab1b2c4
Binary files /dev/null and b/ArithTrainer/src/resources/solve_hover.png differ
diff --git a/ArithTrainer/src/resources/solve_normal.png b/ArithTrainer/src/resources/solve_normal.png
new file mode 100644
index 0000000..0f8ba6d
Binary files /dev/null and b/ArithTrainer/src/resources/solve_normal.png differ
diff --git a/ArithTrainer/src/resources/solve_pressed.png b/ArithTrainer/src/resources/solve_pressed.png
new file mode 100644
index 0000000..71e8154
Binary files /dev/null and b/ArithTrainer/src/resources/solve_pressed.png differ
diff --git a/ArithTrainer/src/resources/train.png b/ArithTrainer/src/resources/train.png
new file mode 100644
index 0000000..829d6ac
Binary files /dev/null and b/ArithTrainer/src/resources/train.png differ
diff --git a/ArithTrainer/src/resources/train_hover.png b/ArithTrainer/src/resources/train_hover.png
new file mode 100644
index 0000000..c93ad9d
Binary files /dev/null and b/ArithTrainer/src/resources/train_hover.png differ
diff --git a/ArithTrainer/src/resources/train_normal.png b/ArithTrainer/src/resources/train_normal.png
new file mode 100644
index 0000000..42c4bb6
Binary files /dev/null and b/ArithTrainer/src/resources/train_normal.png differ
diff --git a/ArithTrainer/src/resources/train_pressed.png b/ArithTrainer/src/resources/train_pressed.png
new file mode 100644
index 0000000..4c4b525
Binary files /dev/null and b/ArithTrainer/src/resources/train_pressed.png differ
diff --git a/ArithTrainer/src/resources/user.png b/ArithTrainer/src/resources/user.png
new file mode 100644
index 0000000..ae0d207
Binary files /dev/null and b/ArithTrainer/src/resources/user.png differ
diff --git a/flowcharts/ExpEval.png b/flowcharts/ExpEval.png
new file mode 100644
index 0000000..ada35f8
Binary files /dev/null and b/flowcharts/ExpEval.png differ
diff --git a/flowcharts/Main.png b/flowcharts/Main.png
new file mode 100644
index 0000000..92e6a76
Binary files /dev/null and b/flowcharts/Main.png differ
diff --git a/flowcharts/Optimized.png b/flowcharts/Optimized.png
new file mode 100644
index 0000000..927fe77
Binary files /dev/null and b/flowcharts/Optimized.png differ

File Metadata

Mime Type
text/x-diff
Expires
Sat, Sep 21, 3:59 PM (1 d, 15 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
34/61/8ac78d95a1879c84db427d688e2f

Event Timeline