Files
httprunner/examples/game/llk/solver.go
lilong.129 fb0418fa95 feat: add LianLianKan (连连看) game bot implementation
- Add complete LianLianKan game bot with AI-powered interface analysis
- Implement static analysis solver with 0-2 turn connection algorithms
- Support cross-platform game automation (Android, iOS, HarmonyOS, Browser)
- Include comprehensive test suite with real game data
- Add command line tool and documentation
- Integrate with HttpRunner @/uixt module and Doubao AI models
2025-06-12 17:51:23 +08:00

379 lines
10 KiB
Go

package llk
import (
"fmt"
"github.com/rs/zerolog/log"
)
// LLKSolver represents a LianLianKan puzzle solver
type LLKSolver struct {
board [][]string // Simplified board matrix with element types (immutable)
elements [][]Element // Original elements with coordinates
rows int
cols int
allPairs [][]Element // All possible pairs found in initial state
}
// NewLLKSolver creates a new LianLianKan solver
func NewLLKSolver(gameElement *GameElement) *LLKSolver {
solver := &LLKSolver{
rows: gameElement.Dimensions.Rows,
cols: gameElement.Dimensions.Cols,
}
// Initialize board matrix and elements grid
solver.board = make([][]string, solver.rows)
solver.elements = make([][]Element, solver.rows)
for i := range solver.board {
solver.board[i] = make([]string, solver.cols)
solver.elements[i] = make([]Element, solver.cols)
}
// Populate board and elements from gameElement
// Check if data uses 1-based indexing by looking for any position >= dimensions
// or by checking if position (1,1) exists (common indicator of 1-based indexing)
uses1BasedIndexing := false
for _, element := range gameElement.Elements {
if element.Position.Row > solver.rows || element.Position.Col > solver.cols {
uses1BasedIndexing = true
break
}
// Also check if we have position (1,1) which is common in 1-based systems
if element.Position.Row == 1 && element.Position.Col == 1 {
uses1BasedIndexing = true
break
}
}
for _, element := range gameElement.Elements {
row, col := element.Position.Row, element.Position.Col
// Convert from 1-based to 0-based indexing if data uses 1-based
if uses1BasedIndexing {
row = row - 1
col = col - 1
}
if solver.isValidPosition(row, col) {
solver.board[row][col] = element.Type
// Store original element (keep original 1-based coordinates)
solver.elements[row][col] = element
}
}
return solver
}
// findAllPairs finds all possible pairs that can be connected in the initial state (private method)
func (solver *LLKSolver) FindAllPairs() [][]Element {
var pairs [][]Element
used := make(map[string]bool) // Track used positions
for row1 := 0; row1 < solver.rows; row1++ {
for col1 := 0; col1 < solver.cols; col1++ {
if solver.board[row1][col1] == "" {
continue
}
// Skip if this position is already used
pos1Key := fmt.Sprintf("%d,%d", row1, col1)
if used[pos1Key] {
continue
}
for row2 := 0; row2 < solver.rows; row2++ {
for col2 := 0; col2 < solver.cols; col2++ {
if solver.board[row2][col2] == "" {
continue
}
// Avoid duplicate pairs by ensuring (row1,col1) < (row2,col2)
if row1 > row2 || (row1 == row2 && col1 >= col2) {
continue
}
// Skip if this position is already used
pos2Key := fmt.Sprintf("%d,%d", row2, col2)
if used[pos2Key] {
continue
}
// Validate and add pair only if it passes all checks
if solver.isValidPair(row1, col1, row2, col2) {
element1 := solver.elements[row1][col1]
element2 := solver.elements[row2][col2]
pairs = append(pairs, []Element{element1, element2})
// Mark both positions as used
used[pos1Key] = true
used[pos2Key] = true
// Break out of inner loops since we found a pair for this element
goto nextElement
}
}
}
nextElement:
}
}
solver.allPairs = pairs
return pairs
}
// isValidPosition checks if position is within board boundaries
func (solver *LLKSolver) isValidPosition(row, col int) bool {
return row >= 0 && row < solver.rows && col >= 0 && col < solver.cols
}
// isEmpty checks if position is empty (already eliminated)
func (solver *LLKSolver) isEmpty(row, col int) bool {
return solver.board[row][col] == ""
}
// canConnect checks if two positions can be connected according to LianLianKan rules
func (solver *LLKSolver) canConnect(row1, col1, row2, col2 int) bool {
// Check if positions are valid and contain the same item
if !solver.isValidPosition(row1, col1) ||
!solver.isValidPosition(row2, col2) ||
solver.isEmpty(row1, col1) ||
solver.isEmpty(row2, col2) ||
solver.board[row1][col1] != solver.board[row2][col2] {
return false
}
// Same position
if row1 == row2 && col1 == col2 {
return false
}
// Try direct connection (0 turns)
if solver.canConnectDirect(row1, col1, row2, col2) {
return true
}
// Try one turn connection
if solver.canConnectWithOneTurn(row1, col1, row2, col2) {
return true
}
// Try two turns connection
if solver.canConnectWithTwoTurns(row1, col1, row2, col2) {
return true
}
return false
}
// canConnectHorizontal checks if two points can be connected horizontally
func (solver *LLKSolver) canConnectHorizontal(row, col1, col2 int) bool {
startCol := col1
endCol := col2
if col1 > col2 {
startCol = col2
endCol = col1
}
// Check all positions between start and end (exclusive)
for col := startCol + 1; col < endCol; col++ {
if !solver.isEmpty(row, col) {
return false
}
}
return true
}
// canConnectVertical checks if two points can be connected vertically
func (solver *LLKSolver) canConnectVertical(col, row1, row2 int) bool {
startRow := row1
endRow := row2
if row1 > row2 {
startRow = row2
endRow = row1
}
// Check all positions between start and end (exclusive)
for row := startRow + 1; row < endRow; row++ {
if !solver.isEmpty(row, col) {
return false
}
}
return true
}
// canConnectDirect checks if two points can be connected directly (straight line)
func (solver *LLKSolver) canConnectDirect(row1, col1, row2, col2 int) bool {
// Same row - horizontal connection
if row1 == row2 {
return solver.canConnectHorizontal(row1, col1, col2)
}
// Same column - vertical connection
if col1 == col2 {
return solver.canConnectVertical(col1, row1, row2)
}
return false
}
// canConnectWithOneTurn checks if two points can be connected with one turn (L-shape)
func (solver *LLKSolver) canConnectWithOneTurn(row1, col1, row2, col2 int) bool {
// Try corner at (row1, col2)
corner1Row, corner1Col := row1, col2
if solver.isEmpty(corner1Row, corner1Col) || (corner1Row == row2 && corner1Col == col2) {
if solver.canConnectHorizontal(row1, col1, corner1Col) &&
solver.canConnectVertical(corner1Col, corner1Row, row2) {
return true
}
}
// Try corner at (row2, col1)
corner2Row, corner2Col := row2, col1
if solver.isEmpty(corner2Row, corner2Col) || (corner2Row == row1 && corner2Col == col1) {
if solver.canConnectVertical(col1, row1, corner2Row) &&
solver.canConnectHorizontal(corner2Row, corner2Col, col2) {
return true
}
}
return false
}
// canConnectWithTwoTurns checks if two points can be connected with two turns (Z-shape)
func (solver *LLKSolver) canConnectWithTwoTurns(row1, col1, row2, col2 int) bool {
// Try horizontal first, then vertical, then horizontal (internal paths)
for col := 0; col < solver.cols; col++ {
if col == col1 || col == col2 {
continue
}
if solver.isEmpty(row1, col) && solver.isEmpty(row2, col) &&
solver.canConnectHorizontal(row1, col1, col) &&
solver.canConnectHorizontal(row2, col, col2) &&
solver.canConnectVertical(col, row1, row2) {
return true
}
}
// Try vertical first, then horizontal, then vertical (internal paths)
for row := 0; row < solver.rows; row++ {
if row == row1 || row == row2 {
continue
}
if solver.isEmpty(row, col1) && solver.isEmpty(row, col2) &&
solver.canConnectVertical(col1, row1, row) &&
solver.canConnectVertical(col2, row, row2) &&
solver.canConnectHorizontal(row, col1, col2) {
return true
}
}
// Try boundary connections
// Left boundary connection: go left -> down/up -> right
if solver.canConnectToBoundary(row1, col1, "left") &&
solver.canConnectToBoundary(row2, col2, "left") {
return true
}
// Right boundary connection: go right -> down/up -> left
if solver.canConnectToBoundary(row1, col1, "right") &&
solver.canConnectToBoundary(row2, col2, "right") {
return true
}
// Top boundary connection: go up -> left/right -> down
if solver.canConnectToBoundary(row1, col1, "top") &&
solver.canConnectToBoundary(row2, col2, "top") {
return true
}
// Bottom boundary connection: go down -> left/right -> up
if solver.canConnectToBoundary(row1, col1, "bottom") &&
solver.canConnectToBoundary(row2, col2, "bottom") {
return true
}
return false
}
// canConnectToBoundary checks if a position can connect to a boundary
func (solver *LLKSolver) canConnectToBoundary(row, col int, boundary string) bool {
switch boundary {
case "left":
// Check if we can go horizontally left to column -1 (boundary)
for c := col - 1; c >= 0; c-- {
if !solver.isEmpty(row, c) {
return false
}
}
return true
case "right":
// Check if we can go horizontally right to column solver.cols (boundary)
for c := col + 1; c < solver.cols; c++ {
if !solver.isEmpty(row, c) {
return false
}
}
return true
case "top":
// Check if we can go vertically up to row -1 (boundary)
for r := row - 1; r >= 0; r-- {
if !solver.isEmpty(r, col) {
return false
}
}
return true
case "bottom":
// Check if we can go vertically down to row solver.rows (boundary)
for r := row + 1; r < solver.rows; r++ {
if !solver.isEmpty(r, col) {
return false
}
}
return true
}
return false
}
// isValidPair checks if two positions form a valid pair according to LianLianKan rules
func (solver *LLKSolver) isValidPair(row1, col1, row2, col2 int) bool {
// Check positions are valid
if !solver.isValidPosition(row1, col1) || !solver.isValidPosition(row2, col2) {
return false
}
// Check positions are different
if row1 == row2 && col1 == col2 {
return false
}
// Check board cells are not empty
if solver.board[row1][col1] == "" || solver.board[row2][col2] == "" {
return false
}
// Check element types match and are not empty
if solver.board[row1][col1] != solver.board[row2][col2] || solver.board[row1][col1] == "" {
return false
}
// Check connectivity according to LianLianKan game rules
return solver.canConnect(row1, col1, row2, col2)
}
// printSolution prints all available pairs for debugging
func (solver *LLKSolver) printSolution() {
log.Info().Int("totalPairs", len(solver.allPairs)).
Msg("All pairs validated and ready")
for i, pair := range solver.allPairs {
element1, element2 := pair[0], pair[1]
log.Info().
Int("pair", i+1).
Str("elementType", element1.Type).
Interface("pos1", element1.Position).
Interface("pos2", element2.Position).
Msg("Valid pair")
}
}