Projectile Simulator

Ein ambitioniertes Python-Projekt.

# 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))