python 編寫遊戲測試機器人客戶端 (一)

A_Jian發表於2020-12-08

遊戲測試機器人搭建 - Player Actor

前言:

第一次寫部落格,文筆有限,加上半路出身的遊測,大佬們別往死裡噴,錯的地方請指正,在正式介紹之前先簡單說下框架:Python 的 pykka(Actor 模型),測試框架 pytest 和測試報告 allure。粗略框架介紹

閱讀前的基礎知識

Python 的 socket 程式設計,多程序,多執行緒,佇列

Actors 做什麼

當一個 actor 接收到訊息後,它能做如下三件事中的一件:

  • Create more actors; 建立其他 actors
  • Send messages to other actors; 向其他 actors 傳送訊息
  • Designates what to do with the next message. 指定下一條訊息到來的行為

Actor 的組成

Actor 是由狀態(state)、行為(behavior)、郵箱(mailbox)三者組成的。

  • 狀態(state):狀態是指 actor 物件的變數資訊,狀態由 actor 自身管理,避免併發環境下的鎖和記憶體原子性等問題。
  • 行為(behavior):行為指定的是 actor 中計算邏輯,透過 actor 接收到的訊息來改變 actor 的狀態。
  • 郵箱(mailbox):郵箱是 actor 之間的通訊橋樑,郵箱內部透過 FIFO 訊息佇列來儲存傳送發訊息,而接收方則從郵箱中獲取訊息。

Actors 一大重要特徵在於 actors 之間相互隔離,它們並不互相共享記憶體。這點區別於上述的物件。也就是說,一個 actor 能維持一個私有的狀態,並且這個狀態不可能被另一個 actor 所改變。
具體 Actor 介紹可參考:傳送門 >> JunChow520 的 Actor 模型介紹

pykka 框架的使用

傳送門 >> pykka 使用說明
引用說明書的例子:

# !/usr/bin/env python3
import pykka

GetMessages = object()

class PlainActor(pykka.ThreadingActor):
    def __init__(self):
        super().__init__()
        self.stored_messages = []

    def on_receive(self, message):
        if message is GetMessages:
            return self.stored_messages
        else:
            self.stored_messages.append(message)


if __name__ == '__main__':
    actor = PlainActor.start()
    actor.tell({'no': 'Norway', 'se': 'Sweden'})
    actor.tell({'a': 3, 'b': 4, 'c': 5})
    print(actor.ask(GetMessages))
    actor.stop()

環境

  • Python:3.7
  • Windows/Linux

正文

機器人架構圖

robot

測試機器人要用的 3 個核心 Actor(Player,Send,Recv)

Player Actor

Player 物件初始化

  • 傳遞引數:使用者名稱、伺服器 IP 和 Port、性別、職業,根據專案型別增減;
  • 其他 Actor 的呼叫:
    • Recv Actor:必要的,socket 接收資料,後面章節介紹;
    • Send Actor:必要的,socket 傳送資料,後面章節介紹;
    • 可自由增加其他的 Actor;
  • 初始化可設定角色的相關屬性,後面根據服務端返回資料進行相關賦值,如:角色的 ID,name,資產貨幣,攻擊防禦等 ;
