mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-10 17:43:00 +08:00
- 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
379 lines
10 KiB
Go
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")
|
|
}
|
|
}
|