Skip to content

Commit

Permalink
feat: Quad Link Legacy solver
Browse files Browse the repository at this point in the history
This is built purely off theory and has not undergone any testing.
  • Loading branch information
My-Name-Is-Jeff committed Dec 13, 2024
1 parent 9d04f67 commit de41bd4
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -361,6 +362,7 @@ class Skytils {
PotionEffectTimers,
PricePaid,
ProtectItems,
QuadLinkLegacySolver,
QuiverStuff,
RainTimer,
RandomStuff,
Expand Down
11 changes: 11 additions & 0 deletions src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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<Boolean?>> = 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<Int, Int> {
// 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
}
}
2 changes: 2 additions & 0 deletions src/main/resources/assets/skytils/lang/en_US.lang
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit de41bd4

Please sign in to comment.