# !/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File    :   player.py    
@Contact :   512759438@qq.com
@Author  :   Jian
'''


import pykka
import websocket
import traceback as tb
from proto import ProtoHandler
from remote import RemoteHandler
from util import MsgSwitch, RecvActor, SendActor


TEST_CASE_CALL = None
class Player(pykka.ThreadingActor):
    def __init__(self, user_name='', server='SERVER_1',sex=1, job=1):
        super(Player, self).__init__()
        self.host = SERVER_LIST[server]['GAME_HOST']
        self.port = SERVER_LIST[server]['GAME_PORT']
        self.web_host = "x.x.x.x"
        self.recv_actor = None
        self.send_actor = None
        self.socket = None
        self.proto_handler = ProtoHandler(self)
        self.remote_handler = RemoteHandler(self)
        '''測試用例執行時需要呼叫player'''
        global TEST_CASE_CALL
        TEST_CASE_CALL = self

        self.player_id = None
        self.state_user_id = 0
        self.state_user_name = user_name
        self.sys_count = 0

封裝訊息傳送,用來給自己傳送訊息或者其它 Actor 呼叫

def send_msg(self, msg_type=None, data=None):
    '''
    :param msg_type:訊息型別 
    :param data: 資料
    '''
    self.actor_ref.tell({
        'msg': msg_type,
        'data': data
    })

Player Actor 例項化之後第一個執行的地方

  • 不指定使用者的話則隨機賬號登入
  • MSG_GUEST_LOGIN: 訊息型別
  • 接下來就把訊息告訴自己,on_receive 會接收到這條訊息
def on_start(self):
    if self.state_user_name is '':
        self.send_msg(MSG_GUEST_LOGIN)
    else:
        self.send_msg(MSG_LOGIN_INFO)

接收訊息及訊息處理

  • 接收的訊息會放進 MsgSwitch 進行引數檢查,MsgSwitch 參考 C 語言 switch
  • 接下來是使用者註冊、創角、登入流程
  • MSG_LOGIN_INFO:獲取玩家的資訊
    • 獲取使用者資訊後通知 MSG_LOGIN
  • MSG_LOGIN:玩家開始連線 socket,有角色則開始登入,沒有角色則通知 MSG_CREATE_PLAYER
    • 連線 socket 後開啟 Send ActorRecv Actor
    • Send Actor 傳送訊息:角色登入需要的引數,Send Actor會進行打包和發給服務端
  • MSG_CREATE_PLAYER:
    • 把需要的創角引數傳給Send Actor,打包後通知服務端要創角
  • MSG_PROTO:Recv Actor 從 socket.recv 接收的資料進行反序列化後會傳送過來到這處理
    • 如果需要對服務端返回的協議資料進行自動處理,可以在 proto 模組寫上對應處理方法,MSG_PROTO 訊息型別接收的每一條協議資料都會去 proto 模組查詢有沒有對應的處理方法 (hasattr 和 getattr)
  • MSG_REMOTE_CMD:後續寫到 remote 再進行結合一起寫,不影響執行
def on_receive(self, msg):
    for case in MsgSwitch(msg):
        # 獲取使用者資訊
        if case(MSG_LOGIN_INFO):
            account_info = Account(self.state_user_name).login_info()
            if account_info['code_str'] == 'OK':
                user_into = account_info['user']
                self.create_player_params  = {
                    'rd3_token': user_into['token'],
                    'rd3_userId': user_into['userId'],
                    'server_list_type': 0,
                    'sid': 1,
                    'token': user_into['token'],
                }
                self.create_player_params.update(Account(self.state_user_name).data)
                self.create_player_params.pop('password')
                self.create_player_params['cmd'] = 'game_login'
                self.send_msg(MSG_LOGIN)
            else:print(f'獲取角色資訊ERROR, 原因: {account_info["code_str"]},{account_info["code"]}')
            break

        # 使用者登入
        if case(MSG_LOGIN):
            self.socket = websocket.create_connection(f'ws://{self.host}:{self.port}/')
            self.recv_actor = RecvActor.start(self, self.socket)
            self.send_actor = SendActor.start(self, self.socket)
            self.send_actor.tell({MSG_PROTO: self.create_player_params})
            break
        # 使用者創角
        if case(MSG_CREATE_PLAYER):
            create_data = {
                'nickname': self.state_user_name,
                'rd3_token': self.create_player_params['rd3_token'],
                'rd3_userId': self.create_player_params['rd3_userId'],
                'sid': self.create_player_params['sid'],
                'token': self.create_player_params['token'],
            }
            self.send_actor.tell({MSG_PROTO: create_data})
            break

        # 服務端返回協議處理
        if case(MSG_PROTO):  
            method, data = msg['data']
            if hasattr(self.proto_handler, method):
                getattr(self.proto_handler, method)(data)
            else:
                print(f"沒有為協議: {method} 定義處理方法, 請前往 proto.py 檔案中定義!")
            break
        # 控制檯呼叫命令
        if case(MSG_REMOTE_CMD):
            method = msg['method']
            method = (type(method) is int and "r" + str(method)) or (type(method) is str and method)
            if hasattr(self.remote_handler, method):
                getattr(self.remote_handler, method)(msg['data'])
            else:
                print(f"沒有為遠端命令: {method} 定義處理方法, 請前往 remote.py 檔案中定義!")
            break

封裝遠端命令

  • 角色登入後可透過 pykka.ActorRegistry.get_by_class_name('Player') 獲取例項物件用遠端命令遙控角色
def remote_msg(self, method:str=None, data=None):
    '''
    呼叫remote裡的方法
    :param method: 方法名
    :param data: 傳入的引數 元組
    '''
    self.actor_ref.tell({
        'msg': MSG_REMOTE_CMD,
        'method': method,
        'data': data
    })

停止 Player Actor

  • 先停止其他的 Actor 再關閉 socket,最後關掉自己
def on_stop(self):
    self.recv_actor.stop()
    self.send_actor.stop()
    self.socket.close()
    self.socket.shutdown()
    self.stop()

log 收集

# 列印報錯訊息
@GetLog(level='fatal')
def on_failure(self, exception_type, exception_value, traceback):
    logging.fatal(f'Player: {self.state_user_name} is down.')
    logging.fatal(f"ErrorType  => {exception_type}")
    logging.fatal(f"ErrorValue => {exception_value}")
    logging.fatal(f"TraceBack  => {tb.print_tb(traceback)}")
    self.on_stop()

後續文章傳送門

python 編寫遊戲測試機器人客戶端 (一)
python 編寫遊戲測試機器人客戶端 (二)
python 編寫遊戲測試機器人客戶端 (三)
python 編寫遊戲測試機器人客戶端 (四)

到這裡 Player Actor 已經寫完了,目前是無法單獨執行的,需要結合後面的 Send Actor 和 Recv Actor 才能執行,寫的不清晰的歡迎在評論留言

最後的最後,各位的關注、點贊、收藏、碎銀子打賞是對我最大的支援,謝謝大家!
需要原始碼的小夥伴關注微信公眾號 ID:gameTesterGz
或掃描二維碼回覆機器人指令碼即可
微信二維碼

相關文章