프로그래밍/동방프로젝트 봇 만들기

7. 그리디 알고리즘 (1)

hideh 2025. 2. 24. 01:28

 

일단 인공지능에 대해 아는것이 잘 없고, 돌릴 환경이 못 되는 관계로 간단한 방식을 먼저 시도해보려고 한다.

가장 단순한게 지금 상황에서 어디로 가는것이 가장 안전한가 를 판단해서 그 방향으로 이동하는 알고리즘을 생각하려고 한다.

프레임 단위로 플레이어의 위치를 확인해본 결과 프레임당 $4$, 저속비행의 경우 $2$ 의 속력으로, 상하좌우 및 대각선 총 8가지 방향으로 움직일 수 있다. 따라서 현재 상황으로부터 어떤 위치에서의 가치를 측정한 다음 그리디 알고리즘을 이용해 가장 가치가 높은 곳으로 이동하는 알고리즘을 구현해보려고 한다. 

  • 그러면 '가치' 를 어떻게 파악하는데?

일단 공격을 해야 하니 보스 바로 아래쪽의 특정 위치로부터 가까울수록 높은 가치를 주도록 하면 최대한 그곳으로 오려고 할 것이다. 이건 일단 베이스로 두고, 이제 어떻게 가치를 주어야 총알을 피하게 할 수 있을지 생각하면 된다.

 

단순하게 생각하면 총알을 피하기만 하기엔 총알로부터의 거리가 멀면 좋다...는 방식을 생각할수 있다. 그러나 동방프로젝트에서는 적용할 수 없는데, 그 이유는..

 

단순히 멀어지도록 알고리즘을 짜면 점점 밑으로 내려가다 화면 최하단에서 움직이지 못하고 결국 맞게 된다. 사실 이런 게임은 총알을 피하면서 오히려 돌파해야 하는데, 그러려면 어느 정도 총알과 가까운 곳에서는 높은 가치를 가지도록 할 필요가 있다.

그래서 거리를 $r$ 로 두었을 때, $r \rightarrow 0+$ 일때 $-\infty$ 로 발산하고 어떤 $r_0$에 대해 $r>r_0$에서는 양수이면서 $r \rightarrow \infty$에서는 $0$으로 수렴하는 그런 함수를 사용한다면, 어느 정도 돌파를 구현할 수 있으리라 기대하였다.

 

운이 좋게도, $e^\pi$ 와 $\pi^e$ 중 뭐가 더 크냐는 문제의 풀이 중 하나인공지능에 대해 아는것이 잘 없고, 돌릴 환경이 못 되는 관계로 간단한 방식을 먼저 시도해보려고 한다.

 

가장 단순한게 지금 상황에서 어디로 가는것이 가장 안전한가 를 판단해서 그 방향으로 이동하는 알고리즘을 생각하려고 한다.

 

프레임 단위로 플레이어의 위치를 확인해본 결과 프레임당 $4$, 저속비행의 경우 $2$ 의 속력으로, 상하좌우 및 대각선 총 8가지 방향으로 움직일 수 있다. 따라서 현재 상황으로부터 어떤 위치에서의 가치를 측정한 다음 그리디 알고리즘을 이용해 가장 가치가 높은 곳으로 이동하는 알고리즘을 구현해보려고 한다. 

 

그러면 '가치' 를 어떻게 파악하는데?

일단 공격을 해야 하니 보스 바로 아래쪽의 특정 위치로부터 가까울수록 높은 가치를 주도록 하면 최대한 그곳으로 오려고 할 것이다. 이건 일단 베이스로 두고, 이제 어떻게 가치를 주어야 총알을 피하게 할 수 있을지 생각하면 된다.

 

 

 

단순하게 생각하면 총알을 피하기만 하기엔 총알로부터의 거리가 멀면 좋다...는 방식을 생각할수 있다. 그러나 동방프로젝트에서는 적용할 수 없는데, 그 이유는..

 

 

 

단순히 멀어지도록 알고리즘을 짜면 점점 밑으로 내려가다 화면 최하단에서 움직이지 못하고 결국 맞게 된다. 사실 이런 게임은 총알을 피하면서 오히려 돌파해야 하는데, 그러려면 어느 정도 총알과 가까운 곳에서는 높은 가치를 가지도록 할 필요가 있다.

 

그래서 거리를 $r$ 로 두었을 때, $r \rightarrow 0+$ 일때 $-\infty$ 로 발산하고 어떤 $r_0$에 대해 $r>r_0$에서는 양수이면서 $r \rightarrow \infty$에서는 $0$으로 수렴하는 그런 함수를 사용한다면, 어느 정도 돌파를 구현할 수 있으리라 기대하였다. 이러한 개형을 가지는

