diff --git a/README.md b/README.md index 1e7a974..6479625 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,25 @@ # TrialORE -ORE's trial management plugin +ORE's trial management and test plugin + +## Test Command Usage + +| Command | Alias | Permission | Description | +|-------------------------|---------------|---------------|-------------------------------------------| +| `/test start` | `/starttest` | trialore.test | Start a test | +| `/test history` | | trialore.test | Shows your test history | +| `/test list [user]` | | trialore.list | Lists the passed tests of an individual | +| `/test stop ` | `/stoptest` | trialore.test | Stop your current test run | +| `/test info [id]` | | trialore.list | Show all info of a test with the given ID | +| `/test check [user]` | `/check` | trialore.list | Check if a user passed the test | +| `/test answer [answer]` | `/testanswer` | trialore.test | Answer a question of the test | + +## Doing a test +1. Start the test using `/test start`. +2. Once you get asked a question run `/test answer [answer]`. \ +The answer should be the answer you think is correct without any prefix. For example it should not be `0b1111` but `1111`. +3. After you finished the test and passed you get a test-id. Paste it in your App. \ +Staff are going to use this to verify your test. ## Full Command Usage @@ -33,4 +52,4 @@ This "abandonment" period defaults to 5 minutes. If a Testificate leaves, they will be demoted to Student and given 5 minutes to rejoin. If they do not rejoin within those 5 minutes, the trial ends. If a trialer leaves, the trial will automatically end in 5 minutes if they do not rejoin, demoting the Testificate to Student. -The only distinction of an abandoned trial is if the trial ends with an automated abandonment note. \ No newline at end of file +The only distinction of an abandoned trial is if the trial ends with an automated abandonment note. diff --git a/build.gradle.kts b/build.gradle.kts index fde8137..b61a806 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,15 +47,16 @@ java { tasks.shadowJar { relocate("co.aikar.commands", "trialore.acf") relocate("co.aikar.locales", "trialore.locales") + + relocate("com.fasterxml.jackson", "org.openredstone.lib.jackson") dependencies { - exclude( - dependency( - "net.luckperms:api:.*" - ) - ) + exclude(dependency("net.luckperms:api:.*")) + exclude(dependency("io.papermc.paper:paper-api:.*")) } + archiveClassifier.set("") } + tasks.build { dependsOn(tasks.shadowJar) } diff --git a/src/main/kotlin/org/openredstone/trialore/Storage.kt b/src/main/kotlin/org/openredstone/trialore/Storage.kt index 0cabcf5..3a31f47 100644 --- a/src/main/kotlin/org/openredstone/trialore/Storage.kt +++ b/src/main/kotlin/org/openredstone/trialore/Storage.kt @@ -2,6 +2,7 @@ package org.openredstone.trialore import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import java.util.* @@ -24,6 +25,16 @@ object Trial : Table("trial") { override val primaryKey = PrimaryKey(id) } +object Test : Table("test") { + val id = integer("id").autoIncrement() + val testificate = varchar("testificate", 36).index() + val start = integer("start") + val end = integer("end").nullable() + val passed = bool("passed") + val wrong = integer("wrong") + override val primaryKey = PrimaryKey(id) +} + object UsernameCache : Table("username_cache") { val uuid = varchar("cache_user", 36).uniqueIndex() val username = varchar("cache_username", 16).index() @@ -41,6 +52,15 @@ data class TrialInfo( val attempt: Int = 0 ) +data class TestInfo( + val testificate: UUID, + val start: Int, + val end: Int, + val passed: Boolean, + val wrong: Int, + val attempt: Int = 0 +) + fun now() = System.currentTimeMillis().floorDiv(1000).toInt() class Storage( @@ -55,8 +75,8 @@ class Storage( } private fun initTables() = transaction(database) { - SchemaUtils.create( - Note, Trial, UsernameCache + SchemaUtils.createMissingTablesAndColumns( + Note, Trial, UsernameCache, Test ) } @@ -76,14 +96,32 @@ class Storage( } } + fun endTest(testificate: UUID, startingtime: Int, passed: Boolean, wrong: Int) = transaction(database) { + Test.insert { + it[Test.testificate] = testificate.toString() + it[start] = startingtime + it[Test.passed] = passed + it[Test.wrong] = wrong + it[end] = now() + }[Test.id] + } + fun getTrials(testificate: UUID): List = transaction(database) { - Trial.selectAll().where { - Trial.testificate eq testificate.toString() - }.map { + Query( + Trial, Trial.testificate eq testificate.toString() + ).map { it[Trial.id] } } + fun getTests(testificate: UUID): List = transaction(database) { + Query( + Test, Test.testificate eq testificate.toString() + ).map { + it[Test.id] + } + } + fun getTrialInfo(trialId: Int): TrialInfo = transaction(database) { val notes = Note.selectAll().where { Note.trial_id eq trialId @@ -102,12 +140,29 @@ class Storage( ) } + fun getTestInfo(testId: Int): TestInfo? = transaction(database) { + val resultRow = Test.selectAll().where { Test.id eq testId }.firstOrNull() ?: return@transaction null + TestInfo( + UUID.fromString(resultRow[Test.testificate]), + resultRow[Test.start], + resultRow[Test.end] ?: 0, + resultRow[Test.passed], + resultRow[Test.wrong] + ) + } + fun getTrialCount(testificate: UUID): Int = transaction(database) { Trial.selectAll().where { Trial.testificate eq testificate.toString() }.count().toInt() } + fun getTestCount(testificate: UUID): Int = transaction(database) { + Test.selectAll().where { + Test.testificate eq testificate.toString() + }.count().toInt() + } + fun insertNote(trialId: Int, note: String) = transaction(database) { Note.insert { it[trial_id] = trialId @@ -133,6 +188,17 @@ class Storage( } } + fun didPass(testificate: UUID) : Boolean { + val tests = getTests(testificate) + tests.forEachIndexed { index, testid -> + val testInfo = getTestInfo(testid) + if (testInfo?.passed ?: false) { + return true + } + } + return false + } + fun ensureCachedUsername(user: UUID, username: String) = transaction(database) { UsernameCache.upsert { it[this.uuid] = user.toString() diff --git a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt new file mode 100644 index 0000000..f5748ea --- /dev/null +++ b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt @@ -0,0 +1,211 @@ +package org.openredstone.trialore + +import co.aikar.commands.BaseCommand +import co.aikar.commands.annotation.* +import org.bukkit.Bukkit +import org.bukkit.entity.Player + + +@CommandAlias("test") +@CommandPermission("trialore.test") +class TestCommand( + private val trialORE: TrialOre, +) : BaseCommand() { + + @Default() + @CommandAlias("starttest") + @Subcommand("start") + @Conditions("notTesting") + @Description("Take a test") + fun onStart(player: Player) { + val testificate = player + + if (trialORE.testMapping.containsKey(testificate.uniqueId)) { + throw TrialOreException("You are already testing. This is 99.9% a Bug. Contact Nick :D") + } + if (trialORE.database.didPass(testificate.uniqueId)) { + player.renderMiniMessage("You already passed the test!") + return + } + val tests = trialORE.database.getTests(testificate.uniqueId) + + val now = System.currentTimeMillis() + val lastThreeTests = tests.takeLast(3) + if (lastThreeTests.size == 3) { + val lastThreeWithin24h = lastThreeTests.all { test -> + val testInfo = trialORE.database.getTestInfo(test) + ?: run { + player.sendMessage("Could not get last 3 tests... Report this to Staff.") + return@all false + } + val startTimeMs = testInfo.start.toLong() * 1000L + now - startTimeMs <= 24 * 60 * 60 * 1000L + } + if (lastThreeWithin24h) { + player.renderMiniMessage("Warning: Your last 3 tests were all taken within the last 24 hours! Do /test history to see them.") + return + } + } + testificate.renderMessage("Starting your test!") + testificate.renderMiniMessage("Note: When answering in binary, don't use a prefix like 0b.") + trialORE.startTest(testificate.uniqueId) + } + + + @Subcommand("list") + @CommandPermission("trialore.list") + @Description("Get the info of an individual from past tests") + @CommandCompletion("@usernameCache") + fun onList(player: Player, @Single target: String) { + var testificate = trialORE.server.onlinePlayers.firstOrNull { it.name == target }?.uniqueId + if (testificate == null) { + testificate = trialORE.database.usernameToUuidCache[target] + ?: throw TrialOreException("Invalid target $target. Please provide an online player or UUID") + } + val tests = trialORE.database.getTests(testificate) + player.renderMiniMessage("$target has taken ${tests.size} tests") + tests.forEachIndexed { index, test -> + val testInfo = trialORE.database.getTestInfo(test) + if (testInfo == null) { + player.sendMessage("Test id was null. If you are Nick, have fun. If not, report to Nick.") + return + } + if (testInfo.passed) { + val startTime = testInfo.start.toLong() + val timestamp = getRelativeTimestamp(startTime) + val wrong = testInfo.wrong + val correct = 25 - wrong + val percentage = (correct.toDouble() / 25.toDouble()) * 100 + val duration = testInfo.end.toLong() - startTime + var rotatingLight = "" + if (duration <= 45) { rotatingLight = ":rotating_light: :rotating_light: :rotating_light: Test done in ${duration}s"} + player.renderMiniMessage("${getDate(startTime)}" + + ", $wrong wrong Answers'>Test: ${index+1} (${percentage}), $timestamp $rotatingLight") + } + } + } + + @Subcommand("info") + @CommandPermission("trialore.list") + @Description("Check a user's test") + fun onInfo(player: Player, @Single id: Int) { + val testInfo = trialORE.database.getTestInfo(id) + if (testInfo == null) { + player.sendMessage("Test not existant. If you are Nick, have fun. If you believe this is an error, report to Nick.") + return + } + val startTime = testInfo.start.toLong() + val duration = testInfo.end.toLong() - startTime + var rotatingLight = "" + if (duration <= 45) { rotatingLight = ":rotating_light: :rotating_light: :rotating_light: Test done in ${duration}s"} + val timestamp = getRelativeTimestamp(startTime) + val wrong = testInfo.wrong + val testificate = testInfo.testificate + val correct = 25 - wrong + val percentage = (correct.toDouble() / 25.toDouble()) * 100 + if (testInfo.passed) { + player.renderMiniMessage("${getDate(startTime)} by $testificate" + + ", $wrong wrong Answers'>Passed! Test: $id (${percentage}), $timestamp $rotatingLight") + } else { + player.renderMiniMessage("Failed! At ${getDate(startTime)} by $testificate" + + ", $wrong wrong Answers'>Failed! Test: $id (${percentage}), $timestamp") + } + } + + @Subcommand("check") + @CommandPermission("trialore.list") + @Description("Check if a User passed the test") + @CommandAlias("check") + fun onCheck(player: Player, target: String) { + val testificate = Bukkit.getOfflinePlayer(target) + val tests = trialORE.database.getTests(testificate.uniqueId) + if (tests.isEmpty()) { + player.renderMiniMessage("Target has not been in any test.") + return + } + tests.forEachIndexed { index, testid -> + val testInfo = trialORE.database.getTestInfo(testid) + if (testInfo?.passed ?: false) { + player.renderMiniMessage("Target passed the test!") + return + } + } + player.renderMiniMessage("Target has failed all their tests!") + } + + @Subcommand("history") + @CommandPermission("trialore.test") + @Description("Get your test history") + fun onHistory(player: Player) { + val testificate = player.uniqueId + val tests = trialORE.database.getTests(testificate) + player.renderMiniMessage("You have taken ${tests.size} tests") + tests.forEachIndexed { index, testid -> + val testInfo = trialORE.database.getTestInfo(testid) + if (testInfo == null) { + player.sendMessage("Test id was null. Report this to Staff.") + return + } + var state = "" + var color = "" + if (testInfo.passed) { + state = "Passed" + color = "" + } else { + state = "Failed" + color = "" + } + val startTime = testInfo.start.toLong() + val timestamp = getRelativeTimestamp(startTime) + val wrong = testInfo.wrong + val correct = 25 - wrong + val percentage = (correct.toDouble() / 25.toDouble()) * 100 + player.renderMiniMessage("${getDate(startTime)}" + + " (State: $color${state}), with ${wrong} wrong Answers'>Test ${index+1} (ID: $testid), $timestamp: $color$percentage%") + } + } + + @CommandAlias("stoptest") + @Subcommand("stop") + @Conditions("testing") + @Description("Stop a test") + fun onStop(player: Player, testMeta: TestMeta) { + player.renderMessage("You have exited your test") + trialORE.endTest(player.uniqueId, testMeta.session.startingtime, false, wrong = 25) + } + + @CommandAlias("testanswer") + @Subcommand("answer") + @Conditions("testing") + @Description("Answer a test question") + fun onAnswer(player: Player, testMeta: TestMeta, answer: String) { + val session = trialORE.testSessions[player.uniqueId] + ?: throw TrialOreException("No active test session found. This is likely a bug.") + + val expected = session.currentAnswer.trim() + val provided = answer.trim() + + val isCorrect = try { + val expNum = if (expected.matches(Regex("^[01]{1,}$"))) { + Integer.parseInt(expected, 2) + } else Integer.parseInt(expected) + val provNum = if (provided.matches(Regex("^[01]{1,}$"))) { + Integer.parseInt(provided, 2) + } else Integer.parseInt(provided) + expNum == provNum + } catch (e: NumberFormatException) { + expected.equals(provided, ignoreCase = true) + } + + if (isCorrect) { + player.renderMiniMessage("Correct!") + } else { + player.renderMiniMessage("Incorrect Answer. Expected: $expected") + session.wrong++ + // trialORE.database.setTestWrong(session.testId, session.wrong) + } + + session.index++ + trialORE.sendNextQuestion(player, session) + } +} diff --git a/src/main/kotlin/org/openredstone/trialore/TrialCommand.kt b/src/main/kotlin/org/openredstone/trialore/TrialCommand.kt index 995641c..010dcd6 100644 --- a/src/main/kotlin/org/openredstone/trialore/TrialCommand.kt +++ b/src/main/kotlin/org/openredstone/trialore/TrialCommand.kt @@ -82,23 +82,30 @@ class TrialCommand( fun onStart(player: Player, @Single target: String, @Single app: String) { val testificate = trialORE.server.onlinePlayers.firstOrNull { it.name == target } ?: throw TrialOreException("That individual is not online and cannot be trialed") - if (trialORE.trialMapping.filter { (_, meta) -> - meta.first == testificate.uniqueId - }.isNotEmpty()) { - throw TrialOreException("That individual is already trialing") - } - if (trialORE.getParent(testificate.uniqueId) != trialORE.config.studentGroup) { - throw TrialOreException("That individual is ineligible for trial due to rank") - } - if (player.uniqueId == testificate.uniqueId) { - throw TrialOreException("You cannot trial yourself") - } - if (!app.startsWith("https://discourse.openredstone.org/")) { - throw TrialOreException("Invalid app: $app") + val tests = trialORE.database.getTests(testificate.uniqueId) + tests.forEachIndexed { index, testid -> + val testInfo = trialORE.database.getTestInfo(testid) + if (testInfo?.passed ?: false) { + if (trialORE.trialMapping.filter { (_, meta) -> + meta.first == testificate.uniqueId + }.isNotEmpty()) { + throw TrialOreException("That individual is already trialing") + } + if (trialORE.getParent(testificate.uniqueId) != trialORE.config.studentGroup) { + throw TrialOreException("That individual is ineligible for trial due to rank") + } + if (player.uniqueId == testificate.uniqueId) { + throw TrialOreException("You cannot trial yourself") + } + if (!app.startsWith("https://discourse.openredstone.org/")) { + throw TrialOreException("Invalid app: $app") + } + player.renderMessage("Starting trial of ${testificate.name}") + testificate.renderMessage("Starting trial with ${player.name}") + trialORE.startTrial(player.uniqueId, testificate.uniqueId, app) + } } - player.renderMessage("Starting trial of ${testificate.name}") - testificate.renderMessage("Starting trial with ${player.name}") - trialORE.startTrial(player.uniqueId, testificate.uniqueId, app) + player.renderMiniMessage("Target has not passed the test yet.") } @Subcommand("note") diff --git a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt index ee01063..3a1d3a0 100644 --- a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt +++ b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt @@ -42,7 +42,8 @@ data class TrialOreConfig( val testificateGroup: String = "testificate", val builderGroup: String = "builder", val webhook: String = "webhook", - val abandonForgiveness: Long = 6000 + val abandonForgiveness: Long = 6000, + val sendFailedTests: Boolean = true ) data class TrialMeta( @@ -50,11 +51,17 @@ data class TrialMeta( val trialId: Int ) +data class TestMeta( + val testificate: UUID, + val session: TrialOre.TestSession +) + class TrialOre : JavaPlugin(), Listener { lateinit var database: Storage lateinit var luckPerms: LuckPerms lateinit var config: TrialOreConfig val trialMapping: MutableMap> = mutableMapOf() + val testMapping: MutableMap = mutableMapOf() private val mapper = ObjectMapper(YAMLFactory()) override fun onEnable() { loadConfig() @@ -75,13 +82,31 @@ class TrialOre : JavaPlugin(), Listener { throw TrialOreException("You are not trialing anyone") } } + commandConditions.addCondition("notTesting") { + // Condition "notTesting" will fail if the person is taking a test + if (testMapping.containsKey(it.issuer.player.uniqueId)) { + throw TrialOreException("You are already taking a Test") + } + } + commandConditions.addCondition("testing") { + // Condition "notTesting" will fail if the person is taking a test + if (!testMapping.containsKey(it.issuer.player.uniqueId)) { + throw TrialOreException("You are not taking a Test") + } + } commandContexts.registerIssuerOnlyContext(TrialMeta::class.java) { context -> val meta = trialMapping[context.player.uniqueId] ?: throw TrialOreException("Invalid trial mapping. This is likely a bug") TrialMeta(meta.first, meta.second) } + commandContexts.registerIssuerOnlyContext(TestMeta::class.java) { context -> + val meta = testMapping[context.player.uniqueId] + ?: throw TrialOreException("Invalid test mapping. This is likely a bug") + TestMeta(context.player.uniqueId, meta) + } commandCompletions.registerCompletion("usernameCache") { database.usernameToUuidCache.keys } registerCommand(TrialCommand(this@TrialOre, VERSION)) + registerCommand(TestCommand(this@TrialOre)) setDefaultExceptionHandler(::handleCommandException, false) } } @@ -101,10 +126,20 @@ class TrialOre : JavaPlugin(), Listener { dataFolder.mkdir() } val configFile = File(dataFolder, "config.yml") - // does not overwrite or throw - configFile.createNewFile() - val config = mapper.readTree(configFile) - val loadedConfig = mapper.treeToValue(config, TrialOreConfig::class.java) + if (!configFile.exists() || configFile.length() == 0L) { + configFile.createNewFile() + val defaultConfig = TrialOreConfig() + mapper.writeValue(configFile, defaultConfig) + } + + val loadedConfig: TrialOreConfig = try { + val config = mapper.readTree(configFile) + mapper.treeToValue(config, TrialOreConfig::class.java) + } catch (e: Exception) { + logger.warning("Failed to load config.yml, using defaults. $e") + TrialOreConfig() + } + logger.info("Loaded config.yml") return loadedConfig } @@ -168,6 +203,11 @@ class TrialOre : JavaPlugin(), Listener { }, config.abandonForgiveness) } } + testMapping.forEach { (testtaker, session) -> + if (uuid == testtaker) { + endTest(testtaker, session.startingtime, false, 25) + } + } } fun startTrial(trialer: UUID, testificate: UUID, app: String) { @@ -192,6 +232,16 @@ class TrialOre : JavaPlugin(), Listener { sendReport(database.getTrialInfo(trialId), database.getTrialCount(testificate)) } + fun endTest(testificate: UUID, startingtime: Int, passed: Boolean, wrong: Int): Int { + val testId = this.database.endTest(testificate, startingtime, passed, wrong) + this.testMapping.remove(testificate) + val testInfo = database.getTestInfo(testId) + if (testInfo?.wrong == 25) { return 0 } + if ((config.sendFailedTests || passed) && testInfo != null) + sendTestReport(testInfo, database.getTestCount(testificate)) + return testId + } + fun getParent(uuid: UUID): String? = luckPerms.userManager.getUser(uuid)?.primaryGroup private fun setLpParent(uuid: UUID, parent: String) { @@ -247,6 +297,43 @@ class TrialOre : JavaPlugin(), Listener { khttp.post(config.webhook, json = payload) } + private fun sendTestReport(testInfo: TestInfo, testCount: Int) { + val correct = 25 - testInfo.wrong + val percentage = (correct.toDouble() / 25.toDouble()) * 100 + var rotatingLight = "" + if (testInfo.end - testInfo.start <= 45 * 1000 ) { rotatingLight = ":rotating_light: :rotating_light: :rotating_light: <45 Seconds!!" } + val lines = mutableListOf( + "**Testificate**: ${database.uuidToUsernameCache[testInfo.testificate]}", + "**Attempt**: $testCount", + "**Start**: ", + "**End**: ", + "**Wrong**: ${testInfo.wrong}", + "**Percentage**: ${"%.2f".format(percentage)}%", + rotatingLight + ) + val color = if(testInfo.passed) { + 0x5fff58 + } else { + 0xff5858 + } + val payload = mapOf( + "embeds" to listOf( + mapOf( + "title" to database.uuidToUsernameCache[testInfo.testificate], + "description" to lines.joinToString("\n"), + "color" to color, + "fields" to listOf( + mapOf( + "name" to "State", + "value" to testInfo.passed + ) + ) + ) + ) + ) + khttp.post(config.webhook, json = payload) + } + private fun handleCommandException( command: BaseCommand, registeredCommand: RegisteredCommand<*>, @@ -263,4 +350,166 @@ class TrialOre : JavaPlugin(), Listener { player.renderMiniMessage("$message") return true } + + data class TestSession( + val startingtime: Int, + val questions: List, + var index: Int = 0, + var currentAnswer: String = "", + var wrong: Int = 0, + val used: MutableMap> = mutableMapOf() + ) + + val testSessions: MutableMap = mutableMapOf() + private val rand = Random() + + fun startTest(testificate: UUID) { + // val testId = this.database.insertTest(testificate) + val startingtime = now() + + val categories = mutableListOf().apply { + repeat(4) { add(1) } + repeat(4) { add(2) } + repeat(8) { add(3) } + repeat(3) { add(4) } + repeat(3) { add(5) } + repeat(3) { add(6) } + } + categories.shuffle(rand) + val session = TestSession(startingtime, categories) + this.testMapping[testificate] = session + testSessions[testificate] = session + + server.getPlayer(testificate)?.let { player -> sendNextQuestion(player, session) + } + + } + + fun sendNextQuestion(player: Player, session: TestSession) { + if (session.index >= session.questions.size) { + val passed = session.wrong <= 2 + val testId = endTest(player.uniqueId, session.startingtime, passed, session.wrong) + testSessions.remove(player.uniqueId) + if (passed) { + player.renderMiniMessage("Test (${testId}) finished. Wrong: ${session.wrong}") + } else { + player.renderMiniMessage(" You failed the test. Wrong: ${session.wrong}") + } + return + } + + val cat = session.questions[session.index] + val (qText, expected) = generateQuestion(cat, session.used) + session.currentAnswer = expected + player.renderMiniMessage("Question ${session.index + 1}/25: $qText") + } + + fun generateQuestion(category: Int, usedPerCategory: MutableMap>): Pair { + val used = usedPerCategory.getOrPut(category) { mutableSetOf() } + fun makeKey(vararg parts: Any) = parts.joinToString(":") + + fun randDecimal(): Int { + val options = (1..14).filterNot { it in listOf(0,1,2,4,8) } + return options[rand.nextInt(options.size)] + } + + fun randTwoSC(): Int { + val options = (-8..7).filter { it != 0 } + return options[rand.nextInt(options.size)] + } + + when(category) { + 1 -> { + var attempts = 0 + while (attempts++ < 200) { + val value = randDecimal() + val bin = value.toString(2).padStart(4,'0') + val key = makeKey("conv-bin->dec", bin) + if (key in used) continue + used.add(key) + return Pair("Convert binary $bin to decimal", value.toString()) + } + return Pair("Convert binary 0101 to decimal", "5") + } + + 2 -> { + var attempts = 0 + while (attempts++ < 200) { + val value = randDecimal() + val bin = value.toString(2).padStart(4,'0') + val key = makeKey("conv-dec->bin", value) + if (key in used) continue + used.add(key) + return Pair("Convert decimal $value to 4-bit binary", bin) + } + return Pair("Convert decimal 5 to 4-bit binary", "0101") + } + + 3 -> { + val gates = listOf("AND","NAND","OR","NOR","XOR","XNOR") + val gateIndex = used.size % gates.size + val op = if (gateIndex < 4) gates[gateIndex] else gates[rand.nextInt(gates.size)] + val a = rand.nextInt(1,15) + var b = rand.nextInt(1,15) + if (b == a) b = (b % 14) + 1 + val aBin = (a and 0xF).toString(2).padStart(4,'0') + val bBin = (b and 0xF).toString(2).padStart(4,'0') + val result = when(op){ + "AND" -> a and b + "NAND" -> (a and b) xor 0xF + "OR" -> a or b + "NOR" -> (a or b) xor 0xF + "XOR" -> a xor b + "XNOR" -> (a xor b) xor 0xF + else -> 0 + } and 0xF + val ansBin = result.toString(2).padStart(4,'0') + used.add(makeKey("gate",op,aBin,bBin)) + return Pair("Apply $op to $aBin and $bBin — give the 4-bit binary result", ansBin) + } + + 4 -> { + var attempts = 0 + while (attempts++ < 200) { + val value = randTwoSC() + val twos = (value and 0xF).toString(2).padStart(4,'0') + val key = makeKey("to-2sc",value) + if (key in used) continue + used.add(key) + return Pair("Write $value as 4-bit two's complement (2sc) binary", twos) + } + return Pair("Write -2 as 4-bit two's complement (2sc) binary", "1110") + } + + 5 -> { + var attempts = 0 + while (attempts++ < 200) { + val x = randTwoSC() and 0xF + val signed = if(x and 0x8 !=0) x-16 else x + val bin = x.toString(2).padStart(4,'0') + val key = makeKey("from-2sc",bin) + if(key in used) continue + used.add(key) + return Pair("What is 4-bit two's complement $bin equal to in decimal?", signed.toString()) + } + return Pair("What is 4-bit two's complement 1110 equal to in decimal?","-2") + } + + 6 -> { + var attempts = 0 + while(attempts++ < 200){ + val v = rand.nextInt(1,9) + val neg = (-v) and 0xF + val negBin = neg.toString(2).padStart(4,'0') + val key = makeKey("neg-2sc",v) + if(key in used) continue + used.add(key) + return Pair("What is the 2's complement (4-bit) representation of -$v ?",negBin) + } + return Pair("What is the 2's complement (4-bit) representation of -5 ?","1011") + } + + else -> return Pair("Invalid category","") + } + } } \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..91841b7 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,6 @@ +studentGroup: "student" +testificateGroup: "testificate" +builderGroup: "builder" +webhook: "https://discord.com/api/webhooks/XXXX/XXXXXXXXXXXXXXXX" +abandonForgiveness: 6000 +sendFailedTests: true \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index fd01a96..f547a4e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: TrialORE -version: 1.0 +version: 1.1 main: org.openredstone.trialore.TrialOre api-version: 1.20 depend: [LuckPerms]