{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Prediction By Simulation\n\nWhile I lay, waiting for any upcoming Kaggle Simulation contests, that I love participating in, this contest came as a pleasant surprise, as it reminded me of the [Google Football](https://www.kaggle.com/competitions/google-football) contest 2 years ago that I started my Kaggle journey with. Though unlike this, we had to design agents that played matches and score more goals than the opponent to win. But regardless, the idea struck, what if we could simulate rocket league itself? This would allow us to simply 'resume' the match from the captured moment we are provided with, and see for ourselves if a goal is scored and by which team.\n\nIt is a very uphill task, designing the terrain, the agents, and the mechanics, but journeys of a thousand miles start with the first step. So here we go:  ","metadata":{}},{"cell_type":"code","source":"!pip install plotly","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-10-06T19:10:08.836738Z","iopub.execute_input":"2022-10-06T19:10:08.837560Z","iopub.status.idle":"2022-10-06T19:10:22.249852Z","shell.execute_reply.started":"2022-10-06T19:10:08.837358Z","shell.execute_reply":"2022-10-06T19:10:22.248124Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport os\nimport gc\nimport plotly.express as px\nimport plotly\nplotly.__version__\nfrom IPython.display import clear_output\n# plotly.offline.init_notebook_mode(connected = True)","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-10-06T19:10:22.253500Z","iopub.execute_input":"2022-10-06T19:10:22.253898Z","iopub.status.idle":"2022-10-06T19:10:23.749000Z","shell.execute_reply.started":"2022-10-06T19:10:22.253859Z","shell.execute_reply":"2022-10-06T19:10:23.748099Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"To start off, we create a very simple environment, with only the positions and velocities of the objects (the ball and the players) in the field. While the training data could be used to validate our environment, for now we will simply focus on generating predictions out of it. Hence we read only the test data, and only the necessary columns.","metadata":{}},{"cell_type":"code","source":"\nCOLS = ['id']\nfor d in list('xy'):\n    COLS.append(f'ball_pos_{d}')\nfor d in list('xyz'):\n    COLS.append(f'ball_vel_{d}')\n    \nfor i in range(6):\n    for d in list('xy'):\n        COLS.append(f'p{i}_pos_{d}')\n    for d in list('xy'):\n        COLS.append(f'p{i}_vel_{d}')\n\ntest_types = pd.read_csv('../input/tabular-playground-series-oct-2022/test_dtypes.csv')\ntest_types_d = {}\nfor ind, row in test_types.iterrows():\n    if row.column != 'id' and row.column in COLS:\n        test_types_d[row.column] = row['dtype']\n    \ntestdf = pd.read_csv('../input/tabular-playground-series-oct-2022/test.csv', index_col='id', usecols=COLS).astype(test_types_d)\ntestdf.head()","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-10-06T19:10:23.750506Z","iopub.execute_input":"2022-10-06T19:10:23.751423Z","iopub.status.idle":"2022-10-06T19:10:34.560196Z","shell.execute_reply.started":"2022-10-06T19:10:23.751374Z","shell.execute_reply":"2022-10-06T19:10:34.558953Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Position Update:\nOur mechanics will be fairly simple as well, just to get the gears running for now. Positions will be updated as such:\n\n$$ pos_{x}^{t + 1} = pos_{x}^{t} + \\alpha \\times vel_{x}^{t}$$\n\nHow do we decide a value for the constant alpha? We look at the training dataset, and cross-verify with the equation above. We see it fits well, and **consistently** is nearly equal to 0.1\n\n$$ \\alpha = \\frac{pos_{x}^{t + 1} - pos_{x}^{t}}{vel_{x}^{t}}$$ \n<br >\n\nNow we will try to approximate the value of $ \\alpha $ from the train dataset","metadata":{}},{"cell_type":"code","source":"traindf = pd.read_csv('../input/tabular-playground-series-oct-2022/train_0.csv', nrows=1000)\ntraindf = traindf[traindf != 0.] # To avoid the initial zeroes\n\npos_t, pos_t2, vel_t = {}, {}, {}\nalpha = {}\n\nfor i in list('xyz'):\n    pos_t[i] = np.array(traindf[f'ball_pos_{i}'])\n    pos_t2[i] = np.concatenate([pos_t[i][1:], [0]])\n    vel_t[i] = np.array(traindf[f'ball_vel_{i}'])\n    alpha[i] = (pos_t2[i] - pos_t[i])/vel_t[i]\n\nfig = px.scatter(alpha,\n                 range_y = [0.0, 1],\n                labels=dict(index='t', value='alpha'))\nfig.show()","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-10-06T19:10:34.563352Z","iopub.execute_input":"2022-10-06T19:10:34.564213Z","iopub.status.idle":"2022-10-06T19:10:35.828148Z","shell.execute_reply.started":"2022-10-06T19:10:34.564162Z","shell.execute_reply":"2022-10-06T19:10:35.826705Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Apart from a few outliers, which may be collisions etc., alpha consistently seems to be close to $0.1$. Hence,\n\n$$ \\alpha \\approx 0.1$$\n\nThis seems to hold for both players and the ball. Nice! But what about velocities?\n\n<br >\n\n### Velocity Update: \n\nWe assume each car is constantly trying to get closer to the ball to help score, which isn't necessarily true, as many people hold back to defend, but it gives us a good generalized estimate for most players movements.\n$$ dpos_{x}^{p} = ball\\_pos_{x} - player\\_pos^{p}_{x}$$\n<br >\n\n$$ vel_{x}^{p} = \\beta \\times vel_{x}^{p} + (1 - \\beta) \\times dpos_{x}^{p} $$\n\nwhere $vel_{x}^{p} $ denotes the velocity of the $p^{th}$ player in the direction $x$. This might have seemed like a good idea in the start because of the momentum factor $\\beta$, but I quickly ran into a problem, the cars were 'converging' at the ball. The match wasn't explosive, as rocket league is supposed to be. Hence, I just resorted to\n<br >\n\n$$ vel_{x}^{p} = \\beta \\times (vel_{x}^{p} + dpos_{x}^{p}) $$\n\nthis can wildly oscillate velocities if $\\beta$ is high, but looking at some gameplay, that just feels common. \n\n<br >\n\n## Collisions\nLast but instead the most important : Collisions. I just couldn't get this to work right unfortunately, and owing to other work I have, I left it at the present state for now. But this needs fixing. \n\nMy assumptions were simple, instead of cuboids, we assume all the cars are spheres, with lets say a reasonable radius 3.5 (Trial and Error Honestly). Compared to the car, the ball is supposed to be very lightweight (or so I assumed, until I watched some gameplay, my bad), hence when they collide, the ball simply takes on the velocity of the car.\n\nI have yet to implement to logic for two cars colliding, that's up on my TO-DO list.\n\nAt each iteration, we store the distance of the ball from each goal. If the ball gets too close, we assume a goal, else we return outputs in the form of probabilities, scaled from those distances.","metadata":{}},{"cell_type":"code","source":"class Game():\n    def __init__(self, ball_pos, ball_vel, p_pos, p_vel, logging=False):\n        self.ball_pos = ball_pos\n        self.ball_vel = ball_vel\n        self.p_pos = p_pos\n        self.p_vel = p_vel\n        self.is_logging = logging\n        \n        self.a = 0.1\n        self.b = 0.9\n        \n        self.min_limit = [-82.5, -100]\n        self.max_limit = [82.5, 100]\n        \n        self.GOALS = np.array([\n            [0, -121],\n            [0, 121]\n        ], dtype=np.int16)\n        \n        self.goalposts = np.array([\n            [0, -105] if i < 3 else [0, 105] for i in range(6)\n        ])\n        \n        self.goalDone = False\n        self.goalDoneIn = -1\n        \n        self.distances = np.array([1e8, 1e8])\n        \n        if self.is_logging:\n            self.logs = []\n            \n\n    def check_distances(self):\n        new_dists = np.sqrt(np.sum((self.ball_pos - self.GOALS)**2, axis=1))\n        self.distances = np.minimum(self.distances, new_dists)\n        \n        \n    def get_dist(self, a, b, axis=0):\n        return np.sqrt(np.sum((a - b)**2, axis=axis))\n\n    \n    def check_collision(self):\n        \n        # Check Balls collisions with the boundaries\n        \n        self.ball_pos = np.maximum(np.minimum(self.ball_pos, self.max_limit), self.min_limit)\n        self.ball_vel = np.where(((self.ball_pos == self.max_limit) | (self.ball_pos == self.min_limit)), -0.9 * self.ball_vel, self.ball_vel)\n\n        self.p_pos = np.maximum(np.minimum(self.p_pos, self.max_limit), self.min_limit)\n        self.p_vel = np.where(((self.p_pos == self.max_limit) | (self.p_pos == self.min_limit)), -0.7 * self.p_vel, self.p_vel)          \n                \n        # If Ball and player collide\n        # If multiple seem to be colliding, consider closest one\n        dists = self.get_dist(self.ball_pos, self.p_pos, axis=1)\n        if dists.min() < 7:\n            self.ball_vel += self.p_vel[np.argmin(dists)]\n        \n        # If ball has come very close to one goal, assume scored\n        # Stop the match at this point\n#         goal_dists = self.get_dist(self.ball_pos, self.GOALS, axis=1)\n#         if goal_dists.min() < 20:\n#             self.goalDone = True\n#             self.goalDoneIn = np.argmin(goal_dists)\n        \n\n\n    def update_vels(self):\n        dx = self.ball_pos - self.p_pos\n        self.p_vel =  self.b * (self.p_vel + dx)\n        # Friction on the ball\n        self.ball_vel = self.ball_vel * 0.9\n        \n        \n    def update_vals(self):\n        # Update positions of ball and player\n        self.ball_pos = self.ball_pos + self.a * self.ball_vel\n        self.p_pos = self.p_pos + self.a * self.p_vel\n        \n        self.update_vels()\n        self.check_collision()\n        self.check_distances()\n    \n    def play(self, iterations=20):\n        for i in range(iterations):\n            self.update_vals()\n            \n#             if self.goalDone: break\n            \n            if self.is_logging:\n                self.logs.append(\n                    [self.ball_pos[0], self.ball_pos[1], 'ball', i, 'Ball']\n                )\n                \n                for j in range(6):\n                    self.logs.append(\n                        [self.p_pos[j][0], self.p_pos[j][1], f'p{j}', i, 'Team 1' if (j < 3) else 'Team 2']\n                    )\n\n    def get_scores(self):\n#         if not self.goalDone:\n        # Scale the distances, the larger the closest distance\n        # less the probability that the ball went into that goal.\n        # Flip the numbers to get probability if the teams won or lost.\n        # Scale using diagonal field distances, maximum possible\n        scores = np.flip(1 - self.distances/ np.sqrt(80**2 + 210**2))\n        if scores.min() >= 0.7:\n            scores -= 0.7\n        return scores\n        \n#         if self.goalDone:\n#             # The team's goal that the ball came close \n#             # enough to be considered a goal, lost. \n#             probs = np.array([1., 1.])\n#             probs[self.goalDoneIn] = 0.\n#             return probs\n#         return 1 - self.distances/171\n    \n    def get_logs(self):\n        return self.logs\n        \n        \n","metadata":{"execution":{"iopub.status.busy":"2022-10-06T19:10:35.829757Z","iopub.execute_input":"2022-10-06T19:10:35.830145Z","iopub.status.idle":"2022-10-06T19:10:35.853444Z","shell.execute_reply.started":"2022-10-06T19:10:35.830109Z","shell.execute_reply":"2022-10-06T19:10:35.852273Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Cool! Now we have a (very) basic environment available to simulate our games in. Let's use plotly to animate and visualize those.","metadata":{}},{"cell_type":"code","source":"\nrow = testdf.iloc[0]\nball_pos = np.array([\n    row.ball_pos_x, row.ball_pos_y\n])\nball_vel = np.array([\n    row.ball_vel_x, row.ball_vel_y\n])\np_pos = np.array([\n    [row[f'p{i}_pos_{d}'] for d in list('xy')] for i in range(6)\n])\np_vel = np.array([\n    [row[f'p{i}_vel_{d}'] for d in list('xy')] for i in range(6)\n])\n\np_pos[np.isnan(p_pos)] = 0\np_vel[np.isnan(p_vel)] = 0\n\ngame = Game(ball_pos, ball_vel, p_pos, p_vel, logging=True)\ngame.play()\nscores = game.get_scores()\nlogs = game.get_logs()\n\ndisplay(f'Probability of A, B Winning Respectively: {scores}')\n\ndf = pd.DataFrame(logs, columns=['x', 'y', 'obj', 'iteration', 'group'])\ndf.head()\n\nfig = px.scatter(df, x='y', y='x',\n                 animation_frame='iteration',\n                 animation_group='obj',\n                 color='group',\n                 range_x = [-100, 100],\n                 range_y = [-80, 80],\n                 title=\"Rocket League Simulation\",\n                labels= {\n                    \"title\": \"Robot League Simulation\"\n                },\n                width=800,\n                height=600)\n\nfig.add_shape(type='rect', x0=-100, x1=-98, y0=-32, y1=32, fillcolor='red', visible=True)\nfig.add_shape(type='rect', x0=98, x1=100, y0=-32, y1=32, fillcolor='green', visible=True)\nfig.add_shape(type='circle', y0=-32, y1=32, x0=-140, x1=-60, fillcolor='red', opacity=0.3)\nfig.add_shape(type='circle', y0=-32, y1=32, x0=140, x1=60, fillcolor='green', opacity=0.3)\nfig.add_hrect(-82.5, 82.5)\nfig.add_vline(0)\nfig.add_annotation(text=f'Probability of Winning: \\n A: {scores[0]:.3f} \\n B: {scores[1]:.3f}',\n                  xref='paper', yref='paper',\n                  x=0.05, y=1, showarrow=False)\n\nfig.show()","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-10-06T19:10:35.855094Z","iopub.execute_input":"2022-10-06T19:10:35.855455Z","iopub.status.idle":"2022-10-06T19:10:36.272681Z","shell.execute_reply.started":"2022-10-06T19:10:35.855422Z","shell.execute_reply":"2022-10-06T19:10:36.271387Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"results = {\n    'id': [],\n    'team_A_scoring_within_10sec': [],\n    'team_B_scoring_within_10sec': []\n}\n\nlogs = None\n\nfor ind, row in testdf.iterrows():\n    if ind % 10000 == 0:\n        print(ind)\n    \n    ball_pos = np.array([\n        row.ball_pos_x, row.ball_pos_y\n    ])\n    ball_vel = np.array([\n        row.ball_vel_x, row.ball_vel_y\n    ])\n    p_pos = np.array([\n        [row[f'p{i}_pos_{d}'] for d in list('xy')] for i in range(6)\n    ])\n    p_vel = np.array([\n        [row[f'p{i}_vel_{d}'] for d in list('xy')] for i in range(6)\n    ])\n    \n    p_pos[np.isnan(p_pos)] = 0\n    p_vel[np.isnan(p_vel)] = 0\n    \n    game = Game(ball_pos, ball_vel, p_pos, p_vel, logging=False)\n    game.play()\n    scores = game.get_scores()\n    \n    results['id'].append(ind)\n    results['team_A_scoring_within_10sec'].append(scores[0])\n    results['team_B_scoring_within_10sec'].append(scores[1])\n    \n#     logs = game.get_logs()\n\n    ","metadata":{"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-10-06T19:10:36.274318Z","iopub.execute_input":"2022-10-06T19:10:36.274676Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"subs = pd.DataFrame(results)\nsubs.to_csv('submission.csv', index=False)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### TO-DO\n\n- Fix and complete collisions\n- Use the games from the training match for testing and validating the environment. Automated searching for best params\n- Instead of a limited rule-based agent, try training a complex-enough RL agent as a substitution.\n- Use an ML model for translating distances to probabilities, instead of simple scaling.","metadata":{}}]}