當前位置:
首頁 > 知識 > 跳一跳Python代碼

跳一跳Python代碼

  1. # coding: utf-8
  2. import os
  3. import sys
  4. import subprocess
  5. import shutil
  6. import time
  7. import math
  8. from PIL import Image, ImageDraw
  9. import random
  10. import json
  11. import re
  12. # === 思路 ===
  13. # 核心:每次落穩之後截圖,根據截圖算出棋子的坐標和下一個塊頂面的中點坐標,
  14. # 根據兩個點的距離乘以一個時間係數獲得長按的時間
  15. # 識別棋子:靠棋子的顏色來識別位置,通過截圖發現最下面一行大概是一條直線,就從上往下一行一行遍歷,
  16. # 比較顏色(顏色用了一個區間來比較)找到最下面的那一行的所有點,然後求個中點,
  17. # 求好之後再讓 Y 軸坐標減小棋子底盤的一半高度從而得到中心點的坐標
  18. # 識別棋盤:靠底色和方塊的色差來做,從分數之下的位置開始,一行一行掃描,由於圓形的塊最頂上是一條線,
  19. # 方形的上面大概是一個點,所以就用類似識別棋子的做法多識別了幾個點求中點,
  20. # 這時候得到了塊中點的 X 軸坐標,這時候假設現在棋子在當前塊的中心,
  21. # 根據一個通過截圖獲取的固定的角度來推出中點的 Y 坐標
  22. # 最後:根據兩點的坐標算距離乘以係數來獲取長按時間(似乎可以直接用 X 軸距離)
  23. # TODO: 解決定位偏移的問題
  24. # TODO: 看看兩個塊中心到中軸距離是否相同,如果是的話靠這個來判斷一下當前超前還是落後,便於矯正
  25. # TODO: 一些固定值根據截圖的具體大小計算
  26. # TODO: 直接用 X 軸距離簡化邏輯
  27. def open_accordant_config():
  28. screen_size = _get_screen_size()
  29. config_file = "{path}/config/{screen_size}/config.json".format(
  30. path=sys.path[0],
  31. screen_size=screen_size
  32. )
  33. if os.path.exists(config_file):
  34. with open(config_file, "r") as f:
  35. print("Load config file from {}".format(config_file))
  36. return json.load(f)
  37. else:
  38. with open("{}/config/default.json".format(sys.path[0]), "r") as f:
  39. print("Load default config")
  40. return json.load(f)
  41. def _get_screen_size():
  42. size_str = os.popen("adb shell wm size").read()
  43. m = re.search("(d+)x(d+)", size_str)
  44. if m:
  45. width = m.group(1)
  46. height = m.group(2)
  47. return "{height}x{width}".format(height=height, width=width)
  48. config = open_accordant_config()
  49. # Magic Number,不設置可能無法正常執行,請根據具體截圖從上到下按需設置
  50. under_game_score_y = config["under_game_score_y"]
  51. press_coefficient = config["press_coefficient"] # 長按的時間係數,請自己根據實際情況調節
  52. piece_base_height_1_2 = config["piece_base_height_1_2"] # 二分之一的棋子底座高度,可能要調節
  53. piece_body_width = config["piece_body_width"] # 棋子的寬度,比截圖中量到的稍微大一點比較安全,可能要調節
  54. # 模擬按壓的起始點坐標,需要自動重複遊戲請設置成「再來一局」的坐標
  55. if config.get("swipe"):
  56. swipe = config["swipe"]
  57. else:
  58. swipe = {}
  59. swipe["x1"], swipe["y1"], swipe["x2"], swipe["y2"] = 320, 410, 320, 410
  60. screenshot_backup_dir = "screenshot_backups/"
  61. if not os.path.isdir(screenshot_backup_dir):
  62. os.mkdir(screenshot_backup_dir)
  63. def pull_screenshot():
  64. process = subprocess.Popen("adb shell screencap -p", shell=True, stdout=subprocess.PIPE)
  65. screenshot = process.stdout.read()
  66. if sys.platform == "win32":
  67. screenshot = screenshot.replace(b"
    ", b"
    ")
  68. f = open("autojump.png", "wb")
  69. f.write(screenshot)
  70. f.close()
  71. def backup_screenshot(ts):
  72. # 為了方便失敗的時候 debug
  73. if not os.path.isdir(screenshot_backup_dir):
  74. os.mkdir(screenshot_backup_dir)
  75. shutil.copy("autojump.png", "{}{}.png".format(screenshot_backup_dir, ts))
  76. def save_debug_creenshot(ts, im, piece_x, piece_y, board_x, board_y):
  77. draw = ImageDraw.Draw(im)
  78. # 對debug圖片加上詳細的注釋
  79. draw.line((piece_x, piece_y) + (board_x, board_y), fill=2, width=3)
  80. draw.line((piece_x, 0, piece_x, im.size[1]), fill=(255, 0, 0))
  81. draw.line((0, piece_y, im.size[0], piece_y), fill=(255, 0, 0))
  82. draw.line((board_x, 0, board_x, im.size[1]), fill=(0, 0, 255))
  83. draw.line((0, board_y, im.size[0], board_y), fill=(0, 0, 255))
  84. draw.ellipse((piece_x - 10, piece_y - 10, piece_x + 10, piece_y + 10), fill=(255, 0, 0))
  85. draw.ellipse((board_x - 10, board_y - 10, board_x + 10, board_y + 10), fill=(0, 0, 255))
  86. del draw
  87. im.save("{}{}_d.png".format(screenshot_backup_dir, ts))
  88. def set_button_position(im):
  89. # 將swipe設置為 `再來一局` 按鈕的位置
  90. global swipe_x1, swipe_y1, swipe_x2, swipe_y2
  91. w, h = im.size
  92. left = w / 2
  93. top = 1003 * (h / 1280.0) + 10
  94. swipe_x1, swipe_y1, swipe_x2, swipe_y2 = left, top, left, top
  95. def jump(distance):
  96. press_time = distance * press_coefficient
  97. press_time = max(press_time, 200) # 設置 200 ms 是最小的按壓時間
  98. press_time = int(press_time)
  99. cmd = "adb shell input swipe {x1} {y1} {x2} {y2} {duration}".format(
  100. x1=swipe["x1"],
  101. y1=swipe["y1"],
  102. x2=swipe["x2"],
  103. y2=swipe["y2"],
  104. duration=press_time
  105. )
  106. print(cmd)
  107. os.system(cmd)
  108. # 轉換色彩模式hsv2rgb
  109. def hsv2rgb(h, s, v):
  110. h = float(h)
  111. s = float(s)
  112. v = float(v)
  113. h60 = h / 60.0
  114. h60f = math.floor(h60)
  115. hi = int(h60f) % 6
  116. f = h60 - h60f
  117. p = v * (1 - s)
  118. q = v * (1 - f * s)
  119. t = v * (1 - (1 - f) * s)
  120. r, g, b = 0, 0, 0
  121. if hi == 0: r, g, b = v, t, p
  122. elif hi == 1: r, g, b = q, v, p
  123. elif hi == 2: r, g, b = p, v, t
  124. elif hi == 3: r, g, b = p, q, v
  125. elif hi == 4: r, g, b = t, p, v
  126. elif hi == 5: r, g, b = v, p, q
  127. r, g, b = int(r * 255), int(g * 255), int(b * 255)
  128. return r, g, b
  129. # 轉換色彩模式rgb2hsv
  130. def rgb2hsv(r, g, b):
  131. r, g, b = r/255.0, g/255.0, b/255.0
  132. mx = max(r, g, b)
  133. mn = min(r, g, b)
  134. df = mx-mn
  135. if mx == mn:
  136. h = 0
  137. elif mx == r:
  138. h = (60 * ((g-b)/df) + 360) % 360
  139. elif mx == g:
  140. h = (60 * ((b-r)/df) + 120) % 360
  141. elif mx == b:
  142. h = (60 * ((r-g)/df) + 240) % 360
  143. if mx == 0:
  144. s = 0
  145. else:
  146. s = df/mx
  147. v = mx
  148. return h, s, v
  149. def find_piece_and_board(im):
  150. w, h = im.size
  151. piece_x_sum = 0
  152. piece_x_c = 0
  153. piece_y_max = 0
  154. board_x = 0
  155. board_y = 0
  156. left_value = 0
  157. left_count = 0
  158. right_value = 0
  159. right_count = 0
  160. from_left_find_board_y = 0
  161. from_right_find_board_y = 0
  162. scan_x_border = int(w / 8) # 掃描棋子時的左右邊界
  163. scan_start_y = 0 # 掃描的起始y坐標
  164. im_pixel=im.load()
  165. # 以50px步長,嘗試探測scan_start_y
  166. for i in range(int(h / 3), int( h*2 /3 ), 50):
  167. last_pixel = im_pixel[0,i]
  168. for j in range(1, w):
  169. pixel=im_pixel[j,i]
  170. # 不是純色的線,則記錄scan_start_y的值,準備跳出循環
  171. if pixel[0] != last_pixel[0] or pixel[1] != last_pixel[1] or pixel[2] != last_pixel[2]:
  172. scan_start_y = i - 50
  173. break
  174. if scan_start_y:
  175. break
  176. print("scan_start_y: ", scan_start_y)
  177. # 從scan_start_y開始往下掃描,棋子應位於屏幕上半部分,這裡暫定不超過2/3
  178. for i in range(scan_start_y, int(h * 2 / 3)):
  179. for j in range(scan_x_border, w - scan_x_border): # 橫坐標方面也減少了一部分掃描開銷
  180. pixel = im_pixel[j,i]
  181. # 根據棋子的最低行的顏色判斷,找最後一行那些點的平均值,這個顏色這樣應該 OK,暫時不提出來
  182. if (50 < pixel[0] < 60) and (53 < pixel[1] < 63) and (95 < pixel[2] < 110):
  183. piece_x_sum += j
  184. piece_x_c += 1
  185. piece_y_max = max(i, piece_y_max)
  186. if not all((piece_x_sum, piece_x_c)):
  187. return 0, 0, 0, 0
  188. piece_x = piece_x_sum / piece_x_c
  189. piece_y = piece_y_max - piece_base_height_1_2 # 上移棋子底盤高度的一半
  190. for i in range(int(h / 3), int(h * 2 / 3)):
  191. last_pixel = im_pixel[0, i]
  192. # 計算陰影的RGB值,通過photoshop觀察,陰影部分其實就是背景色的明度V 乘以0.7的樣子
  193. h, s, v = rgb2hsv(last_pixel[0], last_pixel[1], last_pixel[2])
  194. r, g, b = hsv2rgb(h, s, v * 0.7)
  195. if from_left_find_board_y and from_right_find_board_y:
  196. break
  197. if not board_x:
  198. board_x_sum = 0
  199. board_x_c = 0
  200. for j in range(w):
  201. pixel = im_pixel[j,i]
  202. # 修掉腦袋比下一個小格子還高的情況的 bug
  203. if abs(j - piece_x) < piece_body_width:
  204. continue
  205. # 修掉圓頂的時候一條線導致的小 bug,這個顏色判斷應該 OK,暫時不提出來
  206. if abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2]) > 10:
  207. board_x_sum += j
  208. board_x_c += 1
  209. if board_x_sum:
  210. board_x = board_x_sum / board_x_c
  211. else:
  212. # 繼續往下查找,從左到右掃描,找到第一個與背景顏色不同的像素點,記錄位置
  213. # 當有連續3個相同的記錄時,表示發現了一條直線
  214. # 這條直線即為目標board的左邊緣
  215. # 然後當前的 y 值減 3 獲得左邊緣的第一個像素
  216. # 就是頂部的左邊頂點
  217. for j in range(w):
  218. pixel = im_pixel[j, i]
  219. # 修掉腦袋比下一個小格子還高的情況的 bug
  220. if abs(j - piece_x) < piece_body_width:
  221. continue
  222. if (abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2])
  223. > 10) and (abs(pixel[0] - r) + abs(pixel[1] - g) + abs(pixel[2] - b) > 10):
  224. if left_value == j:
  225. left_count = left_count+1
  226. else:
  227. left_value = j
  228. left_count = 1
  229. if left_count > 3:
  230. from_left_find_board_y = i - 3
  231. break
  232. # 邏輯跟上面類似,但是方向從右向左
  233. # 當有遮擋時,只會有一邊有遮擋
  234. # 算出來兩個必然有一個是對的
  235. for j in range(w)[::-1]:
  236. pixel = im_pixel[j, i]
  237. # 修掉腦袋比下一個小格子還高的情況的 bug
  238. if abs(j - piece_x) < piece_body_width:
  239. continue
  240. if (abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2])
  241. > 10) and (abs(pixel[0] - r) + abs(pixel[1] - g) + abs(pixel[2] - b) > 10):
  242. if right_value == j:
  243. right_count = left_count + 1
  244. else:
  245. right_value = j
  246. right_count = 1
  247. if right_count > 3:
  248. from_right_find_board_y = i - 3
  249. break
  250. # 如果頂部像素比較多,說明圖案近圓形,相應的求出來的值需要增大,這裡暫定增大頂部寬的三分之一
  251. if board_x_c > 5:
  252. from_left_find_board_y = from_left_find_board_y + board_x_c / 3
  253. from_right_find_board_y = from_right_find_board_y + board_x_c / 3
  254. # 按實際的角度來算,找到接近下一個 board 中心的坐標 這裡的角度應該是30°,值應該是tan 30°,math.sqrt(3) / 3
  255. board_y = piece_y - abs(board_x - piece_x) * math.sqrt(3) / 3
  256. # 從左從右取出兩個數據進行對比,選出來更接近原來老演算法的那個值
  257. if abs(board_y - from_left_find_board_y) > abs(from_right_find_board_y):
  258. new_board_y = from_right_find_board_y
  259. else:
  260. new_board_y = from_left_find_board_y
  261. if not all((board_x, board_y)):
  262. return 0, 0, 0, 0
  263. return piece_x, piece_y, board_x, new_board_y
  264. def dump_device_info():
  265. size_str = os.popen("adb shell wm size").read()
  266. device_str = os.popen("adb shell getprop ro.product.model").read()
  267. density_str = os.popen("adb shell wm density").read()
  268. print("如果你的腳本無法工作,上報issue時請copy如下信息:
    **********
  269. Screen: {size}
    Density: {dpi}
    DeviceType: {type}
    OS: {os}
    Python: {python}
    **********".format(
  270. size=size_str.strip(),
  271. type=device_str.strip(),
  272. dpi=density_str.strip(),
  273. os=sys.platform,
  274. python=sys.version
  275. ))
  276. def check_adb():
  277. flag = os.system("adb devices")
  278. if flag == 1:
  279. print("請安裝ADB並配置環境變數")
  280. sys.exit()
  281. def main():
  282. h, s, v = rgb2hsv(201, 204, 214)
  283. print(h, s, v)
  284. r, g, b = hsv2rgb(h, s, v*0.7)
  285. print(r, g, b)
  286. dump_device_info()
  287. check_adb()
  288. while True:
  289. pull_screenshot()
  290. im = Image.open("./autojump.png")
  291. # 獲取棋子和 board 的位置
  292. piece_x, piece_y, board_x, board_y = find_piece_and_board(im)
  293. ts = int(time.time())
  294. print(ts, piece_x, piece_y, board_x, board_y)
  295. set_button_position(im)
  296. jump(math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2))
  297. save_debug_creenshot(ts, im, piece_x, piece_y, board_x, board_y)
  298. backup_screenshot(ts)
  299. time.sleep(random.uniform(1.2, 1.4)) # 為了保證截圖的時候應落穩了,多延遲一會兒
  300. if __name__ == "__main__":
  301. main()

跳一跳Python代碼

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

IDEA 解決代碼提示功能消失
scrollView滾動視圖實現商城模塊(附代碼)

TAG:程序員小新人學習 |