├── .gitignore ├── README.md ├── player.py └── radar.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.jpg 3 | *.bmp 4 | *.log 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TouhouPlayer 2 | ============ 3 | 4 | An in-progress player AI for the scrolling shooter game [Touhou Koumakyou] (http://en.wikipedia.org/wiki/The_Embodiment_of_Scarlet_Devil). 5 | 6 | 7 | Usage 8 | ------------ 9 | Start the game, start the AI using `python player.py`, then switch to the game window. 10 | 11 | 12 | To-Do 13 | ------------ 14 | * Keep track of player location more precisely 15 | * Recognize when a life has been lost 16 | * Terminate gracefully 17 | -------------------------------------------------------------------------------- /player.py: -------------------------------------------------------------------------------- 1 | from radar import Radar 2 | from twisted.internet import reactor 3 | from twisted.internet.task import LoopingCall 4 | import win32api, win32con, win32gui, win32ui 5 | 6 | import random 7 | import os 8 | import time 9 | import logging 10 | 11 | logging.basicConfig(filename='thplayer.log',level=logging.DEBUG) 12 | 13 | MOVE = {'left': 0x25, # 2 pixels each movement 14 | 'up': 0x26, 15 | 'right': 0x27, 16 | 'down': 0x28} 17 | 18 | MISC = {'shift': 0x10, # focus 19 | 'esc': 0x1B} 20 | 21 | ATK = {'z': 0x5A, # shoot 22 | 'x': 0x58} # bomb 23 | 24 | HIT_X = 192 25 | HIT_Y = 385 26 | 27 | def key_press(key): 28 | # TODO: Make this non-blocking 29 | win32api.keybd_event(key, 0, 0, 0) 30 | # reactor.callLater(.02, win32api.keybd_event,key, 0, 31 | # win32con.KEYEVENTF_KEYUP, 0) 32 | time.sleep(.02) 33 | win32api.keybd_event(key, 0, win32con.KEYEVENTF_KEYUP, 0) 34 | 35 | def key_hold(self, key): 36 | win32api.keybd_event(key, 0, 0, 0) 37 | 38 | def key_release(key): 39 | win32api.keybd_event(key, 0, win32con.KEYEVENTF_KEYUP, 0) 40 | 41 | 42 | class PlayerCharacter(object): 43 | def __init__(self, radar, hit_x=HIT_X, hit_y=HIT_Y, radius=3): 44 | self.hit_x = hit_x 45 | self.hit_y = hit_y 46 | self.radius = radius 47 | self.width = 62 48 | self.height = 82 # slight overestimation 49 | self.radar = radar 50 | 51 | def move_left(self): 52 | # for i in range(4): 53 | # TODO: Hitbox should not be allowed to move outside of gameplay area 54 | key_press(MOVE['left']) 55 | self.hit_x -= 4 56 | 57 | def move_right(self): 58 | # for i in range(4): 59 | key_press(MOVE['right']) 60 | self.hit_x += 4 61 | 62 | def move_up(self): 63 | # for i in range(4): 64 | key_press(MOVE['up']) 65 | self.hit_y -= 8 66 | 67 | def move_down(self): 68 | # for i in range(4): 69 | key_press(MOVE['down']) 70 | self.hit_y += 8 71 | 72 | def shift(self, dir): # Focused movement 73 | key_hold(MISC['shift']) 74 | key_press(MOVE[dir]) 75 | key.key_release(MISC['shift']) 76 | 77 | def shoot(self): 78 | key_press(ATK['z']) 79 | 80 | def bomb(self): 81 | key_press(ATK['x']) 82 | 83 | def evade(self): 84 | h_dists, v_dists = self.radar.obj_dists 85 | if h_dists.size > 0: 86 | self.move_left() 87 | logging.debug(h_dists, v_dists) 88 | 89 | print(self.hit_x, self.hit_y) 90 | 91 | def move_to(self, x, y): 92 | """Bring character to (x, y)""" 93 | pass 94 | 95 | def start(self): 96 | self.shoot_constantly = LoopingCall(self.shoot) 97 | self.bomb_occasionally = LoopingCall(self.bomb) 98 | self.evader = LoopingCall(self.evade) 99 | 100 | self.shoot_constantly.start(0) 101 | self.evader.start(.03) 102 | # self.bomb_occasionally.start(10, False) 103 | 104 | def start_game(): 105 | time.sleep(2) 106 | for i in range(5): 107 | key_press(0x5A) 108 | time.sleep(1.5) 109 | 110 | def main(): 111 | start_game() 112 | radar = Radar((HIT_X, HIT_Y)) 113 | player = PlayerCharacter(radar) 114 | 115 | reactor.callWhenRunning(player.start) 116 | reactor.callWhenRunning(radar.start) 117 | reactor.run() 118 | 119 | if __name__ == "__main__": 120 | main() 121 | -------------------------------------------------------------------------------- /radar.py: -------------------------------------------------------------------------------- 1 | import win32api, win32con, win32gui, win32ui 2 | import numpy as np 3 | from PIL import Image, ImageChops, ImageOps 4 | from twisted.internet import reactor 5 | from twisted.internet.task import LoopingCall 6 | 7 | import os 8 | import time 9 | 10 | # Coordinates for gameplay area 11 | GAME_RECT = {'x0': 35, 'y0': 42, 'dx': 384, 'dy': 448} 12 | 13 | def take_screenshot(x0, y0, dx, dy): 14 | """ 15 | Takes a screenshot of the region of the active window starting from 16 | (x0, y0) with width dx and height dy. 17 | """ 18 | hwnd = win32gui.GetForegroundWindow() # Window handle 19 | wDC = win32gui.GetWindowDC(hwnd) # Window device context 20 | dcObj = win32ui.CreateDCFromHandle(wDC) 21 | cDC = dcObj.CreateCompatibleDC() 22 | 23 | dataBitMap = win32ui.CreateBitmap() # PyHandle object 24 | dataBitMap.CreateCompatibleBitmap(dcObj, dx, dy) 25 | cDC.SelectObject(dataBitMap) 26 | cDC.BitBlt((0,0),(dx, dy) , dcObj, (x0, y0), win32con.SRCCOPY) 27 | image = dataBitMap.GetBitmapBits(1) 28 | 29 | dcObj.DeleteDC() 30 | cDC.DeleteDC() 31 | win32gui.ReleaseDC(hwnd, wDC) 32 | 33 | return Image.frombuffer("RGBA", (384, 448), image, "raw", "RGBA", 0, 1) 34 | 35 | class Radar(object): 36 | def __init__(self, (hit_x, hit_y)): 37 | self.x0 = GAME_RECT['x0'] 38 | self.y0 = GAME_RECT['y0'] 39 | self.dx = GAME_RECT['dx'] 40 | self.dy = GAME_RECT['dy'] 41 | 42 | # TODO: Keep updating center to match character's hitbox 43 | self.center_x, self.center_y = (hit_x, hit_y) 44 | 45 | self.apothem = 50 # Distance within which to check for hostiles 46 | self.curr_fov = take_screenshot(self.x0, self.y0, self.dx, self.dy) 47 | self.obj_dists = (np.empty(0), np.empty(0)) # distances of objects in fov 48 | self.blink_time = .03 # Pause between screenshots 49 | self.diff_threhold = 90 # Diffs above this are dangerous 50 | 51 | # TODO: Call self.scan_fov only when self.curr_fov is updated 52 | self.scanner = LoopingCall(self.scan_fov) 53 | 54 | def update_fov(self): 55 | """Takes a screenshot and makes it the current fov.""" 56 | # TODO: Only need to record the part we actually examine in scan_fov 57 | self.curr_fov = take_screenshot(self.x0, self.y0, self.dx, self.dy) 58 | # self.curr_fov.show() 59 | 60 | def get_diff(self): 61 | """Takes a new screenshots and compares it with the current one.""" 62 | # time.sleep(.03) # TODO: Make this non-blocking 63 | old_fov = self.curr_fov 64 | # old_fov.show() 65 | self.update_fov() 66 | # self.curr_fov.show() 67 | diff_img = ImageChops.difference(old_fov, self.curr_fov) 68 | # diff_img.show() 69 | return ImageOps.grayscale(diff_img) 70 | 71 | def scan_fov(self): 72 | """ 73 | Updates self.object_locs with a NumPy array of (x, y) coordinates 74 | (in terms of the current fov) of detected objects. 75 | """ 76 | diff_array = np.array(self.get_diff()) 77 | 78 | # Get the slice of the array representing the fov 79 | # NumPy indexing: array[rows, cols] 80 | x = self.center_x 81 | y = self.center_y 82 | apothem = self.apothem 83 | # Look at front, left, and right of hitbox 84 | fov_array = diff_array[x-apothem:x+apothem, y-apothem:y] 85 | fov_center = fov_array[fov_array[0].size/2] 86 | 87 | # Zero out low diff values; get the indices of non-zero values. 88 | # Note: fov_array is a view of diff_array that gets its own set of indices starting at 0,0 89 | fov_array[fov_array < self.diff_threhold] = 0 90 | # print np.nonzero(fov_array) 91 | obj_locs = np.transpose(np.nonzero(fov_array)) 92 | # print obj_locs, obj_locs.shape 93 | 94 | # Update self.obj_dists with distances of currently visible objects 95 | if obj_locs.size > 0: 96 | self.obj_dists = self.get_distance(obj_locs, fov_center) 97 | else: 98 | self.obj_dists = (np.empty(0), np.empty(0)) 99 | # print(self.obj_dists) 100 | 101 | def get_distance(self, locs, reference): 102 | """Get horizontal and vertical distances of objects in fov as a pair 103 | of NumPy arrays.""" 104 | h_dists = (locs[:, 0] - reference[0]) 105 | v_dists = (locs[:, 1] - reference[1]) 106 | # print(h_dists[0]) 107 | return (h_dists, v_dists) 108 | 109 | def start(self): 110 | self.curr_img = self.update_fov() 111 | self.scanner.start(self.blink_time, False) 112 | 113 | def main(): 114 | radar = Radar((192, 385)) 115 | reactor.callWhenRunning(radar.start) 116 | reactor.run() 117 | 118 | # start = time.time() 119 | # radar.start() 120 | # arr = radar.scan_fov() 121 | # # print(arr) 122 | # print(time.time() - start) 123 | 124 | if __name__ == '__main__': 125 | main() 126 | --------------------------------------------------------------------------------