퐁 게임 무한루프, 고쳤다는 말을 세 번 들었다
퐁 게임을 브라우저에 옮긴 이야기를 적으면서 마지막에 살짝 흘렸지. 사실 그 게임, 완성되고 나서도 한참을 더 고쳤어.
AI 조카가 "이제 됐습니다"라고 말한 게 세 번인데, 그중 진짜로 됐던 건 마지막 한 번뿐이었지. 오늘은 그 세 번의 재판 기록이야.
앞선 에러노트들과 다른 점이 하나 있어. 배포할 때 만난 빨간 글씨들은 알고 보니 대부분 에러가 아니었잖아. 이번 건 아니야. 셋 다 진짜 버그였고 게임이 실제로 안 끝났어.

무한루프가 뭐길래 게임이 안 끝나나
무한루프라는 말부터 하나 풀고 갈게. 나도 이 글을 쓰면서 처음 이름을 붙여본 거니까.
게임은 1초에 수십 번씩 같은 계산을 반복해. 공을 옮기고, 부딪혔나 보고, 점수를 볼지 정하고, 다시 처음으로. 이 반복 자체는 정상이야. 그래야 화면이 움직이니까.
문제는 그 반복이 끝나는 조건에 영영 못 닿을 때야. 공이 매번 같은 두 자리만 오가면 아무리 오래 돌려도 득점이 안 나오고, 득점이 안 나오면 게임도 안 끝나. 계산은 멀쩡히 돌아가는데 결말만 없는 거지. 이걸 무한루프라고 부른다더라고.
그러니까 화면이 멈추는 게 아니야. 오히려 부지런히 움직여. 멀쩡하게 돌아가는 걸 보고 있어야 한다는 게 더 사람 미치게 하는 부분이지.
1차 수정, 패들 모서리 충돌에 정면용 위치 보정이 걸렸다
내가 직접 게임을 해보다가 이상한 걸 발견했어. 패들을 특정 자리에 딱 붙여놓으면 공이 위아래로만 왔다리 갔다리 할 뿐, 끝나지 않는 게임 지옥에 빠지는 거지. 수명 다 한 형광등처럼 깜빡거리기만 하는 공을 한참 지켜보다가 조카를 불렀지.
사실 예전에 처음 결과를 받았을 땐 그대로도 좋았어. 처음이기도 하고 신기했으니까. 그런데 이제 조금 욕심이 생긴다고 할까나. 그래서,
"이게 게임이야? 어? 이게 게임이야?"
조카도 처음엔 애를 먹었어. 실시간으로 재현하려니 화면이 눈 깜짝할 새 지나가버려서 정확한 순간을 못 잡겠다는 거야. 그래서 방법을 바꿔서, 게임을 한 프레임씩 강제로 넘겨가며 정지 화면을 들여다봤대. 그랬더니 공이 딱 같은 두 자리를 영원히 왔다 갔다 하는 게 잡혔다더라고.
원인은 이거였어. 공이 패들에 부딪히는 상황은 사실 두 가지야. "정면으로 딱 맞았을 때"랑 "모서리에 살짝 스쳤을 때". 정면으로 맞았을 때는 공이 패들 속으로 파고든 것처럼 보이면 이상하니까 "공 위치를 패들 바로 앞으로 확 끌어다 놓는" 걸로 그걸 막아준다고 하더라고. 반면 모서리에 스쳤을 때는 그런 것 없이 그냥 방향만 살짝 틀어주면 충분하고.
근데 코드가 이 둘을 구분 안 하고, 모서리에 스쳤을 때도 정면용 위치 보정을 똑같이 적용했다는 거지. 그러니까 공은 방향이 안 바뀌는데(모서리라서 살짝만 틀어져야 하는데), 위치는 자꾸 같은 자리로 끌려오는(정면용 보정이 실행되니까) 상황이 반복된 거야.
공 입장에서는 제자리걸음만 하는 셈이니 영영 못 빠져나갈 수밖에.(유중혁이 이런 기분이었을까?) 녀석은 당당하게 말했지.
"다 됐어!"
2차 수정, 반사 각도가 고정이면 또 안 끝난다
다 됐다고? 이젠 알아. 이 녀석이 거짓말도 꽤 할 줄 안다는 걸. AI가 자동으로 확인했다는 것과 내가 직접 만져본 결과가 다르다니.
"솔직히 말해. 너 모르지?"
"아니, 나 아는데?"
이건 발끈한 척일까? 아니면 진짜 발끈한 걸까? 나는 나비 꿈을 꾼 것인가? 나비가 내 꿈을 꾼 것인가?
아무튼 이번엔 패들을 아예 화면 맨 끝에 딱 붙여놓고 가만히 뒀대. 그랬더니 공은 계속 움직이는데 점수는 20초가 넘도록 단 한 번도 안 나는 걸 확인했다는 거야.(내가 말한 걸 이제 확인하다니.)
진짜 원인은 이거였어. 이 게임은 공이 튕길 때 늘 똑같은 각도로만 반사되고 속도도 절대 안 바뀌게 짜여 있었대. 그러니 패들을 안 움직이고 가만히 두면 공과 AI 사이에서 똑같은 패턴이 영원히 반복되면서 득점이 안 나는 상황이 생기더라는 거야.
벽에 딱 붙은 자리가 제일 쉽게 이 함정에 빠졌어. 조카는 이참에 실제 탁구처럼 "패들 어디에 맞았는지"에 따라 반사 각도가 달라지도록 물리 자체를 다시 짰어. 중앙에 맞으면 똑바로, 가장자리에 맞으면 비스듬히.

