From de41bd45fe96c85698ad663d92faec74fefbe872 Mon Sep 17 00:00:00 2001
From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com>
Date: Fri, 13 Dec 2024 12:11:02 -0500
Subject: [PATCH] feat: Quad Link Legacy solver
This is built purely off theory and has not undergone any testing.
---
.../kotlin/gg/skytils/skytilsmod/Skytils.kt | 2 +
.../gg/skytils/skytilsmod/core/Config.kt | 11 +
.../impl/rift/solvers/QuadLinkLegacySolver.kt | 253 ++++++++++++++++++
.../resources/assets/skytils/lang/en_US.lang | 2 +
4 files changed, 268 insertions(+)
create mode 100644 src/main/kotlin/gg/skytils/skytilsmod/features/impl/rift/solvers/QuadLinkLegacySolver.kt
diff --git a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt
index 63955e336..d00eef443 100644
--- a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt
+++ b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt
@@ -52,6 +52,7 @@ import gg.skytils.skytilsmod.features.impl.mining.StupidTreasureChestOpeningThin
import gg.skytils.skytilsmod.features.impl.misc.*
import gg.skytils.skytilsmod.features.impl.overlays.AuctionPriceOverlay
import gg.skytils.skytilsmod.features.impl.protectitems.ProtectItems
+import gg.skytils.skytilsmod.features.impl.rift.solvers.QuadLinkLegacySolver
import gg.skytils.skytilsmod.features.impl.slayer.SlayerFeatures
import gg.skytils.skytilsmod.features.impl.spidersden.RainTimer
import gg.skytils.skytilsmod.features.impl.spidersden.RelicWaypoints
@@ -361,6 +362,7 @@ class Skytils {
PotionEffectTimers,
PricePaid,
ProtectItems,
+ QuadLinkLegacySolver,
QuiverStuff,
RainTimer,
RandomStuff,
diff --git a/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt b/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt
index f824169ba..2318be329 100644
--- a/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt
+++ b/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt
@@ -3418,6 +3418,17 @@ object Config : Vigilant(
)
var petItemConfirmation = false
+ @Property(
+ type = PropertyType.SWITCH, name = "Quad Link Legacy Solver",
+ description = "§b[WIP]§r Solves the Quad Link Legacy (CONNECT4) puzzle.",
+ category = "Rift", subcategory = "Solvers",
+ i18nName = "skytils.config.rift.solvers.quad_link_legacy_solver",
+ i18nCategory = "skytils.config.rift",
+ i18nSubcategory = "skytils.config.rift.solvers",
+ searchTags = ["Wizardman", "Connect4", "ConnectFOUR"],
+ )
+ var quadLinkLegacySolver = false
+
@Property(
type = PropertyType.DECIMAL_SLIDER, name = "Current Revenant RNG Meter",
description = "Internal value to store current Revenant RNG meter",
diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/rift/solvers/QuadLinkLegacySolver.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/rift/solvers/QuadLinkLegacySolver.kt
new file mode 100644
index 000000000..33d1ed938
--- /dev/null
+++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/rift/solvers/QuadLinkLegacySolver.kt
@@ -0,0 +1,253 @@
+/*
+ * Skytils - Hypixel Skyblock Quality of Life Mod
+ * Copyright (C) 2020-2024 Skytils
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package gg.skytils.skytilsmod.features.impl.rift.solvers
+
+import gg.skytils.skytilsmod.Skytils
+import gg.skytils.skytilsmod.Skytils.Companion.mc
+import gg.skytils.skytilsmod.events.impl.GuiContainerEvent
+import gg.skytils.skytilsmod.utils.withAlpha
+import net.minecraft.client.gui.Gui
+import net.minecraft.init.Blocks
+import net.minecraft.init.Items
+import net.minecraft.inventory.ContainerChest
+import net.minecraft.item.Item
+import net.minecraft.item.ItemStack
+import net.minecraftforge.event.world.WorldEvent
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.awt.Color
+
+object QuadLinkLegacySolver {
+ const val guiTitle = "Quad Link Legacy - Wizardman"
+ const val oppSlot = 17
+ const val ourSlot = 24
+
+ // he takes up the middle 7 slots for each row of the chest
+ // this is [row][column]
+ val boardSlots = (0 until 6).map { it*9+1..it*9+7 }
+ val flatBoardSlots = boardSlots.flatten()
+
+ var ourItem: ItemStack? = null
+ var oppItem: ItemStack? = null
+ var bestColumn = -1
+
+ @SubscribeEvent
+ fun onWorldChange(event: WorldEvent.Unload) {
+ if (event.world != mc.theWorld) return
+ reset()
+ }
+
+ @SubscribeEvent
+ fun onGuiContainerEvent(event: GuiContainerEvent) {
+ when (event) {
+ is GuiContainerEvent.CloseWindowEvent -> reset()
+ is GuiContainerEvent.ForegroundDrawnEvent -> {
+ if (!Skytils.config.quadLinkLegacySolver) return
+ val container = event.container as? ContainerChest ?: return
+ if (event.chestName != guiTitle) return
+
+ if (ourItem == null) {
+ ourItem = container.getSlot(ourSlot).stack
+ oppItem = container.getSlot(oppSlot).stack
+
+ check(ourItem != null && oppItem != null) { "Our item or opponent's item is null" }
+ }
+
+ // if null, it means the placing animation is happening
+ // if painting, it means we are waiting for the move
+ // if item stack, it means they are waiting for us
+ // if there is glass, the game is over
+ if (flatBoardSlots.map { container.getSlot(it).stack }.any {
+ it == null ||
+ it.item == Items.painting ||
+ it.item == Item.getItemFromBlock(Blocks.stained_glass) ||
+ !(it.getIsItemStackEqual(ourItem) || it.getIsItemStackEqual(oppItem) || it.item == Items.item_frame)
+ }) {
+ bestColumn = -1
+ return
+ }
+
+ if (bestColumn == -1) {
+ // read the board in
+ for (column in 0 until 7) {
+ for (row in 0 until 6) {
+ val slot = boardSlots[row].elementAt(column)
+ val item = container.getSlot(slot).stack
+ if (item.getIsItemStackEqual(ourItem)) {
+ board[column][row] = true
+ } else if (item.getIsItemStackEqual(oppItem)) {
+ board[column][row] = false
+ } else if (item.item == Items.item_frame) {
+ board[column][row] = null
+ }
+ }
+ }
+
+ val result = negamax(1000, isOurs = true)
+ bestColumn = result.first
+ }
+
+ if (bestColumn != -1) {
+ val topSlot = container.getSlot(bestColumn+1)
+ Gui.drawRect(
+ topSlot.xDisplayPosition,
+ topSlot.yDisplayPosition,
+ topSlot.xDisplayPosition + 16,
+ topSlot.yDisplayPosition + 16 * 6,
+ Color.RED.withAlpha(100)
+ )
+ }
+ }
+ }
+ }
+
+ fun reset() {
+ board.forEach { it.fill(null) }
+ ourItem = null
+ oppItem = null
+ bestColumn = -1
+ }
+
+ /**
+ * board[column][row]
+ * boolean? = null if empty, true if our piece, false if opponent's piece
+ */
+ val board: Array> = Array(7) { arrayOfNulls(6) }
+
+ /**
+ * Makes a move in Connect 4
+ * @return If the move was successful
+ */
+ fun makeMove(column: Int, ourPiece: Boolean): Boolean {
+ check(column in 0 until 7) { "Column must be between 0 and 6" }
+ board[column].forEachIndexed { index, b ->
+ if (b == null) {
+ board[column][index] = ourPiece
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Removes the top piece on a column in Connect 4
+ * @return If the move was successful
+ */
+ fun popMove(column: Int): Boolean {
+ check(column in 0 until 7) { "Column must be between 0 and 6" }
+ for (row in 5 downTo 0) {
+ if (board[column][row] != null) {
+ board[column][row] = null
+ return true
+ }
+ }
+ return false
+ }
+
+
+ /**
+ * @return true if we won, false if opponent won, null if no one won
+ */
+ fun getWinner(): Boolean? {
+ // Check horizontal
+ for (row in 0 until 6) {
+ for (column in 0 until 4) {
+ if (board[column][row] != null &&
+ board[column][row] == board[column + 1][row] &&
+ board[column][row] == board[column + 2][row] &&
+ board[column][row] == board[column + 3][row]
+ ) {
+ return board[column][row]
+ }
+ }
+ }
+
+ // Check vertical
+ for (column in 0 until 7) {
+ for (row in 0 until 3) {
+ if (board[column][row] != null &&
+ board[column][row] == board[column][row + 1] &&
+ board[column][row] == board[column][row + 2] &&
+ board[column][row] == board[column][row + 3]
+ ) {
+ return board[column][row]
+ }
+ }
+ }
+
+ // Check diagonal
+ for (column in 0 until 4) {
+ for (row in 0 until 3) {
+ if (board[column][row] != null &&
+ board[column][row] == board[column + 1][row + 1] &&
+ board[column][row] == board[column + 2][row + 2] &&
+ board[column][row] == board[column + 3][row + 3]
+ ) {
+ return board[column][row]
+ }
+ }
+ }
+
+ for (column in 0 until 4) {
+ for (row in 3 until 6) {
+ if (board[column][row] != null &&
+ board[column][row] == board[column + 1][row - 1] &&
+ board[column][row] == board[column + 2][row - 2] &&
+ board[column][row] == board[column + 3][row - 3]
+ ) {
+ return board[column][row]
+ }
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * @return The best move to make and the score of that move
+ */
+ fun negamax(depth: Int, alpha: Int = Int.MIN_VALUE, beta: Int = Int.MAX_VALUE, isOurs: Boolean): Pair {
+ // TODO: find better base case score
+ if (depth == 0) return -1 to 0
+ val winner = getWinner()
+ if (winner != null) {
+ return -1 to if (winner) depth * 1000 else -depth * 1000
+ }
+
+ var bestScore = Int.MIN_VALUE
+ var a = alpha
+ var bestMove = -1
+
+ for (column in 0 until 7) {
+ if (makeMove(column, isOurs)) {
+ val score = -negamax(depth - 1, -beta, -a, !isOurs).second
+ popMove(column)
+ if (score >= beta) return column to score
+
+ if (score > bestScore) {
+ bestScore = score
+ bestMove = column
+ }
+ a = maxOf(a, score)
+ if (alpha >= beta) break
+ }
+ }
+
+ return bestMove to bestScore
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/skytils/lang/en_US.lang b/src/main/resources/assets/skytils/lang/en_US.lang
index 3d44c5148..427894070 100644
--- a/src/main/resources/assets/skytils/lang/en_US.lang
+++ b/src/main/resources/assets/skytils/lang/en_US.lang
@@ -421,6 +421,7 @@ skytils.config.farming=Farming
skytils.config.kuudra=Kuudra
skytils.config.mining=Mining
skytils.config.pets=Pets
+skytils.config.rift=Rift
skytils.config.slayer=Slayer
skytils.config.sounds=Sounds
skytils.config.spam=Spam
@@ -461,6 +462,7 @@ skytils.config.miscellaneous.minions=Minions
skytils.config.miscellaneous.quality_of_life=Quality of Life
skytils.config.miscellaneous.other=Other
skytils.config.pets.quality_of_life=Quality of Life
+skytils.config.rift.solvers=Solvers
skytils.config.slayer.quality_of_life=Quality of Life
skytils.config.slayer.general=General
skytils.config.slayer.voidgloom_seraph=Voidgloom Seraph