mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-06 20:32:44 +08:00
256 lines
8.1 KiB
Go
256 lines
8.1 KiB
Go
package llk
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"path/filepath"
|
||
|
||
hrp "github.com/httprunner/httprunner/v5"
|
||
"github.com/httprunner/httprunner/v5/code"
|
||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||
"github.com/httprunner/httprunner/v5/internal/config"
|
||
"github.com/httprunner/httprunner/v5/uixt"
|
||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||
"github.com/rs/zerolog/log"
|
||
)
|
||
|
||
// GameElement represents a game element detected in the interface
|
||
type GameElement struct {
|
||
Content string `json:"content"` // Human-readable description
|
||
Thought string `json:"thought"` // AI reasoning process
|
||
Dimensions Dimensions `json:"dimensions"` // Grid dimensions
|
||
Elements []Element `json:"elements"` // Game elements detected
|
||
}
|
||
|
||
// Dimensions represents grid dimensions
|
||
type Dimensions struct {
|
||
Rows int `json:"rows"` // Number of rows
|
||
Cols int `json:"cols"` // Number of columns
|
||
}
|
||
|
||
// Element represents a single game element
|
||
type Element struct {
|
||
Type string `json:"type"` // Element type/name
|
||
Position Position `json:"position"` // Position in grid
|
||
}
|
||
|
||
// Position represents grid position
|
||
type Position struct {
|
||
Row int `json:"row"` // Row index (0-based)
|
||
Col int `json:"col"` // Column index (0-based)
|
||
}
|
||
|
||
// LLKGameBot represents the main bot for playing LianLianKan game
|
||
type LLKGameBot struct {
|
||
*hrp.UIXTRunner
|
||
|
||
analyzeIndex int
|
||
}
|
||
|
||
// NewLLKGameBot creates a new LianLianKan game bot
|
||
func NewLLKGameBot(platform string, serial string) (*LLKGameBot, error) {
|
||
// Create driver cache config
|
||
config := hrp.UIXTConfig{
|
||
DriverCacheConfig: uixt.DriverCacheConfig{
|
||
Platform: platform,
|
||
Serial: serial,
|
||
AIOptions: []option.AIServiceOption{
|
||
option.WithCVService(option.CVServiceTypeVEDEM),
|
||
option.WithLLMConfig(
|
||
option.NewLLMServiceConfig(option.DOUBAO_1_5_UI_TARS_250328).
|
||
WithQuerierModel(option.DOUBAO_SEED_1_6_250615),
|
||
),
|
||
},
|
||
},
|
||
}
|
||
uixtRunner, err := hrp.NewUIXTRunner(&config)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to create session runner: %w", err)
|
||
}
|
||
bot := &LLKGameBot{
|
||
UIXTRunner: uixtRunner,
|
||
analyzeIndex: 0,
|
||
}
|
||
|
||
log.Info().Msg("LianLianKan game bot initialized successfully")
|
||
return bot, nil
|
||
}
|
||
|
||
func (bot *LLKGameBot) EnterGame(ctx context.Context) error {
|
||
_, err := bot.Session.RunStep(
|
||
hrp.NewStep("进入游戏").
|
||
Android().StartToGoal(
|
||
"启动抖音,搜索「连了又连」小游戏,并启动游戏",
|
||
),
|
||
)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to enter game: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// AnalyzeGameInterface analyzes the game interface and extracts element information
|
||
func (bot *LLKGameBot) AnalyzeGameInterface() (*GameElement, error) {
|
||
bot.analyzeIndex++
|
||
query := `Analyze this LianLianKan (连连看) game interface and provide structured information about:
|
||
1. Grid dimensions (rows and columns)
|
||
2. All game elements with their positions and types`
|
||
|
||
// Query the AI model
|
||
result, err := bot.DriverExt.AIQuery(query,
|
||
option.WithOutputSchema(GameElement{}))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to query AI model: %w", err)
|
||
}
|
||
|
||
// Convert result to GameElement
|
||
gameElement, err := convertToGameElement(result)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to convert query result to GameElement: %w", err)
|
||
}
|
||
|
||
// Save debug data
|
||
gameElementsPath := filepath.Join(config.GetConfig().ResultsPath(),
|
||
fmt.Sprintf("game_elements_%d.json", bot.analyzeIndex))
|
||
if err := builtin.Dump2JSON(gameElement, gameElementsPath); err != nil {
|
||
log.Error().Err(err).Msg("failed to dump game elements data")
|
||
} else {
|
||
log.Info().Str("gameElementsPath", gameElementsPath).Msg("dumped game elements data")
|
||
}
|
||
|
||
return gameElement, nil
|
||
}
|
||
|
||
// convertToGameElement converts AI execution result to GameElement
|
||
func convertToGameElement(result *uixt.AIExecutionResult) (*GameElement, error) {
|
||
if result == nil {
|
||
return nil, fmt.Errorf("AI execution result is nil")
|
||
}
|
||
|
||
if result.QueryResult == nil {
|
||
return nil, fmt.Errorf("query result is nil in AI execution result")
|
||
}
|
||
queryResult := result.QueryResult
|
||
|
||
// Try direct conversion first
|
||
if gameElement, ok := queryResult.Data.(*GameElement); ok {
|
||
return gameElement, nil
|
||
}
|
||
|
||
// Convert to JSON and back for flexible parsing
|
||
var gameElement GameElement
|
||
var sourceData interface{}
|
||
|
||
// Use Data if available, otherwise try Content
|
||
if queryResult.Data != nil {
|
||
sourceData = queryResult.Data
|
||
} else if queryResult.Content != "" {
|
||
var contentData map[string]interface{}
|
||
if err := json.Unmarshal([]byte(queryResult.Content), &contentData); err != nil {
|
||
return nil, fmt.Errorf("failed to parse JSON from Content: %w", err)
|
||
}
|
||
sourceData = contentData
|
||
} else {
|
||
return nil, fmt.Errorf("no data available in query result")
|
||
}
|
||
|
||
// Convert via JSON marshaling/unmarshaling
|
||
jsonBytes, err := json.Marshal(sourceData)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to marshal result data: %w", err)
|
||
}
|
||
|
||
if err := json.Unmarshal(jsonBytes, &gameElement); err != nil {
|
||
return nil, fmt.Errorf("failed to unmarshal to GameElement: %w", err)
|
||
}
|
||
|
||
return &gameElement, nil
|
||
}
|
||
|
||
// SolveGame finds all possible pairs in the initial game state
|
||
func (bot *LLKGameBot) SolveGame(gameElement *GameElement) ([][]Element, error) {
|
||
// Create solver instance
|
||
solver := NewLLKSolver(gameElement)
|
||
// Get all possible pairs from initial state (already validated)
|
||
allPairs := solver.FindAllPairs()
|
||
|
||
log.Info().Int("pairs", len(allPairs)).Msg("Found all valid pairs (passed game rules validation)")
|
||
|
||
// Print solution details
|
||
solver.printSolution()
|
||
|
||
return allPairs, nil
|
||
}
|
||
|
||
// Play analyze game interface and solve game, then execute all clicks in sequence
|
||
func (bot *LLKGameBot) Play() error {
|
||
// Analyze current screen
|
||
gameElement, err := bot.AnalyzeGameInterface()
|
||
if err != nil {
|
||
log.Fatal().Err(err).Msg("Failed to analyze game interface")
|
||
}
|
||
|
||
// Solve game
|
||
clickSequence, err := bot.SolveGame(gameElement)
|
||
if err != nil {
|
||
log.Fatal().Err(err).Msg("Failed to solve game")
|
||
}
|
||
|
||
systemPrompt := `连连看是一款经典的益智消除类小游戏,通常以图案或图标为主要元素。以下是连连看的基本规则说明:
|
||
1. 游戏目标: 玩家需要通过连接相同的图案或图标,将它们从游戏界面中消除。
|
||
2. 连接规则:
|
||
- 两个相同的图案可以通过不超过三条直线连接。
|
||
- 连接线可以水平或垂直,但不能斜线,也不能跨过其他图案。
|
||
- 连接线的转折次数不能超过两次。
|
||
3. 游戏界面:
|
||
- 游戏界面是一个矩形区域,内含多个图案或图标,排列成行和列;图案或图标在未选中状态下背景为白色,选中状态下背景为绿色。
|
||
- 游戏界面下方是道具区域,共有 3 种道具,从左到右分别是:「高亮显示」、「随机打乱」、「减少种类」。
|
||
4、游戏攻略:
|
||
- 游戏失败后,可观看广告视频,待屏幕右上角出现「领取成功」后,点击其右侧的 X 即可关闭广告,继续游戏
|
||
|
||
请严格按照以上游戏规则,仅完成如下2个相同图标的点击,完成后即结束,等待下一次任务:
|
||
`
|
||
|
||
// Execute all clicks in sequence
|
||
for _, pair := range clickSequence {
|
||
prompt := fmt.Sprintf("点击连连看游戏界面上的 2 个相同图标 %s,坐标序列分别为 %+v, %+v",
|
||
pair[0].Type, pair[0].Position, pair[1].Position)
|
||
log.Info().Msg(prompt)
|
||
|
||
_, err := bot.Session.RunStep(
|
||
hrp.NewStep("").
|
||
Android().StartToGoal(
|
||
systemPrompt+prompt, option.WithMaxRetryTimes(2),
|
||
),
|
||
)
|
||
if err != nil && !errors.Is(err, code.MaxRetryError) {
|
||
log.Error().Err(err).Msg("Failed to click game interface")
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (bot *LLKGameBot) GenerateReport() error {
|
||
return bot.Session.GenerateReport()
|
||
}
|
||
|
||
// Close cleans up resources
|
||
func (bot *LLKGameBot) Close() error {
|
||
if bot.DriverExt != nil {
|
||
if err := bot.DriverExt.DeleteSession(); err != nil {
|
||
log.Warn().Err(err).Msg("Warning: failed to delete driver session")
|
||
}
|
||
// Release driver from cache
|
||
serial := bot.DriverExt.GetDevice().UUID()
|
||
if err := uixt.ReleaseXTDriver(serial); err != nil {
|
||
log.Warn().Err(err).Msg("Warning: failed to release driver")
|
||
}
|
||
}
|
||
log.Info().Msg("LianLianKan game bot closed")
|
||
return nil
|
||
}
|