$$f(r) = \log(r)e^{-r}$$

을 사용한다면, 대충 총알 크기의 2~3배 정도 되는 곳에서 가치가 최대가 되도록 설정해준다면 잘 될거 같다는 예상을 할 수 있다.

또한, 총알의 속도와 현재 위치로부터, 총알이 지나갈 궤적과 내 위치 사이의 안전거리를 측정할 수 있다. 점과 직선 사이의 거리 공식을 이용하면, 총알 위치 $\vec{b}$, 속도 $\vec v$, 내 위치 $\vec x$ 로 부터 다음 공식을 얻는다.

$$D = \left|\frac{\vec{v}^\bot}{\| \vec{v}^\bot \| } \cdot (\vec b - \vec x) \right|$$

이 경우는 돌파고 뭐고 그냥 멀수록 안전하기에 지수함수로 씌워서 가치를 계산해주면 된다.

 

이것저것 고려해서 코드를 짜면 대충 다음과 같다. 앞의 포스터에 알고리즘을 실행하는 코드를 작성하였으므로 여기에는 알고리즘 함수만 적는다.

    def algorithm(self):
        self.bullet_catch= 400
        consider = min(self.bullet_catch , self.bullet.info_bullets.shape[0])
        

        if 376> self.player.ex > 8 and 432> self.player.ey > 16 :
            #보스 아래에서 공격할수 있는 위치로 이동
            target_x = self.player.ex 
            target_y = self.player.ey + 250
            
        else:
            #적기 없을 때
            target_x = 190
            target_y = 380
        
        def get_value(position, consider = 20):
            x,y = position[0], position[1]

            #화면 끝에서 박혀있는 현상 방지
            if x<8 or x> 376 or y<16 or y>432 :
                return -np.inf
            
            #캐릭터가 최대한 target 좌표 근처로 가도록 하는 역할
            Value = -0.001* np.hypot(x-target_x  ,   y-target_y ) 
            
            
            def cal_benefit(x,y, bullet_x, bullet_y, bullet_vx, bullet_vy , bullet_s, bullet_d):
                rx = x - bullet_x 
                ry = y - bullet_y 
                r = np.array([rx,ry])
                n = np.hypot(rx,ry)
                ben = 0
                
                def R( z  , k) :
                    #k랑 상관없이 최댓값은 1 정도로 일정한 함수
                    #k는 zero와 maxima의 위치에 영향. 
                    try:
                        return 10*np.log(z/(10*k)) *np.exp(-z/(10*k))
                    except :
                        #0으로 갈때 발산하는 함수. 
                        return -np.inf
                if n < 10:
                    return -np.inf

                #상대거리 증가하는 경우
                if bullet_vx*rx + bullet_vy*ry >0.01 :
                    ben += R(n , 6 ) /(600-n) 

                #가까워지는 탄막의 경우
                elif bullet_vx*rx + bullet_vy*ry < 0.01:
                	#점과 직선 사이의 거리
                    norm_vec = np.array([ -bullet_vy/n , bullet_vx/n])
                    interval = np.abs(norm_vec @  r)
                    if interval > 200:
                        ben += R(interval ,20 )
                    else:
                        Time = bullet_d/(bullet_s+0.01)
                        try:
                            ben += R(interval ,10 ) *np.exp(interval - 10*Time)
                        except:
                            return -np.inf
                return ben
            cnt = min(consider , self.bullet.info_bullets.shape[0] ) #동기화 문제 때문에 직접 개수를 세려서 사용
            for i in range(cnt):
                bx,by, vx,vy , s ,d = self.bullet.info_bullets[ i ,: ]
                Value += cal_benefit(x,y, bx,by, vx,vy , s ,d ) 
                
            return Value

        d_selected = ""
        value = -np.inf
        Spd = 2
        current = np.array([self.player.x, self.player.y])
        #가장 value가 높은 곳으로 이동
        for speed in [2,4]:
            for direction in self.direction_map:
                temp_v = get_value(current + self.direction_map[direction] * speed )
                if value < temp_v:
                    value = temp_v
                    d_selected = direction
                    Spd = speed
        self.msg[7] = f"{value} , {d_selected}"
        self.pressed_keylist = d_selected.split("-")
        self.interprint( slow=(Spd == 2 ) )

 

 

이 알고리즘을 실행한 결과는 다음과 같다.

 

 

 단순한 거 치고 생각보다 꽤 잘 버틴다는 걸 알 수 있다. 하지만 중간중간 급발진해서 총알에 들이박곤 하는데, 이 부분을 개선한다면 일단 플랑을 만날수 있지 않을까 생각해본다.

 

 

가중치는 그냥 의미가 있는거라기보단 일단 넣고 잘되는거 쓰는중이다