각도가 매번 달라지면 같은 패턴이 반복될 수가 없어. 덕분에 예전의 복잡했던 예외 처리도 걷어낼 수 있었대. 근데 이건 인간 입장에선 상식인데 AI는 아닌가 봐.
3차 수정, AI 조준이 너무 정확해서 반사각이 무용지물이었다
각도까지 고치고 나니 이번엔 전혀 다른 종류의 불만이 생겼어. AI가 공을 받을 때마다 어찌나 정확하게 정중앙으로만 받아내는지, 결국 늘 공이 일자로만 튕기더라고. 애써 고친 각도 기능이 AI 앞에서는 무용지물이었던 거지.
이건 엄밀히 말하면 버그가 아니야. 코드는 시킨 대로 정확하게 잘 돌아갔어. 너무 잘 돌아가서 게임이 재미없었을 뿐이지. 시험 문제를 100점 맞는 상대랑 두는 오목 같은 거였어.
그래서 AI가 매번 공의 정확한 위치가 아니라 "살짝 빗나간 목표 지점"을 조준하게 바꾼 거야. 그 빗나가는 정도는 공을 되받아칠 때마다 새로 랜덤하게 정해져. 그러니까 일부러 조금씩 실수하는 척을 시킨 셈이야. 물론 AI 입장에서 말이야.
돌이켜보면 2차와 3차는 결국 같은 자리를 고친 거였어. 아래 코드에서 각도를 정하는 줄(hitOffset)이 무한루프를 끊어준 부분이고, 바로 다음 줄(aiAimOffset)이 AI를 살짝 허술하게 만들어준 부분이야.

자바스크립트 무한루프 버그 요약
| 증상 | 정체 | 처방 |
|---|---|---|
| 패들 모서리에서 공이 안 빠짐 | 모서리 반사에 정면용 위치 보정이 같이 적용됨 | 정면/모서리 케이스를 분리 |
| 가만히 둬도 무한루프에 빠짐 | 반사 각도·속도가 항상 고정 | 맞은 위치에 따라 반사 각도가 달라지게 물리 수정 |
| AI가 너무 완벽해서 재미없음 | 버그 아님, AI 조준이 지나치게 정확 | 매번 랜덤하게 살짝 빗나간 지점을 조준하게 변경 |
세 번을 고치고 나서 든 생각은 하나야. 자동으로 확인했다는 말과 사람이 직접 써봤다는 말은 다르다는 것. 앞의 둘은 녀석이 "됐다"고 한 뒤에도 내가 만져보니 안 됐던 거였고, 세 번째는 아예 녀석 눈엔 문제로 보이지도 않던 거였어.
그리고 그 틈을 메우려고 다시 파고들면, 처음 고쳤던 것보다 한 뼘 더 튼튼한 해법이 나오기도 하더라고. 반사 각도를 바꾼 게 무한루프도 끊고 게임도 재미있게 만들었으니 한 번에 둘을 건진 셈이지.






