# improvement todos
# + use faster OCR
# + find ground line
# - and all tanks on it
# + simulate underground path
# + simulate bounding (game edges
# - and pink obstacles)
# - auto-scale based on game window size
# - calculate high and low hit paths automatically
# - simulate bouncing weps (only on flat ground yet)
# OR
# - based on current shot settings, adapt to hit nearest enemy tank
BENCH_OCR = False#True
OCRS = ["paddleocr", "winrt", "tesseract"]
NO_PP = True # paddle is very robust and doesn't need it, preprocessing can fail, too
import math
import sys
import re
import getopt
import os
import time
from timeit import default_timer as timer
import threading
from tkinter import *
# ocr
# keras won't run and conflicts with paddle
if "doctr" in OCRS: # very slow: >4s
from doctr.models import ocr_predictor # pip install python-doctr
model = ocr_predictor(pretrained=True)
from doctr.io import DocumentFile
if "tesseract" in OCRS: # slow: >200ms
import pytesseract # pip install pytesseract
pytesseract.pytesseract.tesseract_cmd = r'C:\Storage\Tools\Tesseract-OCR\tesseract.exe' # xxx edit
if "winrt" in OCRS: # very fast: 12ms
from screen_ocr import Reader # pip install screen-ocr[winrt]
ocr_reader = Reader.create_quality_reader()
if "easyocr" in OCRS: # slow: >300ms
import easyocr # pip install easyocr
easy_reader = easyocr.Reader(['en'])
if "paddleocr" in OCRS: # fast and accurate: 60ms
from paddleocr import PaddleOCR # pip install paddlepaddle, paddleocr
from numpy import asarray
paddle_ocr = PaddleOCR(lang='en', show_log=False)
import logging
from paddleocr.ppocr.utils.logging import get_logger as ppocr_get_logger
ppocr_get_logger().setLevel(logging.ERROR)
from PIL import ImageTk
import PIL.ImageOps
# screenshot
import pyautogui
# windows
import ctypes
from ctypes.wintypes import HWND, DWORD, RECT
def GetWindowRectFromName(name:str)-> tuple:
hwnd = ctypes.windll.user32.FindWindowW(0, name)
rect = ctypes.wintypes.RECT()
try:
ctypes.windll.user32.GetWindowRect(hwnd, ctypes.pointer(rect))
return (rect.left+9, rect.top, rect.right-8, rect.bottom-11)
except:
return (0,0,1882,1421)
gcoord = GetWindowRectFromName('ShellShock Live')
# Options
xoffs,yoffs,x2,y2 = gcoord
width = x2-xoffs
height = y2-yoffs
verbose = False
auto = True # auto now default on
def printhelp():
print(os.path.basename(__file__)+' [-x OFFSET] [-y OFFSET] [-v|--verbose] [-a|--noauto]')
# print("MODES:\n - 0|1: default trajectory\n - 2: slow projectile\n - 3: hover ball\n - 4: straight\n - 5: 3d bomb")
try:
opts, args = getopt.getopt(sys.argv[1:], "hax:y:v", ["verbose","auto"])
except getopt.GetoptError:
printhelp()
sys.exit()
# print(opts)
for opt, arg in opts:
if opt == '-h':
printhelp()
sys.exit()
elif opt in ("-x"):
xoffs = int(arg)
width = 1882 # fixed to yoga window size
elif opt in ("-y"):
yoffs = int(arg)
height = 1421 # fixed
elif opt in ("-v", "--verbose"):
verbose = True
elif opt in ("-a", "--noauto"):
auto = False
# Config
GUI_SIZE = "950x800" # init value, size changes are stored to cfg file
WINDOW_OFFS = (xoffs, yoffs) # for moved window
IMG_SCALE = 2.0 # scale output down
SHOW_CROPS = verbose # show cropped img for wind and pa
GAME_CRD = (WINDOW_OFFS[0], WINDOW_OFFS[1], width+WINDOW_OFFS[0], height+WINDOW_OFFS[1]) # game window
# size-dependent
# todo find on scaled window
PLAY_CRD = (5, 180, width-5, height-(1421-1270)) # playing field
WEP_CRD = (700-5, 1375, 999, 1405) # weapon name
FIRE_CRD = (1070, 1300, 1070+1, 1300+1) # a pixel in fire button
WIND_CRD = (924, 143, 958, 167) # wind speed - this is super sensitive to window x displacements!!!
WINDR_X = 979 # wind dir indicators
WINDL_X = 902
WIND_Y = 152
TANKY_OFFS = 2 # center of shot offs to found tank center
BARREL_LENGTH = 21.0
BARREL_LENGTH_Y = 21.0 # todo get rid of different y
BOUNDER_HEIGHT = 218 # side-bounders
A_GND_RANGE = 20 # pixel to scan left&right to find gnd angle
# unscaled values
BOMB_T = 194
HOVER_T = 155 # ca. 0.8s
MAX_T = 2000 # max. flight simulation time ca 10s
BOOMERANG_F = 486.0 # pseudo wind factor for boomerang weps
RAM_F = 4.9 # grav increase on apex for battering weps
BOUNCE_FACTOR = (0.31,0.51,0.71,0.81) # reduction of bouncing wep speed, (normal, med, high, ultra-bounce); bouncing is experimental - stone-decoration makes ground-detection imprecise, bouncing is only correct on flat ground
# ballistic parameters
SF = 0.04885#0.04222 # speed factor
WF = 0.0000128#sf/8.12*power/100.0 # wind acc factor
GACC = 9.803/1000.0 # earth acceleration
SCAN_WEP = True # OCR wep name
COLOR = [(255, 255, 255), (200, 200, 255)] # trajectory color right;left
AUTO = auto
PRINT = not auto
# read weapon list
weps = []
try:
for l in open("weps.txt").readlines():
weps.append(l.strip())
except:
print("Weapon list wep.txt not found")
WEAPONS = weps
#Create an instance of tkinter frame
win = Tk()
win.title("Shellshock Simulator")
#Set the geometry of tkinter frame
CONFIG_FILE = os.path.basename(__file__)+".conf"
if os.path.isfile(CONFIG_FILE):
#Here I read the X and Y positon of the window from when I last closed it.
with open(CONFIG_FILE, "r") as conf:
win.geometry(conf.read())
else:
#Default window position.
win.geometry(GUI_SIZE)
wind = StringVar()
power = StringVar()
angle = StringVar()
bfactor = DoubleVar()
bfactor.set(0.3)
subwep = IntVar()
bounce = IntVar()
bound = IntVar()
autowep = IntVar()
override = IntVar()
md = IntVar() # mode
md.set(0)
autowep.set(1)
sfv = DoubleVar()
sfv.set(SF)
wfv = DoubleVar()
wfv.set(WF)
gaccv = DoubleVar()
gaccv.set(GACC)
def ocr_winrt(im):
return ocr_reader.read_image(im).as_string()
def ocr_tesser(img, cfg='--psm 7 -c tessedit_char_whitelist=0123456789-,'):
return pytesseract.image_to_string(img, config=cfg)
def ocr_easy(im):
im.save('temp.png')
result = easy_reader.readtext('temp.png')
r = ""
for (bbox, text, prob) in result:
r += text
return r
def ocr_paddle(im):
numpydata = asarray(im)
result = paddle_ocr.ocr(numpydata)
r = ""
for idx in range(len(result)):
res = result[idx]
try:
for line in res:
r += line[1][0]
except:
pass#print(res) # reader failed; returned None
return r
def ocr_doctr(im):
im.save('temp.png')
doc = DocumentFile.from_images("temp.png")
result = model(doc)
print(result)
return ""
def ocr(im, method=OCRS[0], cfg=''):
if method == "paddleocr":
return ocr_paddle(im)
elif method == "winrt":
return ocr_winrt(im)
elif method == "tesseract":
return ocr_tesser(im, cfg)
elif method == "easyocr":
return ocr_easy(im)
elif method == "doctr":
return ocr_doctr(im)
return ""
def distance(p1, p2):
return math.sqrt((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)
def preprocess(img):
if NO_PP:
return img
size_x, size_y = img.size
img = img.resize((size_x*2, size_y*2), 3)
# img = PIL.ImageOps.invert(img)
# if SHOW_CROPS:
# img.show()
# img = img.resize(((pow_rect[2]-pow_rect[0])*2, (pow_rect[3]-pow_rect[1])*2), Image.ANTIALIAS)
pixdata = img.load()
d = 16
for y in range(0, img.size[1]):
for x in range(0, img.size[0]):
r, g, b = pixdata[x, y]
#splitp = 120
#repl = 200
#if (r in range(0, splitp)) or (g in range(0, splitp)) or (b in range(0, splitp)):
# if (r in range(splitp,255)) and (g in range(splitp,255)) and (b in range(splitp,255)):
if r in range(b-d, b+d) and g in range(b-d, b+d):# and g>150:# (r in range(0, splitp)) or (g in range(0, splitp)) or (b in range(0, splitp)):#b > g*1.5 and r > g*1.5:#g > 160:#
pixdata[x, y] = (b//2, b//2, b//2)
else:
pixdata[x, y] = (0, 0, 0)
#~ if (r in range(splitp, 256)) and (g in range(splitp, 256)) and (b in range(splitp, 256)):
#~ pixdata[x, y] = (255, 255, 255)
return PIL.ImageOps.invert(img)
def postprocess(pa):
try:
p, a = re.split('[^0123456789-]+', pa.strip())
if re.search(r"\d+", p.strip()) and re.search(r"\d+", a.strip()): # check if output is 2 numbers
return (p, a)
except:
pass
return ""
img_pa = ""
def get_pow_ang(im, pow_rect):
global img_pa
img = im.crop(pow_rect)#.convert('L')#.resize()#.invert()
img = preprocess(img)
if SHOW_CROPS:
img.show()
img_pa = ImageTk.PhotoImage(img)
pa = ocr(img)
return pa
try:
p, a = re.split('[^0123456789-]+', pa.strip())
if re.search(r"\d+", p.strip()) and re.search(r"\d+", a.strip()): # check if output is 2 numbers
return pa
except:
pass
return ""
def get_pow_ang_t(im, pow_rect, ocr_method = "tesseract"):
global img_pa
img = im.crop(pow_rect)
img = preprocess(img)
if SHOW_CROPS:
img.show()
img_pa = ImageTk.PhotoImage(img)
pow_ang = ocr(img, ocr_method)#pytesseract.image_to_string(img, config='--psm 7 -c tessedit_char_whitelist=0123456789-,')
return pow_ang
def find_pa(im, tx, ty):
im_ = im.load()
limit = 210
px = []
for x in range(tx+30, tx+130):
try:
r,g,b = im_[x, ty+5]
except:
r,g,b = 0,0,0
if r>limit and g>limit and b>limit:
px.append(x)
if len(px) > 2: # found on right side
return (min(px)-10, ty-10, max(px)+10, ty+30)
for x in range(tx-130, tx-30):
try:
r,g,b = im_[x, ty+5]
except:
r,g,b = 0,0,0
if r>limit and g>limit and b>limit:
px.append(x)
if len(px) > 2: # found on left side
return (min(px)-10, ty-10, max(px)+10, ty+30)
return (tx-64, ty+25, tx+64, ty+70) # extended x bc on edges the display jumps a bit to the middle
img_wind = ""
def get_wind(im, wind_rect):
global img_wind
img = im.crop(wind_rect)
if SHOW_CROPS:
img.show()
img_wind = ImageTk.PhotoImage(img)
dircheck = im.load()
splitp = 90
cnt = 0
sign = 0
# print('sign:')
# print(dircheck[WINDL_X, WIND_Y])
r, g, b = dircheck[WINDL_X, WIND_Y]
if (r in range(0, splitp)) or (g in range(0, splitp)) or (b in range(0, splitp)): # black -> no arrow
sign = 1
cnt += 1
# print(dircheck[WINDR_X, WIND_Y])
r, g, b = dircheck[WINDR_X, WIND_Y]
if (r in range(0, splitp)) or (g in range(0, splitp)) or (b in range(0, splitp)):
sign = -1
cnt += 1
if cnt == 2: # no arrow seen
if PRINT:
print("no wind")
return 0
for o in OCRS:
w = ocr(img, o)
try:
if w != "":
w = int(w)
return sign*int(w)
except:
pass
if PRINT:
print("failed to read wind")
return 0
def calc_dir(gnd, x, wx):
dir = 0
try:
if gnd[wx] < gnd[x]: # smaller means up
dir = 1
if gnd[wx] > gnd[x]:
dir = -1
except:
pass
return dir
def calc_gnd_angle(gnd, x):
wx = x
if x > 1: # walk backwards for N pixels as long as they are monotonous
wx = x-1
dir = calc_dir(gnd, x, wx)
while wx>1 and x-wx < A_GND_RANGE and calc_dir(gnd, wx, wx-1) in [0,dir]:
wx -= 1
leftx = wx
wx = x
if x < len(gnd)-1: # walk forwards for N pixels as long as they are monotonous
wx = x+1
dir = calc_dir(gnd, x, wx)
while wx<len(gnd)-1 and wx-x < A_GND_RANGE and calc_dir(gnd, wx, wx+1) in [0,dir]:
wx += 1
rightx = wx
dx = rightx - leftx
dy = gnd[rightx] - gnd[leftx]
a = math.atan2(-dy, dx)
if a >= math.pi/2.0:
a -= math.pi
return (a, dx, dy)
def calc_refl_angle(a, v0x, v0y):
v1x = v0x*math.cos(2*a) + v0y*math.sin(2*a)
v1y = v0x*math.sin(2*a) - v0y*math.cos(2*a)
return (v1x, v1y)
img = "" # keep img reference alive
def sim_shot(im, x, y, power, angle, wind, weapon, gnd):
global img
slow_weps = ('Baby Seagull', 'Mama Seagull', 'Seagull', 'BFG-1000', 'BFG-9000', 'Chopper', 'Apache')
straight_weps = ('AK-47', 'M4', 'M16', 'Glock', 'M9', 'Desert Eagle', 'UZI', 'MP5', 'P90', 'Laser Beam', 'Great Beam', 'Ultra Beam', 'Master Beam', 'Flintlock', 'Blunderbuss', 'Fat Stacks', 'Dolla Bills', 'Make-it-Rain')
submersive_weps = ('Sub-Sniper', 'Tunneler', 'Mega-Tunneler', 'Torpedoes', '3D-Bomb', 'Rainbow', 'MegaRainbow', 'Dead Riser', 'Yin Yang', 'Yin Yang Yong', 'Penetrator', 'Penetrator v2', 'Sausage', 'Bratwurst', 'Kielbasa')
noprojectile_weps = ('Earthquake', 'Mega-Quake', 'Shank', 'Dagger', 'Sword', 'Attractoids', 'Shockwave', 'Sonic Pulse', 'Rampage', 'Riot', 'Imploder', 'Ultimate Imploder', 'ShockShell', 'ShockShell Trio')
hover_wep = ('Hover-Ball', 'Heavy Hover-Ball')
drei_d_wep = ('3D-Bomb', '2x3D', '3x3D') # wind affects those, too!
boomerang_wep = ('Boomerang', 'BigBoomerang')
taser_wep = ('Taser', 'Heavy Taser')
ram_wep = ('Battering Ram', 'Double Ram', 'Ram-Squad', 'Double Ram-Squad')
ultra_bounce_wep = ('Snowball','Bounsplode','Double-Bounsplode','Triple-Bounsplode')
highbounce_wep = ('One-Bounce', 'Three-Bounce', 'Five-Bounce', 'Seven-Bounce','Stone','Boulder','Fireball')
medium_bounce_wep = ('Grenade', 'Tri-Nade', 'Multi-Nade','Sillyballs','Wackyballs','Crazyballs','Beacon','Beaconator','Jack-o-Lantern','Jack-o-Vomit')
bounce_wep = ('Grenade Storm', 'Cactus Strike', 'Tunnel Strike', 'Air Strike','Helicopter Strike','AC-130','Artillery', 'Bolt','Lightning','2012','Disco Ball','Groovy Ball','Water Balloon','Water Trio','Water Fight','Pinata','Fiesta','Mine Layer','Sticky Rain',
'Rain','Hail','FireStorm','Shrapnel','Shredders','Carpet Bomb','Carpet Fire','Incendiary Bombs','Snowstorm','Bounstream','Bounstrike','Shooting Star','Kamikaze','Suicide Bomber',
'Acid Rain','Acid Hail','Recruiter','Enroller','Enlister','Cats and Dogs','Satellite','UFO','Kernels','Popcorn','Burnt Popcorn','Pinpoint','Needles','Pins and Needles',
'God Rays','Deity')
if PRINT:
print("SIM Power & Angle: ", power, angle)
print("SIM Wind: ", wind)
if weapon in noprojectile_weps:
return
size_x, size_y = im.size
ts = BARREL_LENGTH # tank gun offset
tsy = BARREL_LENGTH_Y
sf = sfv.get()#SF
wf = wfv.get()#WF
apogee_cnt_init = 0
max_t = MAX_T
gacc = gaccv.get()#GACC
gacc_sub = gacc*10.0/9.803#GACC_SUB
tasermax = 1
taserangle = [angle, angle]
bouncing = False
bf = 0.0 # boomerang
ram = False
ram_active = False
if weapon in slow_weps or md.get() == 2:
#md.set(2)
if PRINT:
print("SLOW")
sf = sf/2.0
wf = wf/2.0#WF_SLOW
gacc = gacc/4.0
gacc_sub = gacc_sub/4.0
elif weapon in hover_wep or md.get() == 3:
#md.set(3)
if PRINT:
print("HOVER")
apogee_cnt_init = int(HOVER_T)
if weapon == "Heavy Hover-Ball":
apogee_cnt_init = apogee_cnt_init*10//8
# Heavy flies 1.0s, normal 0.8s
elif weapon in straight_weps or md.get() == 4:
#md.set(4)
if PRINT:
print("STRAIGHT")
gacc = 0.0
gacc_sub = 0.0
wind = 0.0
elif weapon in drei_d_wep or md.get() == 5: # 3d bomb is different with wind, not quite right yet (shot up/dn is deviating more in real game)
#md.set(5)
if PRINT:
print("3D")
gacc = 0.0
gacc_sub = 0.0
# wf = 0.00005
max_t = BOMB_T # limit flight time
elif weapon in taser_wep or md.get() == 6:
#md.set(6)
if PRINT:
print("TASER")
tasermax = 2
taserangle = [angle-5.7, angle+5.7]
elif weapon in bounce_wep or md.get() == 7:
#md.set(7)
if PRINT:
print("BOUNCING")
if override.get() == 0:
bfactor.set(BOUNCE_FACTOR[0])
bouncing = True
max_t = 2*max_t
elif weapon in medium_bounce_wep or md.get() == 11:
#md.set(11)
if PRINT:
print("MEDIUM-BOUNCING")
if override.get() == 0:
bfactor.set(BOUNCE_FACTOR[1])
bouncing = True
max_t = int(1.5*max_t)
elif weapon in highbounce_wep or md.get() == 8:
#md.set(8)
if PRINT:
print("HIGH-BOUNCING")
if override.get() == 0:
bfactor.set(BOUNCE_FACTOR[2])
bouncing = True
max_t = 2*max_t
elif weapon in ultra_bounce_wep or md.get() == 12:
#md.set(12)
if PRINT:
print("ULTRAHIGH-BOUNCING")
if override.get() == 0:
bfactor.set(BOUNCE_FACTOR[3])
bouncing = True
max_t = 2*max_t
elif weapon in boomerang_wep or md.get() == 9:
#md.set(9)
sx = power*math.cos(angle*math.pi/180.0)
bf = wf*BOOMERANG_F*sx/(100.0*math.cos(45.0*math.pi/180.0))
if PRINT:
print("BOOMERANG",sx,bf)
elif weapon in ram_wep or md.get() == 10:
#md.set(10)
ram = True # Trigger grav increase on apex
gacc_ram = gacc*RAM_F
if PRINT:
print("RAM")
else:
pass#md.set(0)
sw = subwep.get() != 0
if weapon in submersive_weps:
sw = True
pixdata = im.load()
pixdata[x, y] = (255, 0, 0)
pixdata[x+1, y+1] = (255, 0, 0)
pixdata[x+1, y] = (255, 0, 0)
pixdata[x, y+1] = (255, 0, 0)
#bcnt=0 # dbg
for taserp in range(0, tasermax):
if tasermax == 2:
angle = taserangle[taserp]
for q in range(0, 2): # ugly workaround for aiming dir (not unique)
sx = sf*power*math.cos(angle*math.pi/180.0)
sy = sf*power*math.sin(angle*math.pi/180.0)
lx = x+ts*math.cos(angle*math.pi/180.0)
if q==1:
lx = x-ts*math.cos(angle*math.pi/180.0)
ly = y-tsy*math.sin(angle*math.pi/180.0)
p = [[lx, ly], [lx, ly]]
renderbuf = [(0, 0)]*max_t
rbcnt = 0
windacc = 0.0
rfcooldown = 0
bouncing_cnt = 0
hover = False
apogee_cnt = apogee_cnt_init
for t in range(0, max_t):
oldsy = sy
if q==0:
p[q][0] += sx
else:
p[q][0] -= sx
p[q][0] += windacc
if q==0:
bff=-bf
windacc += wind*wf + bff
if not hover:
p[q][1] -= sy
ix = int(p[q][0]+0.5)
iy = int(p[q][1]+0.5)
# if q==0:
# print(ix, iy, gnd[ix], sy)
if bouncing: # hitting the ground bounces the weapon
if ix<0 or ix>=size_x-1 or iy<=gnd[ix] or bouncing_cnt>0 or sy>0: # shot in field and in air, upwards, or bouncing cooldown
if bouncing_cnt>0:
bouncing_cnt-=1
else:
# print("bouncing")
bouncing_cnt=5
p[q][1] = gnd[ix]+1 # ugly hack to prevent endless bouncing
gnd_angle,dx,dy = calc_gnd_angle(gnd, ix)
# calc entry/exit angles and modify sx,sy
sxn, syn = calc_refl_angle(gnd_angle, sx, sy)
#if q==0 and bcnt==0:
# bcnt+=1
# print("angle:", gnd_angle*180/math.pi,dx,dy)
# print("in:",sx,sy)
# print("out:",sxn,syn)
sy = syn * bfactor.get()#BOUNCE_FACTOR[btype]
sx = sxn * bfactor.get()#BOUNCE_FACTOR[btype]
sy -= gacc # normal flight curve
elif sw:
if ix<0 or ix>=size_x-1 or iy<=gnd[ix]: # shot in field and in air
if not hover:
sy -= gacc
else:
sy += gacc_sub # underground
# print("sub")
else:
if not hover:
sy -= gacc
if oldsy>0 and sy<0 and apogee_cnt>0:
hover = True
# print("Apogee detected", t)
if ram and oldsy>0 and sy<0:
ram_active = True
if ram_active:
gacc = gacc_ram
if hover:
apogee_cnt -= 1
if apogee_cnt == 0:
hover = False
if bound.get()!=0 and ix >= size_x and rfcooldown==0:# and iy in range(gnd[-1]-BOUNDER_HEIGHT,gnd[-1]+BOUNDER_HEIGHT): # right bounder
sx = -sx
rfcooldown = 10
windacc = -windacc
if bound.get()!=0 and ix <= 0 and rfcooldown==0:# and iy in range(gnd[0]-BOUNDER_HEIGHT,gnd[0]+BOUNDER_HEIGHT): # left bounder
sx = -sx
rfcooldown = 10
windacc = -windacc
if rfcooldown>0:
rfcooldown -= 1
if ix in range(0, size_x) and iy in range(0, size_y):
renderbuf[rbcnt] = (ix, iy, COLOR[q])
rbcnt += 1
if ix not in range(-10, size_x+10): # end meaningless calculation
break
if iy > size_y+10 and sy < 0: # end meaningless calculation
break
for t in range(0, rbcnt):
ix, iy, c = renderbuf[t]
pixdata[ix, iy] = c # draw curve
try:
pixdata[ix, iy+1] = c # draw curve
pixdata[ix+1, iy] = c # draw curve
pixdata[ix+1, iy+1] = c # draw curve
except:
pass
if verbose:
im.save("sss.jpg")
im_r = im.resize((int(size_x/IMG_SCALE), int(size_y/IMG_SCALE)), 3)
img = ImageTk.PhotoImage(im_r)
canvas.create_image(0,0,anchor=NW,image=img)
def find_tank(im):
im_tank = im.load()
found = False
x = 0
y = 0
size_x, size_y = im.size
for y in range(size_y-1, 0, -2):
for x in range(0, size_x, 2):
r, g, b = im_tank[x, y]
if (r in range(100, 140)) and (g in range(200, 255)) and (b in range(100, 140) and (g-b > 90)): # color matching the green tank
tank_at = (x, y)
found = True
break
if found:
break
if found==False:
return (0, 0)
p = [[0]*2]*40*40
cnt = 0
for xx in range(x-40, x+40):
for yy in range(y-40, y+40):
if xx>=0 and yy>=0 and xx<size_x and yy<size_y:
r, g, b = im_tank[xx, yy]
if (r in range(0, 127)) and (g in range(160, 255)) and (b in range(0, 127) and (g-b > 100)):
try:
p[cnt] = [xx, yy]
# im_tank[xx,yy] = (255,0,0) # highlight tank pixels
cnt += 1
except:
pass
if cnt > 0:
xsum = 0
ysum = 0
for pp in p:
xsum += pp[0]
ysum += pp[1]
xsum /= cnt
ysum /= cnt
rx, ry = int(xsum+0.5), int(ysum+0.5) + TANKY_OFFS
# im_tank[rx,ry] = (0,0,0) # use this to precision-match the center
# im_tank[rx+1,ry] = (0,0,0)
# im_tank[rx,ry+1] = (0,0,0)
# im_tank[rx+1,ry+1] = (0,0,0)
# im.show()
return (rx, ry)
return (0, 0)
def find_gnd_color(im, size_x, size_y): # find the general color of the ground, possible improvements: sample more, only take majority color
# try left bottom
col = im[10, size_y-10]
if col[0] in range(50,255) or col[1] in range(50,255) or col[2] in range(50,255): # with the risk that a tank or obstacle is there
return col
col = im[size_x//2, size_y-10]
if col[0] in range(50,255) or col[1] in range(50,255) or col[2] in range(50,255): # with the risk that a tank or obstacle is there
return col
col = im[size_x-10, size_y-10]
if col[0] in range(50,255) or col[1] in range(50,255) or col[2] in range(50,255): # with the risk that a tank or obstacle is there
return col
def check_col(p, col, dn=-20, dp=50):
if p[0] in range(col[0]-dn,col[0]+dp) and p[1] in range(col[1]-dn,col[1]+dp) and p[2] in range(col[2]-dn,col[2]+dp):
return True
return False
def find_y_border(im, col, x, size_x, size_y):
y = size_y-1 # scan from bottom -> up
en = False
collimit = 120
enlimit = 200
if x >= size_x-25 or x <= 25: # darkened border reagon
collimit = 70
enlimit = 100
while y>0:
p = im[x, y]
if p[2] > enlimit:
en = True
if en and p[2] < collimit:# massive decrease in blue brightness -- check_col(p, col, -20, 50): # already gnd color
return y+1
y -= 1
#xxx find tanks
return size_y-1
def find_ground(im):
im_gnd = im.load()
size_x, size_y = im.size
ground = [-1]*size_x
gnd_col = find_gnd_color(im_gnd, size_x, size_y)
if PRINT:
print("Ground color: ", gnd_col)
for x in range(0,size_x): # todo multithreaded loop to speed up
y = find_y_border(im_gnd, gnd_col, x, size_x, size_y)
# print(x,y)
# im_gnd[x, min(y,size_y-1)] = (255,0,0)
# im_gnd[x, min(y+1,size_y-1)] = (255,0,0)
# im_gnd[x, min(y+2,size_y-1)] = (255,0,0)
ground[x] = y
# im.show()
# interpolate missing gnd?
return ground
im_play = ""
running = False
def run_click():
global running
# run()
# return
if running:
btn_run["text"] = "Run"
running = False
else:
btn_run["text"] = "Stop"
running = True
run()
img_wep = ""
def run():
global im_play, img_wep
# global wind, weapon, power, angle
im = pyautogui.screenshot()
im = im.crop(GAME_CRD)
if AUTO:
f_rect = FIRE_CRD
im_f = im.crop(f_rect)
im_fire = im_f.load()
c = im_fire[0, 0]
r, g, b = c
# print((r,g,b))
if not (r>220 and g<40 and b<40):#r==g and g==b
if running:# and not stop_event.is_set():
win.after(10, run)
return
if override.get()==0:#wind == "":
wind_rect = WIND_CRD#(946, 102, 943+29, 102+18)
wind.set(f"{get_wind(im, wind_rect)}")
weapon = "not scanned"
if autowep.get() != 0:
wep_rect = WEP_CRD
im_wep = im.crop(wep_rect)
img_wep = ImageTk.PhotoImage(im_wep)
for o in OCRS:
weapon = ocr(im_wep, o, '--psm 7').strip()
if weapon in WEAPONS:
break
if SHOW_CROPS:
im_wep.show()
lw["text"] = weapon
play_rect = PLAY_CRD#(264, 34, 264+1393, 71+854)
im_play = im.crop(play_rect)
#start = timer()
# find green tank's x and y
x, y = find_tank(im_play)
if y==0:
if PRINT:
print("Tank not found!")
if PRINT:
print("Tank at: ", x, y)
gnd = find_ground(im_play)
#print("find stuff:",timer()-start)
canvas.delete("all")
if y != 0:
# pow_rect = ((max(0, x-42), y+22, min(x+42, size_x), min(y+54+17, size_y)), (max(0, x-105), y-10, max(1, x-20), min(y+20, size_y))) # match the 2 possible positions
# for pr in pow_rect:
pr = find_pa(im_play, x, y)
# print(pr,x,y)
if pr[1] < pr[3]:
col = "#888"
fnt = "Tahoma 14"
ocrm = OCRS#["paddleocr", "winrt", "tesseract"]
for m in ocrm: # sorted by accuracy
try:
pa = get_pow_ang_t(im_play, pr, m).strip()
pap = postprocess(pa)
except:
pap = ""
if pap != "" and not BENCH_OCR: # sim shot
p, a = pap
if p.strip() != "" and a.strip() != "":
# try:
if override.get() == 0: #power == "":
power.set(f"{int(p)}")
if override.get() == 0: #angle == "":
angle.set(f"{int(a)}")
sim_shot(im_play, x, y, int(power.get()), int(angle.get()), int(wind.get()), weapon, gnd)
if img_pa != "":
canvas.create_image(5, 5, anchor=NW, image=img_pa)
canvas.create_text(180, 20, text=f"{pa}", fill=col, font=fnt)
break
# except:
if PRINT:
print("invalid pa")
else:
if PRINT:
print("invalid pa")
if BENCH_OCR: # show ocr activity
if img_wind != "":
canvas.create_image(10, 5, anchor=NW, image=img_wind)
canvas.create_text(150, 14, text=f"{wind.get()}", fill=col, font=fnt)
ox = 350
for o in OCRS:
start = timer()
pa = get_pow_ang_t(im_play, pr, o).strip()
t = "{:1.1f}".format((timer()-start)*1000.0)
canvas.create_text(ox, 80, text=o+f":\n{pa}\n{t}", fill=col, font=fnt)
ox += 145
if img_pa != "":
canvas.create_image(10, 80-40, anchor=NW, image=img_pa)
if img_wep != "":
canvas.create_image(250, 0, anchor=NW, image=img_wep)
if AUTO:
if running:# and not stop_event.is_set():
win.after(10, run)
x=""
stop_event = threading.Event()
def run_auto():
while(True):
run()
time.sleep(0.05)
if stop_event.is_set():
break
def save():
im_play.save("sss.jpg")
#Create a canvas
size_x = PLAY_CRD[2]-PLAY_CRD[0]
size_y = PLAY_CRD[3]-PLAY_CRD[1]
canvas = Canvas(win, width=int(size_x/IMG_SCALE), height=int(size_y/IMG_SCALE))
canvas.pack()
btn_run = Button(win, text = "Run", width = 200, command = run_click)
btn_run.pack()
f1 = Frame(win)
Checkbutton(f1, text="Enable Underground", variable=subwep).pack(side='left')
Checkbutton(f1, text="Enable Bounders", variable=bound).pack(side='left')
Checkbutton(f1, text="Detect Weapon", variable=autowep).pack(side='left')
Label(f1, text="SF:").pack(side=LEFT)
Entry(f1, textvariable=sfv, width=10).pack(side=LEFT)
Label(f1, text="WF:").pack(side=LEFT)
Entry(f1, textvariable=wfv, width=10).pack(side=LEFT)
Label(f1, text="G:").pack(side=LEFT)
Entry(f1, textvariable=gaccv, width=10).pack(side=LEFT)
f1.pack(fill='x', expand=True)
f2 = Frame(win)
lw = Label(f2, text="Weapon:")
lw.pack(side=LEFT)
Label(f2, text="Wind:").pack(side=LEFT)
Entry(f2, textvariable=wind, width=4).pack(side=LEFT)
Label(f2, text="Power:").pack(side=LEFT)
Entry(f2, textvariable=power, width=4).pack(side=LEFT)
Label(f2, text="Angle:").pack(side=LEFT)
Entry(f2, textvariable=angle, width=4).pack(side=LEFT)
Label(f2, text="Bounce:").pack(side=LEFT)
Entry(f2, textvariable=bfactor, width=4).pack(side=LEFT)
Checkbutton(f2, text="Override", variable=override).pack(side=LEFT)
Button(f2, text = "Save", command = save).pack(side=LEFT)
f2.pack(fill='x', expand=True)
f3 = Frame(win)
Radiobutton(f3, text="Normal", variable=md, value=0).pack(side=LEFT)
Radiobutton(f3, text="Slow", variable=md, value=2).pack(side=LEFT)
Radiobutton(f3, text="Hover", variable=md, value=3).pack(side=LEFT)
Radiobutton(f3, text="3D", variable=md, value=5).pack(side=LEFT)
Radiobutton(f3, text="Taser", variable=md, value=6).pack(side=LEFT)
Radiobutton(f3, text="Bounce", variable=md, value=7).pack(side=LEFT)
Radiobutton(f3, text="Med-", variable=md, value=11).pack(side=LEFT)
Radiobutton(f3, text="High-", variable=md, value=8).pack(side=LEFT)
Radiobutton(f3, text="Ultra-Bounce", variable=md, value=12).pack(side=LEFT)
Radiobutton(f3, text="Boomerang", variable=md, value=9).pack(side=LEFT)
Radiobutton(f3, text="Ram", variable=md, value=10).pack(side=LEFT)
f3.pack(fill='x', expand=True)
def on_close():
global x, stop_event
if AUTO:
stop_event.set()
#x.join()
with open(CONFIG_FILE, "w") as conf:
conf.write(win.geometry())
win.destroy()
win.protocol("WM_DELETE_WINDOW", on_close)
def key_handler(event):
if event.keycode == 27:
sys.exit()
if event.char == "r" or event.keysym == "space":
run_click()
if event.char == "s":
save()
if event.char == "o":
override.set(1-override.get())
if event.char == "b":
bound.set(1-bound.get())
if event.char == "u":
subwep.set(1-subwep.get())
# print(event.char, event.keysym, event.keycode)
win.bind("<Key>", key_handler)
#if AUTO:
# x = threading.Thread(target=run_auto)
# x.start()
# loop
win.mainloop()
stop_event.set()
#~ # the math solution
#~ # calc shellshock
#~ C = 573.0/(0.73 * 2.58 * math.cos(math.pi/4.0))
#~ G = (0.73 * math.sqrt(2.0) * C)/2.58
#~ D = 446.0 # Durchmesser Zielkreis
#~ B = 446.0/D/1.53 # screen correction
#S = 2.82/1.51
#~ def cr(x, y, angle):
#~ x = B*x
#~ y = B*y
#~ ang = angle*math.pi/180.0
#~ num = G*x*x
#~ den = C*C*(x*math.sin(2.0*ang) - 2.0*y*(math.cos(ang) ** 2.0))
#~ k = math.sqrt(num/den)
#~ return k * 100.0
#~ print(cr(385, -226, 30))