From 08646a3f90971bb8383912ffca803f77934291e0 Mon Sep 17 00:00:00 2001 From: Divvy <54956345+DivvyCr@users.noreply.github.com> Date: Tue, 5 May 2020 15:43:30 +0100 Subject: [PATCH 1/5] Remove examples from README.md They were moved to their own wiki page. --- README.md | 58 ++----------------------------------------------------- 1 file changed, 2 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index df010d45..ee990554 100644 --- a/README.md +++ b/README.md @@ -38,63 +38,9 @@ python init.py ``` ## Examples / Usage -One of the main data structures used in carball is the pandas.DataFrame, to learn more, see [its wiki page](https://github.com/SaltieRL/carball/wiki/data_frame). +The GitHub carball wiki has a lot of useful information about using carball. Including the most useful examples to get started with carball, which can be found [here](https://github.com/SaltieRL/carball/wiki/All-Examples "Examples"). -Decompile and analyze a replay: -```Python -import carball - -analysis_manager = carball.analyze_replay_file('9EB5E5814D73F55B51A1BD9664D4CBF3.replay', - output_path='9EB5E5814D73F55B51A1BD9664D4CBF3.json', - overwrite=True) -proto_game = analysis_manager.get_protobuf_data() - -# you can see more example of using the analysis manager below - -``` - -Just decompile a replay to a JSON object: - -```Python -import carball - -_json = carball.decompile_replay('9EB5E5814D73F55B51A1BD9664D4CBF3.replay', - output_path='9EB5E5814D73F55B51A1BD9664D4CBF3.json', - overwrite=True) -``` - -Analyze a JSON game object: -```Python -import carball -import os -import gzip -from carball.json_parser.game import Game -from carball.analysis.analysis_manager import AnalysisManager -# _json is a JSON game object (from decompile_replay) -game = Game() -game.initialize(loaded_json=_json) - -analysis_manager = AnalysisManager(game) -analysis_manager.create_analysis() - -# write proto out to a file -# read api/*.proto for info on the object properties -with open(os.path.join('output.pts'), 'wb') as fo: - analysis_manager.write_proto_out_to_file(fo) - -# write pandas dataframe out as a gzipped numpy array -with gzip.open(os.path.join('output.gzip'), 'wb') as fo: - analysis_manager.write_pandas_out_to_file(fo) - -# return the proto object in python -proto_object = analysis_manager.get_protobuf_data() - -# return the proto object as a json object -json_oject = analysis_manager.get_json_data() - -# return the pandas data frame in python -dataframe = analysis_manager.get_data_frame() -``` +One of the main data structures used in carball is the pandas.DataFrame, to learn more, see [its wiki page](https://github.com/SaltieRL/carball/wiki/data_frame "DataFrame"). ### Command Line From e346c875de89655c376e9116a5a0dba129cd31a5 Mon Sep 17 00:00:00 2001 From: Divvy <54956345+DivvyCr@users.noreply.github.com> Date: Tue, 5 May 2020 15:58:42 +0100 Subject: [PATCH 2/5] Update examples. (Make some clarifications, clean-up a bit) --- README.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee990554..d906da78 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,82 @@ python init.py ``` ## Examples / Usage -The GitHub carball wiki has a lot of useful information about using carball. Including the most useful examples to get started with carball, which can be found [here](https://github.com/SaltieRL/carball/wiki/All-Examples "Examples"). - One of the main data structures used in carball is the pandas.DataFrame, to learn more, see [its wiki page](https://github.com/SaltieRL/carball/wiki/data_frame "DataFrame"). +### Decompile a Replay +```Python +from carball import decompile_replay + +# This is the path to your replay file (.replay file extension) +# On Windows, by default, Rocket League saves replays in C:\Users\%USER%\Documents\My Games\Rocket League\TAGame\Demos +replay_path = 'path/to/your/replayID.replay' + +# This is the path where the replay's JSON file will be stored. It is recommended to use the replay ID as the file name. +output_path = 'path/to/desired/location/replayID.json' + +# Returns the JSON object for the given replay. +_json = decompile_replay(replay_path, output_path=output_path,overwrite=True) +``` + +### Analyse a Replay +If you haven't manually decompiled the replay, use the following: +```Python +from carball import analyze_replay_file + +# This is the path to your replay file (.replay extension) +# On Windows, by default, Rocket League saves replays in C:\Users\%USER%\Documents\My Games\Rocket League\TAGame\Demos +replay_path = 'path/to/your/replayID.replay' + +# This is the path where the replay's JSON file will be stored. +output_path = 'path/to/desired/location/replayID.json' + +# The analyze_replay_file() method creates an instance of AnalysisManager and also runs the analysis. +analysis_manager = analyze_replay_file(replay_path, output_path=output_path,overwrite=True) +``` + +If you have a decompiled replay, as a JSON file (e.g. ```_json```), use the following: +```Python +from carball.json_parser.game import Game +from carball.analysis.analysis_manager import AnalysisManager + +# Create and intialise the Game object. +game = Game() +game.initialize(loaded_json=_json) + +# Create an AnalysisManager object and run the analysis. +analysis_manager = AnalysisManager(game) +analysis_manager.create_analysis() +``` + +### Retrieve Analysis Data +Once you have created and ran the analysis, you can retrieve each of the data types by using the following methods: +```Python +# Returns the Protobuf object +proto_object = analysis_manager.get_protobuf_data() + +# Returns the Protobuf object as a json object +json_object = analysis_manager.get_json_data() + +# Returns the DataFrame object +data_frame = analysis_manager.get_data_frame() +``` + +You may also choose to write the analysed replay data into a file, so that you don't have to wait for all of the processes to run again: +```Python +import os +import gzip + +# Writes the Protobuf data out to the given file. The file mode is 'wb' for 'write bytes'. +# See api/*.proto for all fields and properties. +with open(os.path.join('output.pts'), 'wb') as file: + analysis_manager.write_proto_out_to_file(file) + +# Writes the pandas.DataFrame data out to the given file, as a gzipped numpy array. The file mode is 'wb' for 'write bytes'. +with gzip.open(os.path.join('output.gzip'), 'wb') as file: + analysis_manager.write_pandas_out_to_file(file) +``` + + ### Command Line Carball comes with a command line tool to analyze replays. To use carball from the command line: From 0fdc23355e18981a49b2fb374d2e19e9f50422ac Mon Sep 17 00:00:00 2001 From: Divvy <54956345+DivvyCr@users.noreply.github.com> Date: Tue, 5 May 2020 16:08:28 +0100 Subject: [PATCH 3/5] Update README.md. Shorten development section by moving it to the wiki and linking there. --- README.md | 36 ++++-------------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index d906da78..cdbebd7c 100644 --- a/README.md +++ b/README.md @@ -190,35 +190,7 @@ Fix: `chmod +x "carball/rattletrap/rattletrap-6.2.2-linux"` -## Developing -Everyone is welcome to join the carball (and calculated.gg) project! Even if you are a beginner, this can be used as an opportunity to learn more - you just need to be willing to learn and contribute. - -### Usage of GitHub -All contributions end up on the carball repository. If you are new to the project you are required to use your own fork for first changes. If you do not have any previous git / github experience that is completely fine - we can help with it. -If we believe that you are comitted to working on the project and have experience in git we may give you write access so that you no longer have to use a fork. Nonetheless, please wait until your contrubtion is ready for a review to make the pull request because that will save resources for our tests and reduce spam. -For testing you should use your own fork, but take note that some carball tests may fail on a fork - -### Learning about carball -Currently, there is active creation of the carball wiki on GitHub - it aims to provide all relevant information about carball and development, so if you are a beginner, definitely have a look there. If you can't find information that you were looking for, your next option is the calculated.gg Discord server, where you may send a message to the #help channel. - -The carball code is also documented, although sparsely. However, you still may find information there, too. - -### Testing -The main requirement is to run PyTest. If you are using an IDE that supports integrated testing (e.g. PyCharm), you should enable PyTest there. The secondary requirement (to compile the proto files) is to run the appropriate `setup` file (setup.bat for Windows, setup.sh for Linux/Mac). - -If you've never tested your code before, it is a good idea to learn that skill with PyTest! Have a look at their official documentation, or any other tutorials. - -### carball Performance -Carball powers calculated.gg, which analyses tens of thousands of replays per day. Therefore, performance is very important, and it is monitored and controlled using PyTest-Benchmarking, which is implemented via GitHub Actions. However, you may see your contribution's performance locally - look into PyTest-Benchmarking documentation. If your contribution is very inefficient - it will fail automatically. - -If you wish to see the current carball analysis performance, it is split into 5 replay categories, and can be accessed below: -* [Short Sample](https://saltierl.github.io/carball/dev/bench/short_sample/) - * A very short soccar replay - for fast benchmarking. -* [Short Dropshot](https://saltierl.github.io/carball/dev/bench/short_dropshot/) - * A very short dropshot replay - to test dropshot performance. -* [Rumble](https://saltierl.github.io/carball/dev/bench/full_rumble/) - * A full game of rumble - to test rumble performance. -* [RLCS](https://saltierl.github.io/carball/dev/bench/oce_rlcs/) - * A full soccar RLCS game. -* [RLCS (Intensive)](https://saltierl.github.io/carball/dev/bench/oce_rlcs_intensive/) - * A full soccar RLCS game, but run with the intense analysis flag. +# Developing +If you are looking to help us develop carball, please have a look at [THIS WIKI PAGE](https://github.com/SaltieRL/carball/wiki/Developing-carball). It's a good introduction to what you might need to know as well as some other key notions. + +You are advised to explore [the carball wiki](https://github.com/SaltieRL/carball/wiki), because it has information on some FAQs. From d3b3216b37347091a3bf66208e53a05a949cb9cf Mon Sep 17 00:00:00 2001 From: DivvyCr Date: Wed, 22 Jul 2020 22:58:09 +0100 Subject: [PATCH 4/5] Improve documentation and readability for boost.py stat. --- carball/analysis/stats/boost/boost.py | 106 +++++++++++++++++--------- 1 file changed, 69 insertions(+), 37 deletions(-) diff --git a/carball/analysis/stats/boost/boost.py b/carball/analysis/stats/boost/boost.py index bad06f4f..a3b2160b 100644 --- a/carball/analysis/stats/boost/boost.py +++ b/carball/analysis/stats/boost/boost.py @@ -27,50 +27,20 @@ def calculate_player_stat(self, player_stat_map: Dict[str, PlayerStats], game: G player_name = player_map[player_key].name player_data_frame = data_frame[player_name].copy() player_data_frame.loc[:, 'delta'] = data_frame['game'].delta + proto_boost.boost_usage = self.get_player_boost_usage(player_data_frame) - proto_boost.wasted_usage = self.get_player_boost_usage_max_speed(player_data_frame) - proto_boost.time_full_boost = self.get_time_with_max_boost(data_frame, player_data_frame) proto_boost.time_low_boost = self.get_time_with_low_boost(data_frame, player_data_frame) proto_boost.time_no_boost = self.get_time_with_zero_boost(data_frame, player_data_frame) proto_boost.average_boost_level = self.get_average_boost_level(player_data_frame) - + if 'boost_collect' not in player_data_frame: logger.warning('%s did not collect any boost', player_key) else: - gains_index = player_data_frame['boost'].diff().clip(0) - gains_index = gains_index.loc[gains_index > 0].index.to_numpy() - collect_frames = player_data_frame.loc[player_data_frame.index[player_data_frame['boost_collect'] > 34]] - # Have to loop to fuzzy match - wasted_big = 0 - for index in collect_frames.index.to_numpy(): - idx = gains_index[(np.abs(gains_index - index).argmin())] - int_idx = player_data_frame.index.get_loc(idx) - wasted_big += player_data_frame['boost'].iloc[int_idx - 1] / 256 * 100 - - collect_frames = player_data_frame.loc[player_data_frame.index[player_data_frame['boost_collect'] <= 34]] - prior_vals = np.empty([0]) - for index in collect_frames.index.to_numpy(): - idx = gains_index[(np.abs(gains_index - index).argmin())] - int_idx = player_data_frame.index.get_loc(idx) - val = player_data_frame['boost'].iloc[int_idx-1] - prior_vals = np.append(prior_vals, val) - deltas = ((prior_vals + 30.6) - 255) - wasted_small = deltas[deltas > 0].sum() / 256 * 100 - - collection = self.get_player_boost_collection(player_data_frame) - proto_boost.wasted_collection = wasted_big + wasted_small - proto_boost.wasted_big = wasted_big - proto_boost.wasted_small = wasted_small - - if 'small' in collection and collection['small'] is not None: - proto_boost.num_small_boosts = collection['small'] - if 'big' in collection and collection['big'] is not None: - proto_boost.num_large_boosts = collection['big'] - - proto_boost.num_stolen_boosts = self.get_num_stolen_boosts(player_data_frame, - player_map[player_key].is_orange) + self.calculate_and_set_player_wasted_collection(player_data_frame) + self.count_and_set_pad_collection(player_data_frame) + self.count_and_set_stolen_boosts(player_data_frame, player_map[player_key].is_orange) @staticmethod def get_player_boost_usage(player_dataframe: pd.DataFrame) -> np.float64: @@ -84,7 +54,7 @@ def get_average_boost_level(player_dataframe: pd.DataFrame) -> np.float64: return player_dataframe.boost.mean(skipna=True) / 255 * 100 @classmethod - def get_num_stolen_boosts(cls, player_dataframe: pd.DataFrame, is_orange): + def count_and_set_stolen_boosts(cls, player_dataframe: pd.DataFrame, is_orange): big = cls.field_constants.get_big_pads() # Get big pads below or above 0 depending on team # The index of y position is 1. The index of the label is 2. @@ -94,7 +64,7 @@ def get_num_stolen_boosts(cls, player_dataframe: pd.DataFrame, is_orange): opponent_pad_labels = big[big[:, 1] > 0][:, 2] #big[where[y] is > 0][labels] # Count all the places where isin = True by summing stolen = player_dataframe.boost_collect.isin(opponent_pad_labels).sum() - return stolen + proto_boost.num_stolen_boosts = stolen @staticmethod def get_player_boost_usage_max_speed(player_dataframe: pd.DataFrame) -> np.float64: @@ -138,3 +108,65 @@ def get_player_boost_collection(player_dataframe: pd.DataFrame) -> Dict[str, int except (AttributeError, KeyError): return {} return ret + +# NEW (DivvyC) + + @staticmethod + def get_wasted_big(gains_index, player_data_frame): + # Get all frames (by their index) where the player collected more than 34 boost. + collect_frames = player_data_frame.loc[player_data_frame.index[player_data_frame['boost_collect'] > 34]] + + # Have to loop to fuzzy match + wasted_big = 0 + for index in collect_frames.index.to_numpy(): + idx = gains_index[(np.abs(gains_index - index).argmin())] + int_idx = player_data_frame.index.get_loc(idx) + wasted_big += player_data_frame['boost'].iloc[int_idx - 1] / 256 * 100 + return wasted_big + + @staticmethod + def get_wasted_small(gains_index, player_data_frame): + # Now, get all frames (by their index) where the player collected less than 34 boost. + collect_frames = player_data_frame.loc[player_data_frame.index[player_data_frame['boost_collect'] <= 34]] + + prior_vals = np.empty([0]) + for index in collect_frames.index.to_numpy(): + idx = gains_index[(np.abs(gains_index - index).argmin())] + int_idx = player_data_frame.index.get_loc(idx) + val = player_data_frame['boost'].iloc[int_idx-1] + prior_vals = np.append(prior_vals, val) + deltas = ((prior_vals + 30.6) - 255) + wasted_small = deltas[deltas > 0].sum() / 256 * 100 + return wasted_small + + @staticmethod + def get_gains_index(player_data_frame): + # Get differences in boost (on a frame-by-frame basis), and only keep entries that are >= 0. + gains_index = player_data_frame['boost'].diff().clip(0) + # Get all frame indexes with non-zero values (i.e. boost gain), as a numpy array. + gains_index = gains_index.loc[gains_index > 0].index.to_numpy() + return gains_index + + @staticmethod + def calculate_and_set_player_wasted_collection(player_data_frame): + # Get gains_index, which returns a numpy array of all indexes where the player gained (collected) boost. + gains_index = self.get_gains_index(player_data_frame) + + # Get wasted_big, and set it to the appropriate API field. + wasted_big = self.get_wasted_big(gains_index, player_data_frame) + proto_boost.wasted_big = wasted_big + + # Get wasted_small, and set it to the appropriate API field. + wasted_small = self.get_wasted_small(gains_index, player_data_frame) + proto_boost.wasted_small = wasted_small + + # Add wasted_small/big to get wasted_collection, and set it to the appropriate API field. + proto_boost.wasted_collection = wasted_big + wasted_small + + @staticmethod + def count_and_set_pad_collection(player_data_frame): + collection = self.get_player_boost_collection(player_data_frame) + if 'small' in collection and collection['small'] is not None: + proto_boost.num_small_boosts = collection['small'] + if 'big' in collection and collection['big'] is not None: + proto_boost.num_large_boosts = collection['big'] From 3a660aaa2e4cbcf14907353095880b3ad341a2f0 Mon Sep 17 00:00:00 2001 From: DivvyCr Date: Wed, 22 Jul 2020 23:36:05 +0100 Subject: [PATCH 5/5] Syntax fixes + Type clarifications --- carball/analysis/stats/boost/boost.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/carball/analysis/stats/boost/boost.py b/carball/analysis/stats/boost/boost.py index a3b2160b..8e284cf0 100644 --- a/carball/analysis/stats/boost/boost.py +++ b/carball/analysis/stats/boost/boost.py @@ -38,9 +38,9 @@ def calculate_player_stat(self, player_stat_map: Dict[str, PlayerStats], game: G if 'boost_collect' not in player_data_frame: logger.warning('%s did not collect any boost', player_key) else: - self.calculate_and_set_player_wasted_collection(player_data_frame) - self.count_and_set_pad_collection(player_data_frame) - self.count_and_set_stolen_boosts(player_data_frame, player_map[player_key].is_orange) + self.calculate_and_set_player_wasted_collection(player_data_frame, proto_boost) + self.count_and_set_pad_collection(player_data_frame, proto_boost) + self.count_and_set_stolen_boosts(player_data_frame, player_map[player_key].is_orange, proto_boost) @staticmethod def get_player_boost_usage(player_dataframe: pd.DataFrame) -> np.float64: @@ -54,7 +54,7 @@ def get_average_boost_level(player_dataframe: pd.DataFrame) -> np.float64: return player_dataframe.boost.mean(skipna=True) / 255 * 100 @classmethod - def count_and_set_stolen_boosts(cls, player_dataframe: pd.DataFrame, is_orange): + def count_and_set_stolen_boosts(cls, player_dataframe: pd.DataFrame, is_orange, proto_boost): big = cls.field_constants.get_big_pads() # Get big pads below or above 0 depending on team # The index of y position is 1. The index of the label is 2. @@ -112,7 +112,7 @@ def get_player_boost_collection(player_dataframe: pd.DataFrame) -> Dict[str, int # NEW (DivvyC) @staticmethod - def get_wasted_big(gains_index, player_data_frame): + def get_wasted_big(gains_index: np.ndarray, player_data_frame: pd.DataFrame): # Get all frames (by their index) where the player collected more than 34 boost. collect_frames = player_data_frame.loc[player_data_frame.index[player_data_frame['boost_collect'] > 34]] @@ -125,7 +125,7 @@ def get_wasted_big(gains_index, player_data_frame): return wasted_big @staticmethod - def get_wasted_small(gains_index, player_data_frame): + def get_wasted_small(gains_index: np.ndarray, player_data_frame: pd.DataFrame): # Now, get all frames (by their index) where the player collected less than 34 boost. collect_frames = player_data_frame.loc[player_data_frame.index[player_data_frame['boost_collect'] <= 34]] @@ -140,7 +140,7 @@ def get_wasted_small(gains_index, player_data_frame): return wasted_small @staticmethod - def get_gains_index(player_data_frame): + def get_gains_index(player_data_frame: pd.DataFrame): # Get differences in boost (on a frame-by-frame basis), and only keep entries that are >= 0. gains_index = player_data_frame['boost'].diff().clip(0) # Get all frame indexes with non-zero values (i.e. boost gain), as a numpy array. @@ -148,24 +148,24 @@ def get_gains_index(player_data_frame): return gains_index @staticmethod - def calculate_and_set_player_wasted_collection(player_data_frame): + def calculate_and_set_player_wasted_collection(player_data_frame: pd.DataFrame, proto_boost): # Get gains_index, which returns a numpy array of all indexes where the player gained (collected) boost. - gains_index = self.get_gains_index(player_data_frame) + gains_index = BoostStat.get_gains_index(player_data_frame) # Get wasted_big, and set it to the appropriate API field. - wasted_big = self.get_wasted_big(gains_index, player_data_frame) + wasted_big = BoostStat.get_wasted_big(gains_index, player_data_frame) proto_boost.wasted_big = wasted_big # Get wasted_small, and set it to the appropriate API field. - wasted_small = self.get_wasted_small(gains_index, player_data_frame) + wasted_small = BoostStat.get_wasted_small(gains_index, player_data_frame) proto_boost.wasted_small = wasted_small # Add wasted_small/big to get wasted_collection, and set it to the appropriate API field. proto_boost.wasted_collection = wasted_big + wasted_small @staticmethod - def count_and_set_pad_collection(player_data_frame): - collection = self.get_player_boost_collection(player_data_frame) + def count_and_set_pad_collection(player_data_frame: pd.DataFrame, proto_boost): + collection = BoostStat.get_player_boost_collection(player_data_frame) if 'small' in collection and collection['small'] is not None: proto_boost.num_small_boosts = collection['small'] if 'big' in collection and collection['big'] is not None: