diff --git a/.gitignore b/.gitignore index 9be45762..3ceda31f 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,7 @@ test.sh backend/dev_settings_sensitive.py infrastructure/worker/app/gcloud-key.json + +# Resumes +backend/resumes/files/* +backend/resumes/*.csv diff --git a/README.md b/README.md index acb89bab..a166b644 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ 🚩 +## Note, Competitors +This is the development repo! Do not clone this. Instead, follow the instructions [here](http://2021.battlecode.org/getting-started) ## Repository Structure - `/backend`: Backend API in Django Rest Framework @@ -58,6 +60,10 @@ You can generate javadocs as follows: This will create a `zip` file. Unzip and open the `index.html` file in it to view the docs. In particular, looking at the documentation for `RobotController` will be helpful. +To deploy these docs, get the above zip file, and unzip it. Rename the resulting folder to `javadoc`. Then, put it in `frontend/public`, suchh that there's a `frontend/public/javadoc/index.html`. Then run the frontend deploy process! + +TODO -- these steps ought to simply be in the frontend deploy script. + ## Notes for porting to a new repo When the next edition of Battlecode comes around, it will probably useful to reuse a fair amount of this codebase. Maintaining git history is nice. Use `git-filter-repo` for this: diff --git a/RELEASE_JAVA.md b/RELEASE_JAVA.md new file mode 100644 index 00000000..210a25b1 --- /dev/null +++ b/RELEASE_JAVA.md @@ -0,0 +1,43 @@ +# HOW TO RELEASE A JAVA GAME + +## Prereqs + +a bash-like shell (windows command prompt won't work for this). Also, zsh (and perhaps other shell environments) don't work to run the frontend deploy script. bash seems to work. + +npm + +a git key: Obtain a git key that has "publish packages" permissions. This key is a string. Keep it around somewhere + +## Get updates + +Make sure you have all the most recent updates to the repo! (Ideally they're pushed to git. Then do git checkout master, git fetch, git pull.) + +## Update some version numbers + +`client/visualizer/src/config` -- find ``gameVersion`, and update that. + +`gradle.properties` -- update `release_version`. + +Make sure these updates are pushed to master! + +## Update specs and javadoc + +In our game spec (specs folder), make sure changes are up to date. + +Pay attention to the version number at the top of specs.md.html, and to the changelog at the bottom. + +push to master btw! + +## Release packages + +Set BC21_GITUSERNAME: `export BC21_GITUSERNAME=n8kim1`, etc + +Set BC21_GITKEY similarly. This git key is the string discussed above. + +./gradlew publish + +Now set version.txt in gcloud (also set cache policy to no-store) + +## Deploy frontend + +Run the deploy.sh script! For arguments, follow the instructions in the file. For example: `bash ./deploy.sh deploy 2021.3.0.2` diff --git a/RELEASE.md b/RELEASE_PYTHON.md similarity index 82% rename from RELEASE.md rename to RELEASE_PYTHON.md index dd01013a..697f3de3 100644 --- a/RELEASE.md +++ b/RELEASE_PYTHON.md @@ -1,4 +1,6 @@ -# HOW TO RELEASE +# HOW TO RELEASE A PYTHON GAME + +In general, this guide and script may be out of date. Make any changes as necessary. ### Preliminaries - Install the frontend using `npm install`. diff --git a/backend/README.md b/backend/README.md index 92b2eb83..2eddaa7a 100644 --- a/backend/README.md +++ b/backend/README.md @@ -2,6 +2,8 @@ Written in Django Rest Framework. Based on `battlecode19/api`. +NOTE: If you are ever working with teams' eligility (for example, to pull teams for the newbie tournament), note that the columns in the database are poorly named. Please see backend/docs/ELIGIBILITY.md before you do anything! + ## Local Development The best way to run the backend locally is to run `docker-compose up --build backend` from the repo's root directory. diff --git a/backend/api/migrations/0021_auto_20210104_0550.py b/backend/api/migrations/0021_auto_20210104_0550.py new file mode 100644 index 00000000..54c2c66e --- /dev/null +++ b/backend/api/migrations/0021_auto_20210104_0550.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2021-01-04 05:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0020_scrimmage_losescore'), + ] + + operations = [ + migrations.AlterField( + model_name='scrimmage', + name='status', + field=models.TextField(choices=[('created', 'Created'), ('pending', 'Pending'), ('queued', 'Queued'), ('running', 'Running'), ('redwon', 'Red Won!'), ('bluewon', 'Blue Won!'), ('rejected', 'Rejected'), ('failed', 'Failed'), ('cancelled', 'Cancelled')], default='created'), + ), + ] diff --git a/backend/api/migrations/0022_auto_20210104_0807.py b/backend/api/migrations/0022_auto_20210104_0807.py new file mode 100644 index 00000000..b287198a --- /dev/null +++ b/backend/api/migrations/0022_auto_20210104_0807.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2021-01-04 08:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0021_auto_20210104_0550'), + ] + + operations = [ + migrations.AlterField( + model_name='team', + name='divisions', + field=models.CharField(max_length=64), + ), + ] diff --git a/backend/api/migrations/0023_auto_20210104_0810.py b/backend/api/migrations/0023_auto_20210104_0810.py new file mode 100644 index 00000000..6816b1e1 --- /dev/null +++ b/backend/api/migrations/0023_auto_20210104_0810.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.13 on 2021-01-04 08:10 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0022_auto_20210104_0807'), + ] + + operations = [ + migrations.AlterField( + model_name='team', + name='divisions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('highschool', 'High School'), ('newbie', 'Newbie'), ('college', 'College'), ('pro', 'Pro')]), default=list, size=None), + ), + migrations.AlterField( + model_name='tournament', + name='divisions', + field=models.TextField(blank=True), + ), + ] diff --git a/backend/api/migrations/0024_scrimmage_map_ids.py b/backend/api/migrations/0024_scrimmage_map_ids.py new file mode 100644 index 00000000..e34c5fa0 --- /dev/null +++ b/backend/api/migrations/0024_scrimmage_map_ids.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2021-01-04 09:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0023_auto_20210104_0810'), + ] + + operations = [ + migrations.AddField( + model_name='scrimmage', + name='map_ids', + field=models.TextField(blank=True), + ), + ] diff --git a/backend/api/migrations/0025_auto_20210104_0921.py b/backend/api/migrations/0025_auto_20210104_0921.py new file mode 100644 index 00000000..a352a369 --- /dev/null +++ b/backend/api/migrations/0025_auto_20210104_0921.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2021-01-04 09:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0024_scrimmage_map_ids'), + ] + + operations = [ + migrations.AlterField( + model_name='scrimmage', + name='map_ids', + field=models.TextField(null=True), + ), + ] diff --git a/backend/api/migrations/0026_auto_20210105_0702.py b/backend/api/migrations/0026_auto_20210105_0702.py new file mode 100644 index 00000000..631daac8 --- /dev/null +++ b/backend/api/migrations/0026_auto_20210105_0702.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.13 on 2021-01-05 07:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0025_auto_20210104_0921'), + ] + + operations = [ + migrations.AddField( + model_name='scrimmage', + name='blue_submission_id', + field=models.IntegerField(null=True), + ), + migrations.AddField( + model_name='scrimmage', + name='red_submission_id', + field=models.IntegerField(null=True), + ), + ] diff --git a/backend/api/migrations/0027_auto_20210106_0308.py b/backend/api/migrations/0027_auto_20210106_0308.py new file mode 100644 index 00000000..0f957ad0 --- /dev/null +++ b/backend/api/migrations/0027_auto_20210106_0308.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.13 on 2021-01-06 03:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0026_auto_20210105_0702'), + ] + + operations = [ + migrations.AddField( + model_name='scrimmage', + name='error_msg', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='submission', + name='error_msg', + field=models.TextField(blank=True), + ), + ] diff --git a/backend/api/migrations/0028_auto_20210107_0126.py b/backend/api/migrations/0028_auto_20210107_0126.py new file mode 100644 index 00000000..4686e857 --- /dev/null +++ b/backend/api/migrations/0028_auto_20210107_0126.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2021-01-07 01:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0027_auto_20210106_0308'), + ] + + operations = [ + migrations.AlterField( + model_name='scrimmage', + name='error_msg', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/backend/api/migrations/0029_auto_20210107_0137.py b/backend/api/migrations/0029_auto_20210107_0137.py new file mode 100644 index 00000000..64f4e5aa --- /dev/null +++ b/backend/api/migrations/0029_auto_20210107_0137.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2021-01-07 01:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0028_auto_20210107_0126'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='error_msg', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index f268aa9a..93be9524 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -81,7 +81,10 @@ class Tournament(models.Model): name = models.TextField() style = models.TextField(choices=TOURNAMENT_STYLE_CHOICES) date_time = models.DateTimeField() - divisions = fields.ArrayField(models.TextField(choices=TOURNAMENT_DIVISION_CHOICES), blank=True, default=list) + # Allow for divisions to be anything. + # This could be dangerous, but I don't think we use divsions in our code anywhere else. + # divisions = fields.ArrayField(models.TextField(choices=TOURNAMENT_DIVISION_CHOICES), blank=True, default=list) + divisions = models.TextField(blank=True) stream_link = models.TextField(blank=True) hidden = models.BooleanField(default=True) bracket_link = models.TextField(blank=True) @@ -114,6 +117,8 @@ class Team(models.Model): draws = models.IntegerField(default=0) #eligibility + # NOTE -- these columns are unfortunately poorly named. + # If you want to work with them, see backend/docs/ELIGIBILITY.md! student = models.BooleanField(default=False) mit = models.BooleanField(default=False) high_school = models.BooleanField(default=False) @@ -153,6 +158,7 @@ class Submission(models.Model): submitted_at = models.DateTimeField(auto_now_add=True) link = models.TextField(null=True) compilation_status = models.IntegerField(default=0) #0 = in progress, 1 = succeeded, 2 = failed, 3 = server failed + error_msg = models.TextField(null=True, blank=True) def save(self, *args, **kwargs): if self.id is not None: @@ -189,6 +195,7 @@ def __str__(self): class Scrimmage(models.Model): SCRIMMAGE_STATUS_CHOICES = ( + ('created', 'Created'), ('pending', 'Pending'), ('queued', 'Queued'), ('running', 'Running'), @@ -204,9 +211,12 @@ class Scrimmage(models.Model): red_team = models.ForeignKey(Team, null=True, on_delete=models.PROTECT, related_name='red_team') blue_team = models.ForeignKey(Team, null=True, on_delete=models.PROTECT, related_name='blue_team') ranked = models.BooleanField(default=False) + map_ids = models.TextField(null=True) + # Match-running (completed by match runner) - status = models.TextField(choices=SCRIMMAGE_STATUS_CHOICES, default='pending') + status = models.TextField(choices=SCRIMMAGE_STATUS_CHOICES, default='created') + error_msg = models.TextField(null=True, blank=True) winscore = models.IntegerField(null=True) losescore = models.IntegerField(null=True) replay = models.TextField(blank=True) @@ -214,6 +224,8 @@ class Scrimmage(models.Model): # Metadata red_mu = models.IntegerField(null=True) blue_mu = models.IntegerField(null=True) + red_submission_id = models.IntegerField(null=True) + blue_submission_id = models.IntegerField(null=True) requested_by = models.ForeignKey(Team, null=True, on_delete=models.PROTECT, related_name='requested_by') requested_at = models.DateTimeField(auto_now_add=True) started_at = models.DateTimeField(null=True) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 4f0b9325..34d9df8f 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -95,6 +95,7 @@ def update(self, instance, validated_data): """ Update and return an existing user object, given the validated data. """ + instance.email = validated_data.get('email', instance.email) instance.first_name = validated_data.get('first_name', instance.first_name) instance.last_name = validated_data.get('last_name', instance.last_name) instance.date_of_birth = validated_data.get('date_of_birth', instance.date_of_birth) @@ -123,7 +124,7 @@ class SubmissionSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Submission - fields = ('url', 'id', 'team', 'link', 'submitted_at', 'compilation_status') + fields = ('url', 'id', 'team', 'link', 'submitted_at', 'compilation_status', 'error_msg') class TeamSubmissionSerializer(serializers.HyperlinkedModelSerializer): serializer_url_field = LeagueHyperlinkedIdentityField @@ -154,8 +155,8 @@ class ScrimmageSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Scrimmage - fields = ('url', 'id', 'league', 'red_team', 'red_mu', 'blue_team', 'blue_mu', 'ranked', - 'status', 'winscore', 'losescore', 'replay', 'requested_by', 'requested_at', 'started_at', 'updated_at', 'tournament_id') + fields = ('url', 'id', 'league', 'red_team', 'red_mu', 'red_submission_id', 'blue_team', 'blue_mu', 'blue_submission_id', 'ranked', + 'status', 'error_msg', 'winscore', 'losescore', 'replay', 'requested_by', 'requested_at', 'started_at', 'updated_at', 'tournament_id', 'map_ids') read_only_fields = ('url', 'requested_at', 'started_at', 'updated_at') diff --git a/backend/api/views.py b/backend/api/views.py index d40aa0d3..ac4e9021 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -30,6 +30,9 @@ # NOTE: throughout our codebase, we sometimes refer to a pubsub as a "queue", adding a message to a pubsub as "queueing" something, etc. Technically this is not true: the pubsub gives no guarantee at all of a true queue or FIFO order. However, this detail of pubsub order is generally nonconsequential, and when it does matter, we have workarounds for non-FIFO-order cases. +# Some helper methods that don't belong in any one class, +# and shouldn't be directly callable through the api. + # Methods for publishing a message to a pubsub. # Note that data must be a bytestring. # Adapted from https://github.com/googleapis/python-pubsub/blob/master/samples/snippets/quickstart/pub.py @@ -62,15 +65,103 @@ def pub(project_id, topic_name, data, num_retries=5): else: break -def scrimmage_pub_sub_call(red_submission_id, blue_submission_id, red_team_name, blue_team_name, scrimmage_id, scrimmage_replay, map_ids=None): +# TODO at some point, these two methods should be moved into the scrim class. +# by not adding decorators, we can create a method which has no url -- essentially a private helper method. +# moving into scrim class would make more sense conceptually / for organization. +def create_scrimmage_helper(red_team_id, blue_team_id, ranked, requested_by, is_tour_match, tournament_id, accept, league, map_ids): + # Don't use status as a var name, to avoid some http status enum + scrim_status = 'created' + # String used to associate to a replay file/link. + # Sufficiently random, to ensure privacy (so that others can't guess the link and find a replay). + replay = binascii.b2a_hex(os.urandom(15)).decode('utf-8') + + # get team submission ids and names, with careful attention to tour matches + red_team_sub = TeamSubmission.objects.get(pk=red_team_id) + blue_team_sub = TeamSubmission.objects.get(pk=blue_team_id) + if is_tour_match: + tour = Tournament.objects.get(pk=int(tournament_id)) + column_name = tour.teamsubmission_column_name + red_submission_id = getattr(red_team_sub, column_name) + blue_submission_id = getattr(blue_team_sub, column_name) + else: + red_submission_id = red_team_sub.last_1_id + blue_submission_id = blue_team_sub.last_1_id + red_team_name = Team.objects.get(pk=red_team_id).name + blue_team_name = Team.objects.get(pk=blue_team_id).name + + # no need to set blue rating, red rating for ranked matches -- this is actually done when the outcome is set + + # if map_ids is not specified, use some default way to select maps. + if map_ids is None: + # By default, pick 3 random maps (requires specifying maps in settings.py). + map_ids = ','.join(get_random_maps(3)) + + # TODO we save red_team_id, etc by passing the red_team name to the serializer; the serializer queries the db, and find the corresponding team, and gets its team ID. This is really inefficient (since we already have IDs to start); also, if we have dupe team names, this query fails. + # We should change this, although not sure how best. (Perhaps as easy as removing SlugRelatedFields in serializers, and then passing in IDs.) + data = { + 'league': league, + 'red_team': red_team_name, + 'blue_team': blue_team_name, + 'red_submission_id': red_submission_id, + 'blue_submission_id': blue_submission_id, + 'ranked': ranked, + 'requested_by': requested_by, + 'tournament_id': tournament_id, + 'replay': replay, + 'map_ids': map_ids, + } + + serializer = ScrimmageSerializer(data=data) + if not serializer.is_valid(): + return Response(serializer.errors, status.HTTP_400_BAD_REQUEST) + scrimmage = serializer.save() + + # If applicable, immediately accept scrimmage, rather than wait for the other team to accept. + if accept: + result = queue_scrimmage_helper(scrimmage.id) + else: + scrimmage.status = 'pending' + scrimmage.save() + result = Response({'message': scrimmage.id}, status.HTTP_200_OK) + return result + +def queue_scrimmage_helper(scrimmage_id): + scrimmage = Scrimmage.objects.get(pk=scrimmage_id) + # put onto pubsub + # TODO if called through create_scrimmage_helper, then a lot of these queries are performed twice in succession, once in each method. Could use optimization. + # for example, pass data from create_scrimmage_helper into queue_scrimmage_helper, as an argument, and get your values from there. + red_team_id = scrimmage.red_team.id + blue_team_id = scrimmage.blue_team.id + red_submission_id = scrimmage.red_submission_id + blue_submission_id = scrimmage.blue_submission_id + red_team_name = Team.objects.get(pk=red_team_id).name + blue_team_name = Team.objects.get(pk=blue_team_id).name + replay = scrimmage.replay + map_ids = scrimmage.map_ids + tourmode = (scrimmage.tournament_id != -1) + scrimmage_pub_sub_call(red_submission_id, blue_submission_id, red_team_name, blue_team_name, scrimmage.id, replay, map_ids, tourmode) + + # save the scrimmage, again, to mark save + if red_submission_id is None or blue_submission_id is None: + scrimmage.status = 'error' + scrimmage.error_msg = 'Make sure your team and the team you requested have a submission.' + else: + scrimmage.status = 'queued' + scrimmage.save() + + return Response({'message': scrimmage.id}, status.HTTP_200_OK) + +def scrimmage_pub_sub_call(red_submission_id, blue_submission_id, red_team_name, blue_team_name, scrimmage_id, scrimmage_replay, map_ids, tourmode): - print('attempting publication to scrimmage pub/sub') if red_submission_id is None and blue_submission_id is None: return Response({'message': 'Both teams do not have a submission.'}, status.HTTP_400_BAD_REQUEST) if red_submission_id is None: return Response({'message': 'Red team does not have a submission.'}, status.HTTP_400_BAD_REQUEST) if blue_submission_id is None: return Response({'message': 'Blue team does not have a submission.'}, status.HTTP_400_BAD_REQUEST) + # gametype is intended to always be scrimmage: + # the infra uses gametype to figure out the url it should report results to, and we only have a set of urls for scrimmage. + # TODO change gametype here to actually be meaningful, and in the infra, always send to url of 'scrimmage' scrimmage_server_data = { 'gametype': 'scrimmage', 'gameid': str(scrimmage_id), @@ -78,11 +169,14 @@ def scrimmage_pub_sub_call(red_submission_id, blue_submission_id, red_team_name, 'player2': str(blue_submission_id), 'name1': str(red_team_name), 'name2': str(blue_team_name), - 'replay': scrimmage_replay + 'maps': str(map_ids), + 'replay': scrimmage_replay, + 'tourmode': tourmode } - if not map_ids is None: - scrimmage_server_data['maps'] = map_ids data_bytestring = json.dumps(scrimmage_server_data).encode('utf-8') + # In testing, it's helpful to comment out the actual pubsub call, and print what would be added instead, so you can see it. + # Make sure to revert this before pushing to master and deploying! + # print(data_bytestring) pub(GCLOUD_PROJECT, GCLOUD_SUB_SCRIMMAGE_NAME, data_bytestring) def get_random_maps(num): @@ -110,7 +204,7 @@ def get_blob(file_path, bucket): return blob @staticmethod - def signed_upload_url(file_path, bucket): + def signed_upload_url(file_path, bucket, origin): """ returns a pre-signed url for uploading the submission with given id to google cloud this URL can be used with a PUT request to upload data; no authentication needed. @@ -121,7 +215,7 @@ def signed_upload_url(file_path, bucket): # https://stackoverflow.com/questions/25688608/xmlhttprequest-cors-to-google-cloud-storage-only-working-in-preflight-request # https://stackoverflow.com/questions/46971451/cors-request-made-despite-error-in-console # https://googleapis.dev/python/storage/latest/blobs.html - return blob.create_resumable_upload_session(origin=settings.THIS_URL) + return blob.create_resumable_upload_session(origin=origin) @staticmethod def signed_download_url(file_path, bucket): @@ -138,6 +232,11 @@ def signed_download_url(file_path, bucket): class SearchResultsPagination(PageNumberPagination): page_size = 10 + def get_page_size(self, request): + if 'page' in request.query_params: + return self.page_size + return None + class PartialUpdateModelMixin(mixins.UpdateModelMixin): def update(self, request, partial=False, league_id=None, pk=None): @@ -172,9 +271,13 @@ class UserViewSet(viewsets.GenericViewSet, serializer_class = FullUserSerializer permission_classes = (IsAuthenticatedAsRequestedUser,) - @action(detail=True, methods=['get']) + @action(detail=True, methods=['post']) def resume_upload(self, request, pk=None): - upload_url = GCloudUploadDownload.signed_upload_url(RESUME_FILENAME(pk), GCLOUD_RES_BUCKET) + # Note that post requests always include Origin headers + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin + # https://stackoverflow.com/questions/42239643/when-do-browsers-send-the-origin-header-when-do-browsers-set-the-origin-to-null + origin = request.headers['Origin'] + upload_url = GCloudUploadDownload.signed_upload_url(RESUME_FILENAME(pk), GCLOUD_RES_BUCKET, origin) user = self.queryset.get(pk=pk) user.verified = True user.save() @@ -241,7 +344,7 @@ def scrimmage_list(self, request): ratings.sort() # Partition into blocks, and round robin in each block - IDEAL_BLOCK_SIZE = 5 + IDEAL_BLOCK_SIZE = 4 block_sizes = [IDEAL_BLOCK_SIZE] * (len(ratings) // IDEAL_BLOCK_SIZE) num_blocks = len(block_sizes) for i in range(len(ratings) % IDEAL_BLOCK_SIZE): @@ -263,10 +366,12 @@ def scrimmage_list(self, request): # where the offset is randomly determined between 5 and 15 scatter_step = random.randint(5,15) for i in range(len(ratings)-scatter_step): - scrim_list.append({ - "player1": ratings[i][2].id, - "player2": ratings[i+scatter_step][2].id - }) + # Each team only gets one pair + if (i % (2*scatter_step)) < scatter_step: + scrim_list.append({ + "player1": ratings[i][2].id, + "player2": ratings[i+scatter_step][2].id + }) return Response({'matches': scrim_list}, status.HTTP_200_OK) @@ -275,84 +380,45 @@ def scrimmage_list(self, request): @action(detail=False, methods=['post']) def enqueue(self, request): - is_admin = User.objects.all().get(username=request.user).is_superuser + user = User.objects.all().get(username=request.user) + is_admin = user.is_superuser if is_admin: + # TODO multiple accesses to request.data are annyoing; replace w setting to a stingle var, data match_type = request.data.get("type") if match_type == "scrimmage" or match_type == "tour_scrimmage": - team_1 = Team.objects.get(pk=request.data.get("player1")) - team_2 = Team.objects.get(pk=request.data.get("player2")) - team_sub_1 = TeamSubmission.objects.get(pk=team_1.id) - team_sub_2 = TeamSubmission.objects.get(pk=team_2.id) - sub_1 = team_sub_1.last_1_id - sub_2 = team_sub_2.last_1_id - if match_type == "tour_scrimmage": - tour = Tournament.objects.get(pk=int(request.data.get("tournament_id"))) - column_name = tour.teamsubmission_column_name - sub_1 = getattr(team_sub_1, column_name) - sub_2 = getattr(team_sub_2, column_name) - scrimmage = { - 'league': 0, - 'red_team': team_1.name, - 'blue_team': team_2.name, - 'requested_by': team_1.id, - 'ranked': True, - 'replay': binascii.b2a_hex(os.urandom(15)).decode('utf-8'), - 'status': 'queued' - } - map_ids = None - if match_type == "tour_scrimmage": - tour_id = int(request.data.get("tournament_id")) - scrimmage['tournament_id'] = tour_id - map_ids = request.data.get("map_ids") + team_1_id = request.data.get("player1") + team_2_id = request.data.get("player2") - ScrimSerial = ScrimmageSerializer(data=scrimmage) - if not ScrimSerial.is_valid(): - return Response(ScrimSerial.errors, status.HTTP_400_BAD_REQUEST) - scrim = ScrimSerial.save() - print("team names are", team_1.name, team_2.name) - # scrimmage_pub_sub_call(sub_1, sub_2, team_1.name, team_2.name, scrim.id, scrim.replay, map_ids) - scrimmage_pub_sub_call(sub_1, sub_2, team_1.name, team_2.name, scrim.id, scrim.replay) - # print("team names are", team_1.name, team_2.name) - return Response({'message': scrim.id}, status.HTTP_200_OK) + is_tour_match = (match_type == "tour_scrimmage") + + if is_tour_match: + # Tour matches are unranked. + ranked = False + tournament_id = int(request.data.get("tournament_id")) + # In a tour, we use specific maps for each round. + # Infra handles picking the maps, and sends it through the request. + map_ids = request.data.get("map_ids") + else: + # For now regular matches created automatically are ranked; subjject to change. + ranked = True + # tournament_id of -1 indicates a normal scrimmage match (a non-tour match). + tournament_id = -1 + # Use default map selection + map_ids = None + + # TODO admin_team has to be requeried every time. Easier to run this once (like as a setting, kinda). + admin_team = Team.objects.get(users__username=user.username) + requested_by = admin_team.id + + league = 0 + + result = create_scrimmage_helper(team_1_id, team_2_id, ranked, requested_by, is_tour_match, tournament_id, True, league, map_ids) + return result else: return Response({'message': 'unsupported match type'}, status.HTTP_400_BAD_REQUEST) else: return Response({'message': 'make this request from server account'}, status.HTTP_401_UNAUTHORIZED) - # Kept only for reverse-compatibility with infrastructure, no longer needed - def actually_generate_matches(self, request): - scrimmage_list = self.scrimmage_list(request).data['matches'] - for scrim in scrimmage_list: - team_1 = Team.objects.get(pk=scrim["player1"]) - team_2 = Team.objects.get(pk=scrim["player2"]) - sub_1 = TeamSubmission.objects.get(pk=team_1.id).last_1_id - sub_2 = TeamSubmission.objects.get(pk=team_2.id).last_1_id - scrimmage = { - 'league': 0, - 'red_team': team_1.name, - 'blue_team': team_2.name, - 'requested_by': team_1.id, - 'ranked': True, - 'replay': binascii.b2a_hex(os.urandom(15)).decode('utf-8'), - 'status': 'queued' - } - - ScrimSerial = ScrimmageSerializer(data=scrimmage) - if not ScrimSerial.is_valid(): - return Response(ScrimSerial.errors, status.HTTP_400_BAD_REQUEST) - scrim = ScrimSerial.save() - scrimmage_pub_sub_call(sub_1, sub_2, team_1.name, team_2.name, scrim.id, scrim.replay) - - # Kept only for reverse-compatibility with infrastructure, no longer needed - @action(detail=False, methods=['post']) - def generate_matches(self, request): - is_admin = User.objects.all().get(username=request.user).is_superuser - if is_admin: - threading.Thread(target=self.actually_generate_matches, args=(request,)).start() - return Response({'message': 'matches are being generated!'}, status.HTTP_202_ACCEPTED) - else: - return Response({'message': 'make this request from server account'}, status.HTTP_401_UNAUTHORIZED) - class UserTeamViewSet(viewsets.ReadOnlyModelViewSet): """ @@ -548,14 +614,14 @@ def history(self, request, league_id, pk=None): return_data = [] # loop through all scrimmages involving this team - # only add ranked scriammges, and scrimmages with no tournament ID + # only add ranked, non-tournament scrimmages # add entry to result array defining whether or not this team won and time of scrimmage for scrimmage in scrimmages: - if (scrimmage.ranked and scrimmage.tournament_id is None): + if (scrimmage.ranked and scrimmage.tournament_id == -1): won_as_red = (scrimmage.status == 'redwon' and scrimmage.red_team_id == team_id) won_as_blue = (scrimmage.status == 'bluewon' and scrimmage.blue_team_id == team_id) team_mu = scrimmage.red_mu if scrimmage.red_team_id == team_id else scrimmage.blue_mu - return_data.append({'won': (won_as_red or won_as_blue) if (scrimmage.status != 'error') else None, + return_data.append({'won': (won_as_red or won_as_blue) if (scrimmage.status == 'redwon' or scrimmage.status == 'bluewon') else None, 'date': scrimmage.updated_at, 'mu': team_mu}) return Response(return_data, status.HTTP_200_OK) @@ -657,7 +723,8 @@ def create(self, request, team, league_id): team.score = settings.ELO_START team.save() - upload_url = GCloudUploadDownload.signed_upload_url(SUBMISSION_FILENAME(serializer.data['id']), GCLOUD_SUB_BUCKET) + origin = request.headers['Origin'] + upload_url = GCloudUploadDownload.signed_upload_url(SUBMISSION_FILENAME(serializer.data['id']), GCLOUD_SUB_BUCKET, origin) return Response({'upload_url': upload_url, 'submission_id': submission.id}, status.HTTP_201_CREATED) @@ -854,6 +921,7 @@ class ScrimmageViewSet(viewsets.GenericViewSet, that requested the scrimmage. """ queryset = Scrimmage.objects.all().order_by('-requested_at') + pagination_class = SearchResultsPagination serializer_class = ScrimmageSerializer permission_classes = (SubmissionsEnabledOrSafeMethodsOrIsSuperuser, IsAuthenticatedOnTeam, IsStaffOrGameReleased) @@ -873,7 +941,7 @@ def get_submission(self, team_id): def get_queryset(self): team = self.kwargs['team'] - return super().get_queryset().filter((Q(red_team=team) | Q(blue_team=team)) & Q(tournament_id=None)) + return super().get_queryset().filter((Q(red_team=team) | Q(blue_team=team)) & Q(tournament_id=-1)) def get_serializer_context(self): context = super().get_serializer_context() @@ -895,7 +963,8 @@ def create(self, request, league_id, team): red_team_id = int(request.data['red_team']) blue_team_id = int(request.data['blue_team']) # ranked = request.data['ranked'] == 'True' - ranked = True + # Scrimmages created by regular challenges should not be ranked, to prevent ladder manipulation. + ranked = False # Validate teams team = self.kwargs['team'] @@ -910,34 +979,23 @@ def create(self, request, league_id, team): if that_team is None: return Response({'message': 'Requested team does not exist'}, status.HTTP_404_NOT_FOUND) - replay_string = binascii.b2a_hex(os.urandom(15)).decode('utf-8') - data = { - 'league': league_id, - 'red_team': red_team.name, - 'blue_team': blue_team.name, - 'ranked': ranked, - 'requested_by': this_team.id, - 'replay': replay_string, - } + requested_by = this_team.id + is_tour_match = False + # tournament_id of -1 indicates a normal scrimmage match (a non-tour match). + tournament_id = -1 # Check auto accept if (ranked and that_team.auto_accept_ranked) or (not ranked and that_team.auto_accept_unranked): - data['status'] = 'queued' + accept = True + else: + accept = False - serializer = self.get_serializer(data=data) - if not serializer.is_valid(): - return Response(serializer.errors, status.HTTP_400_BAD_REQUEST) - scrimmage = serializer.save() + # Use default map selection + map_ids = None - # check the ID - # if auto accept, then create scrimmage - if (ranked and that_team.auto_accept_ranked) or (not ranked and that_team.auto_accept_unranked): - red_submission_id = TeamSubmission.objects.get(pk=scrimmage.red_team_id).last_1_id - blue_submission_id = TeamSubmission.objects.get(pk=scrimmage.blue_team_id).last_1_id - red_team_name = Team.objects.get(pk=scrimmage.red_team_id).name - blue_team_name = Team.objects.get(pk=scrimmage.blue_team_id).name - scrimmage_pub_sub_call(red_submission_id, blue_submission_id, red_team_name, blue_team_name, scrimmage.id, scrimmage.replay) - return Response(serializer.data, status.HTTP_201_CREATED) + result = create_scrimmage_helper(red_team_id, blue_team_id, ranked, requested_by, is_tour_match, tournament_id, accept, league_id, map_ids) + + return result except Exception as e: error = {'message': ','.join(e.args) if len(e.args) > 0 else 'Unknown Error'} return Response(error, status.HTTP_400_BAD_REQUEST) @@ -951,16 +1009,8 @@ def accept(self, request, league_id, team, pk=None): if scrimmage.status != 'pending': return Response({'message': 'Scrimmage is not pending.'}, status.HTTP_400_BAD_REQUEST) - scrimmage.status = 'queued' - scrimmage.save() - red_submission_id = TeamSubmission.objects.get(pk=scrimmage.red_team_id).last_1_id - blue_submission_id = TeamSubmission.objects.get(pk=scrimmage.blue_team_id).last_1_id - red_team_name = scrimmage.red_team.name - blue_team_name = scrimmage.blue_team.name - scrimmage_pub_sub_call(red_submission_id, blue_submission_id, red_team_name, blue_team_name, scrimmage.id, scrimmage.replay) - - serializer = self.get_serializer(scrimmage) - return Response(serializer.data, status.HTTP_200_OK) + result = queue_scrimmage_helper(scrimmage.id) + return result except Scrimmage.DoesNotExist: return Response({'message': 'Scrimmage does not exist.'}, status.HTTP_404_NOT_FOUND) @@ -998,6 +1048,23 @@ def cancel(self, request, league_id, team, pk=None): except Scrimmage.DoesNotExist: return Response({'message': 'Scrimmage does not exist.'}, status.HTTP_404_NOT_FOUND) + @action(methods=['post'], detail=True) + def requeue(self, request, league_id, team, pk=None): + is_admin = User.objects.all().get(username=request.user).is_superuser + if is_admin: + try: + scrimmage = Scrimmage.objects.all().get(pk=pk) + except: + return Response({'message': 'Scrimmage does not exist.'}, status.HTTP_404_NOT_FOUND) + + if scrimmage.status in ('redwon', 'bluewon'): + return Response({'message': 'Success response already received for this scrimmage'}, status.HTTP_400_BAD_REQUEST) + + response = queue_scrimmage_helper(scrimmage.id) + return response + else: + return Response({'message': 'make this request from server account'}, status.HTTP_401_UNAUTHORIZED) + @action(methods=['patch'], detail=True) def set_outcome(self, request, league_id, team, pk=None): is_admin = User.objects.all().get(username=request.user).is_superuser @@ -1009,6 +1076,7 @@ def set_outcome(self, request, league_id, team, pk=None): if 'status' in request.data: sc_status = request.data['status'] + sc_error_msg = request.data['error_msg'] if sc_status == "redwon" or sc_status == "bluewon": if 'winscore' in request.data and 'losescore' in request.data: @@ -1024,8 +1092,13 @@ def set_outcome(self, request, league_id, team, pk=None): scrimmage.winscore = sc_winscore scrimmage.losescore = sc_losescore + if 'new_replay' in request.data: + sc_new_replay = request.data['new_replay'] + if sc_new_replay is not None: + scrimmage.replay = sc_new_replay + # if tournament, then return here - if scrimmage.tournament_id is not None: + if scrimmage.tournament_id != -1: scrimmage.save() return Response({'status': sc_status, 'winscore': sc_winscore, 'losescore': sc_losescore}, status.HTTP_200_OK) @@ -1064,10 +1137,10 @@ def set_outcome(self, request, league_id, team, pk=None): return Response({'status': sc_status, 'winscore': sc_winscore, 'losescore': sc_losescore}, status.HTTP_200_OK) elif sc_status == "error": scrimmage.status = sc_status - + scrimmage.error_msg = sc_error_msg scrimmage.save() # Return 200, because the scrimmage runner should be informed that it successfully sent the error status to the backend - return Response({'status': sc_status, 'winscore': None, 'losescore': None}, status.HTTP_200_OK) + return Response({'status': sc_status}, status.HTTP_200_OK) else: return Response({'message': 'Set scrimmage to pending/queued/cancelled with accept/reject/cancel api calls'}, status.HTTP_400_BAD_REQUEST) else: diff --git a/backend/backend_script.py b/backend/backend_script.py index 7d23e415..922f7ad5 100644 --- a/backend/backend_script.py +++ b/backend/backend_script.py @@ -13,20 +13,15 @@ response = requests.post(domain + 'auth/token/', data=data) token = json.loads(response.text)['access'] -# data = { -# 'type': 'tour_scrimmage', -# 'tournament_id': '-1', -# 'player1': '917', -# 'player2': '919' -# } data = { - 'status': 'error', - 'winscore': None, - 'losescore': None + 'type': 'scrimmage', + 'tournament_id': '0', + 'player1': '1744', + 'player2': '1810', } + headers = {"Authorization": "Bearer " + token} -# response = requests.post(domain + 'api/match/enqueue/', data=data, headers=headers) -response = requests.patch(domain + 'api/0/scrimmage/1/set_outcome/', data=data, headers=headers) +response = requests.post(domain + 'api/match/enqueue/', data=data, headers=headers) print(response.text) diff --git a/backend/docs/ELIGIBILITY.md b/backend/docs/ELIGIBILITY.md new file mode 100644 index 00000000..bbbe56a0 --- /dev/null +++ b/backend/docs/ELIGIBILITY.md @@ -0,0 +1,13 @@ +# Database and Eligibility columns + +In our team table of the database are four columns: `high_school`, `international`, `mit`, and `student`. Unfortunately, these names don't actually mean what they may seem at first glance... + +`high_school=True` means that the team is all high school students. (This should be a strict subset of `student` -- that is, `student` can not be false while `high_school` is true, unless someone filled something out wrong.) + +`international=True` means that the team is **not [all (US students)]**. (i.e. at least one non-student and/or one int'l person.) The value of `international` is the boolean opposite of the "US students" checkbox in the frontend. **A team participates in the Intl Tournament if and only if `international=True` and `student=True`.** + +`mit=True` means that the team is all **newbies**. + +`student=True` means that the team is all full-time students. + +(Changing the column names requires either server downtime or messy workarounds. Perhaps when the competition isn't active, it'd be a good idea to rethink what information we hold about a team, or what the columns are named.) \ No newline at end of file diff --git a/backend/docs/SETUP.md b/backend/docs/SETUP.md index 29b705a0..5d796a3c 100644 --- a/backend/docs/SETUP.md +++ b/backend/docs/SETUP.md @@ -58,7 +58,7 @@ Next, we need to register a superuser account (for use by the infra). Run the ba Also, have this superuser create and join a team (this is necessary for some permissions). Then, go back to your Postgres editor. In `api_user`, find the user you just created. Change `is_superuser` and `is_staff` to true. Finally, pass the username and password of this account to the infrastructure team. -Then stop the old database (on its main page, press "stop"). +Then stop the old database (on its main page, press "stop"). **Don't delete it!** It's free to keep a stopped database, and handy to have around for future development. ## Deployment Setup @@ -72,7 +72,7 @@ After registering a domain name for the competition, set `THIS_URL` (in `setting ### Storage Buckets Go to "Storage" on GCP console. A bucket for submissions should have been created (if not, instructions are in the infrastructure readme.) -Set up the CORS policy, which allows us to upload to the bucket on external websites. Find `docs/cors,json`; in there, update the domain URLs listed. Then, run `gsutil cors set path/to/cors.json gs://bc21-submissions` (updating the bucket name) to whatever it is this year. +Set up the CORS policy, which allows us to upload to the bucket on external websites. Find `docs/cors,json`; in there, update the domain URLs listed. Then, run `gsutil cors set path/to/cors.json gs://bc21-submissions` (updating the bucket name) to whatever it is this year. Similarly, also run `gsutil cors set path/to/cors.json gs://bc21-replays`. More info is here: https://cloud.google.com/storage/docs/configuring-cors#gsutil ### Cloud Build Triggers diff --git a/backend/resumes/download.py b/backend/resumes/download.py new file mode 100644 index 00000000..1aabc53a --- /dev/null +++ b/backend/resumes/download.py @@ -0,0 +1,125 @@ +import os, csv +from google.oauth2 import service_account +from google.cloud import storage +FILE_PATH = os.path.dirname(__file__) +# cd up, to be able to import GOOGLE_APPLICATION_CREDENTIALS +# (we still preserve the original path to use later) +os.sys.path.append(os.path.join(FILE_PATH, '..')) +from dev_settings_sensitive import GOOGLE_APPLICATION_CREDENTIALS + +# constants, please configure +# NOTE - Make sure to update GCLOUD_BUCKET_RESUMES! +GCLOUD_BUCKET_RESUMES = 'bc21-resumes' +USERS_ALL_PATH = os.path.join(FILE_PATH, 'users_all.csv') +USERS_TEAMS_PATH = os.path.join(FILE_PATH, 'users_teams.csv') +NUM_RETRIES = 5 + +# load up the sql query results, as list of dictionaries +users_all = [] +users_all_header = [] +with open(USERS_ALL_PATH, 'r') as csvfile: + reader = csv.reader(csvfile) + users_all_header = next(reader) + for row in reader: + row_dict = dict() + for i in range (0, len(row)): + key = users_all_header[i] + row_dict[key] = row[i] + users_all.append(row_dict) + +users_teams = [] +users_teams_header = [] +with open(USERS_TEAMS_PATH, 'r') as csvfile: + reader = csv.reader(csvfile) + users_teams_header = next(reader) + for row in reader: + row_dict = dict() + for i in range (0, len(row)): + key = users_teams_header[i] + if key in ["high_school", "international", "student"]: + # we want booleans for these + row_dict[key] = bool(row[i]) + else: + row_dict[key] = row[i] + users_teams.append(row_dict) + +# initialize google bucket things +with open('gcloud-key.json', 'w') as outfile: + outfile.write(GOOGLE_APPLICATION_CREDENTIALS) + outfile.close() +os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = str(os.path.join(FILE_PATH, 'gcloud-key.json')) +client = storage.client.Client() +os.remove('gcloud-key.json') # important!!! +bucket = client.get_bucket(GCLOUD_BUCKET_RESUMES) + +# initialize file paths for downloads +def safe_makedirs(directory): + if not os.path.exists(directory): + os.makedirs(directory) +files_dir = os.path.join(FILE_PATH, 'files') +safe_makedirs(files_dir) +hs_us_dir = os.path.join(files_dir, 'hs-us') +safe_makedirs(hs_us_dir) +hs_intl_dir = os.path.join(files_dir, 'hs-intl') +safe_makedirs(hs_intl_dir) +col_us_dir = os.path.join(files_dir, 'col-us') +safe_makedirs(col_us_dir) +col_intl_dir = os.path.join(files_dir, 'col-intl') +safe_makedirs(col_intl_dir) +other_dir = os.path.join(files_dir, 'other') +safe_makedirs(other_dir) +os.chmod(files_dir, 0o777) + +# download helper! +def download(user_id, file_name, bucket, files_dir): + for i in range (NUM_RETRIES): + try: + blob = bucket.get_blob(os.path.join(str(user_id), 'resume.pdf')) + with open(os.path.join(files_dir, file_name), 'wb+') as file_obj: + blob.download_to_file(file_obj) + file_obj.close() + break + except PermissionError: + print("Could not obtain permissions to save; try running as sudo") + except Exception as e: + print("Could not retrieve source file from bucket, user id", user_id) + print("Exception:", e) + +# actually download resumes, first from users_teams +def download_user(user, bucket, files_dir): + if 'student' in user: # user comes from users_teams + if user['student']: + if user['high_school']: + if user['international']: + subfolder = 'hs-intl' + else: #domestic + subfolder = 'hs-us' + else: # college + if user['international']: + subfolder = 'col-intl' + else: + subfolder = 'col-us' + else: + subfolder = 'other' + else: # user comes from users_all + subfolder = 'other' + user_id = user["id"] + # file name: "0ELO-FirstLast" (elo left padded, min 0) + if "student" in user: + elo_int = int(float(user['score'])) + elo_str_padded = str(max(0, elo_int)).zfill(4) + short_file_name = elo_str_padded + "-" + user["first_name"] + user["last_name"] + else: + short_file_name = user["first_name"] + user["last_name"] + full_file_name = subfolder + '/' + short_file_name +'.pdf' + + download(user_id, full_file_name, bucket, files_dir) + +ids_users_downloaded = set() +for user in users_teams: + download_user(user, bucket, files_dir) + ids_users_downloaded.add(user["id"]) +for user in users_all: + if user["id"] not in ids_users_downloaded: + download_user(user, bucket, files_dir) + ids_users_downloaded.add(user["id"]) diff --git a/backend/resumes/notes.txt b/backend/resumes/notes.txt new file mode 100644 index 00000000..efd5f4cb --- /dev/null +++ b/backend/resumes/notes.txt @@ -0,0 +1,13 @@ +First, we need info about all of the users. See `sql.txt` for two scripts, that produce two files (users_all.csv, users_teams.csv). Run the scripts and save the csvs, according to the instructions in sql.txt. + +Next, run the `download.py` script. **Make sure to update GCLOUD_BUCKET_RESUMES!** +For posterity, here's an outline of what it does: +pull all resumes (for all verified ones), preserve user ids +for each group of users (hs us, hs intl, college us, college intl, others that aren't devs): + in ascending scrim rank, find associated resume + rename to "#elo FirstLastResume" +Also for users in users_all not in users_teams find resume as "FirstLastResume" + +Then, the resumes are in the `files` folder! Make sure to go through all of them, to remove any pdfs that seem corrupt, throwaway resumes (eg blank files), etc. + +Then publish them (we usually just upload all of them to a gdrive folder) and share them with sponsors! diff --git a/backend/resumes/sql.txt b/backend/resumes/sql.txt new file mode 100644 index 00000000..db661eee --- /dev/null +++ b/backend/resumes/sql.txt @@ -0,0 +1,20 @@ +Downloading all users with resumes: run the following query. Export (including headers) to a csv and save as `users_all.csv` in this directory. (This export can be done through GUIs, or through the `COPY` or `\copy` commands, here: https://dataschool.com/learn-sql/export-to-csv-from-psql/) + +``` +SELECT api_user.id, api_user.first_name, api_user.last_name FROM api_user +WHERE api_user.verified=True +ORDER BY api_user.id +``` + +----- + +Downloading all competitive resume users _on teams_ and team info: run the following query. Export (including headers) to a csv, save as `users_teams.csv` in this directory. + +``` +SELECT api_user.id, api_user.first_name, api_user.last_name, api_team_users.team_id, api_team.score, api_team.high_school, api_team.international, api_team.student +FROM api_user +LEFT JOIN api_team_users on api_user.id=api_team_users.user_id +LEFT JOIN api_team on api_team_users.team_id=api_team.id +WHERE api_user.verified=True and api_team.staff_team=False +ORDER BY api_user.id +``` diff --git a/backend/settings.py b/backend/settings.py index 53e65e16..da924dca 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -34,57 +34,82 @@ # TODO: update this every time we update maps SERVER_MAPS = [ - "Maze", - "Squares", - "RealArt", - "DoesNotExist", - "IceCream", - "Constriction", - "Islands2", - "Prison", - "DisproportionatelySmallGap", - "Climb", - "TwoLakeLand", - "Europe", - "AMaze", - "BeachFrontProperty", - "Egg", + "maptestsmall", + "circle", + "quadrants", + "Andromeda", + "Arena", + "Bog", + "Branches", + "Chevron", + "Corridor", + "Cow", + "CrossStitch", + "CrownJewels", + "ExesAndOhs", + "FiveOfHearts", + "Gridlock", + "Illusion", + "NotAPuzzle", + "Rainbow", + "SlowMusic", + "Snowflake", + "BadSnowflake", + "CringyAsF", + "FindYourWay", + "GetShrekt", + "Goldfish", + "HexesAndOhms", + "Licc", + "MainCampus", + "Punctuation", + "Radial", + "SeaFloor", + "Sediment", + "Smile", + "SpaceInvaders", + "Surprised", + "VideoGames", + "AmidstWe", + "BattleCode", + "BattleCodeToo", + "BlobWithLegs", + "ButtonsAndBows", + "CowTwister", + "Extensions", "Hourglass", - "MtDoom", - "Sheet4", - "Showerhead", - "Spiral", - "Swirl", - "TheHighGround", - "Toothpaste", - "WhyDidntTheyUseEagles", - "NoU", - "MoreCowbell", - "FourLakeLand", - "CentralLake", - "ALandDivided", - "SoupOnTheSide", - "TwoForOneAndTwoForAll", - "WaterBot", - "CentralSoup", - "ChristmasInJuly", - "CosmicBackgroundRadiation", - "ClearlyTwelveHorsesInASalad", - "CowFarm", - "DidAMonkeyMakeThis", - "GSF", - "Hills", - "InADitch", - "Infinity", - "Islands", - "IsThisProcedural", - "OmgThisIsProcedural", - "ProceduralConfirmed", - "RandomSoup1", - "RandomSoup2", - "Soup", - "Volcano", - "WateredDown", + "Maze", + "NextHouse", + "Superposition", + "TicTacTie", + "UnbrandedWordGame", + "Z", + "Zodiac", + "Flawars", + "FrogOrBath", + "HappyBoba", + "Networking", + "NoInternet", + "PaperWindmill", + "Randomized", + "Star", + "Tiger", + "WhatISeeInMyDreams", + "Yoda", + "Blotches", + "CToE", + "Circles", + "EggCarton", + "InaccurateBritishFlag", + "JerryIsEvil", + "Legends", + "Mario", + "Misdirection", + "OneCallAway", + "Saturn", + "Stonks", + "TheClientMapEditorIsSuperiorToGoogleSheetsEom", + "TheSnackThatSmilesBack", ] # this is the constant used in the ELO calculation diff --git a/backend/tournaments/README.md b/backend/tournaments/README.md new file mode 100644 index 00000000..7e5da704 --- /dev/null +++ b/backend/tournaments/README.md @@ -0,0 +1,39 @@ +# Tournament Presentation + +Basically, everything that isn't directly running the matches themselves. (setup before matches; Challonge input after matches) + +## Beforehand + +TODO: +freeze submissions (allow for grace period) +Allow teams to submit extra submissions, through Discord +To handle these: (insert what Quinn and I just did -- notes in Slack) + +Once all these submissions are processed: +Run some SQL, etc. (find notes in slack, and in tournament.sql) + +## Match Running + +Infrastructure likely knows how to do this! + +## Afterward + +### Installation + +Install pychal, link here -- https://pypi.org/project/pychal/. Also, grab the Challonge API key, link here: https://challonge.com/settings/developer. Set this as `CHALLONGE_API_KEY` in `dev_settings_sensitive.py`. + +On Challonge website, create a new tour. Pay special attention to the "tournament format" section; make sure that is as it should be. Also, to add participants, click on "add participants in bulk". + +Parse results: can do this manually. if you want to do programatically -- +In the initial json, would be nice to have round # (altho i think this can be caluclated), scrimmage id (can be worked around), and score (can be worked around too). + +Get all matches, in json -- they're be in some order. maybe have extra matches too. +Get all matches, from challonge -- this is good canonical order. + +For a match number in challonge: + Use API to query: (this match number wil be 'suggested_play_order') + Turn challonge player_id's into battlecode player IDs + Get any matches involving those two teams, in that order OR create a way of matching challonge rounds to JSON rounds + +TBH we should just run a tour w challonge integration already built in, instead. + diff --git a/backend/tournaments/dev_settings_sensitive.py b/backend/tournaments/dev_settings_sensitive.py new file mode 120000 index 00000000..b227cdc1 --- /dev/null +++ b/backend/tournaments/dev_settings_sensitive.py @@ -0,0 +1 @@ +../dev_settings_sensitive.py \ No newline at end of file diff --git a/backend/tournament.sql b/backend/tournaments/tournament.sql similarity index 100% rename from backend/tournament.sql rename to backend/tournaments/tournament.sql diff --git a/backend/tournaments/update_challonge.py b/backend/tournaments/update_challonge.py new file mode 100644 index 00000000..6418c17b --- /dev/null +++ b/backend/tournaments/update_challonge.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 + +""" +This script updates challonge. +Run `./updatechallonge.py --help` for more info. +THIS SCRIPT ASSUMES THAT THERE ARE NEVER TWO TEAMS THAT ARE MATCHED TO EACH OTHER TWICE. This is +a faulty assumption. Use this script as inspiration for the future, but don't actually run it. +""" + +import challonge +import time +import click +import dev_settings_sensitive + +@click.group() +def cli(): + pass + + +GAMES_PER_MATCH = 5 + +#Configure your settings +challonge.set_credentials("mitbattlecode", dev_settings_sensitive.CHALLONGE_API_KEY) + + +@cli.command() +def list_tournaments(): + # uncommment this to get a new value for s + tournaments = challonge.tournaments.index() + for tournament in tournaments: + print(tournament['name'], tournament['id']) + + + + +def load_tournament(s, tournament_str): + + inp = tournament_str + "replaysraw.txt" + + dd = {} + + ddw = {} + + checkforwinner = {} + + boN = GAMES_PER_MATCH + + # go through every line and parse it and download the replays + # name the replays something informative + redwins = 0 + counter = 0 + with open(inp, 'r') as f: + for l in f.readlines(): + if counter % boN == 0: + redwins = 0 + # create a filename + counter += 1 + teams = l.split('|')[0].strip() + mp = l.split('|')[1].strip().split(' ')[0].strip() + idd = l.split('|')[1].strip().split(' ')[-1].strip() + downloadlink = "https://2020.battlecode.org/replays/" + idd + ".bc20" + fn = "sprint/" + "{:03d}".format(counter) + "______" + teams + "____" + mp + "____" + idd + ".bc20" + displink = "https://2020.battlecode.org/visualizer.html?" + downloadlink + ts = [x.strip() for x in teams.split('-vs-')] + tt = "___".join(sorted([x.strip() for x in teams.split('-vs-')])) + if tt not in dd: + dd[tt] = [displink] + else: + dd[tt].append(displink) + + if counter % boN == 0 or (counter % boN) % 2 != 0: + if 'redwon' in l: + redwins += 1 + else: + if 'redwon' not in l: + redwins += 1 + + if counter % boN == 0 and redwins >= boN//2+1: + ddw[tt] = ts[0] + else: + ddw[tt] = ts[1] + + return dd, ddw + + + + + +def lookupname(s, ps): + return challonge.participants.show(str(s), ps)['name'] + +def findreplays(dd, t1, t2): + tt = "___".join(sorted([t1.strip(), t2.strip()])) + return dd[tt] + + + + +@cli.command() +@click.argument("tournament_id") +@click.argument("tournament_str") +def add_replays(tournament_id, tournament_str): + dd, ddw = load_tournament(tournament_id, tournament_str) + s = tournament_id + r = challonge.matches.index(str(s)) + while True: + r = challonge.matches.index(str(s)) + for d in r: + if (d['state'] =='complete' and d['attachment_count'] is None): + r = findreplays(dd, lookupname(s, d['player1_id']),lookupname(s, d['player2_id'])) + for rr in r: + myparams = {'url': rr} + challonge.match_attachments.create(str(s), d['id'], params=myparams) + print('updated ') + print(d) + time.sleep(10) + +@cli.command() +@click.argument("tournament_id") +@click.argument("tournament_str") +@click.option("--game-limit", help="The index of the first game not to release scores for.") +def update_results(tournament_id, tournament_str, game_limit): + dd, ddw = load_tournament(tournament_id, tournament_str) + s = tournament_id + r = challonge.matches.index(str(s)) + for d in r: + if (d['state'] == 'open' and d['suggested_play_order'] < game_limit): + ts = [lookupname(s, d['player1_id']), lookupname(s, d['player2_id'])] + tt = "___".join(sorted([ts[0].strip(), ts[1].strip()])) + winner_id = ts.index(ddw[tt]) + ss = 'player' + str(winner_id+1) + '_id' + fds = '1-0' + if winner_id == 1: + fds = '0-1' + challonge.matches.update(str(s), d['id'], params={'winner_id': d[ss], 'scores_csv': fds}) + print(d) + + +if __name__ == '__main__': + cli() \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3eb5cd69..d442c6e6 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,7 @@ task headless(type: JavaExec, dependsOn: [':engine:build', ':example-bots:build' '-Dbc.server.map-path=maps', '-Dbc.server.debug=true', '-Dbc.engine.debug-methods=true', + '-Dbc.engine.enable-profiler='+project.property('profilerEnabled'), '-Dbc.game.team-a='+project.property('teamA'), '-Dbc.game.team-b='+project.property('teamB'), '-Dbc.game.team-a.url='+project(':example-bots').sourceSets.main.output.classesDirs.getAsPath(), @@ -79,6 +80,7 @@ task debug(type: JavaExec, dependsOn: [':engine:build', ':example-bots:build']) '-Dbc.server.map-path=maps', '-Dbc.server.debug=true', '-Dbc.engine.debug-methods=true', + '-Dbc.engine.enable-profiler='+project.property('profilerEnabled'), '-Dbc.game.team-a='+project.property('teamA'), '-Dbc.game.team-b='+project.property('teamB'), '-Dbc.game.team-a.url='+project(':example-bots').sourceSets.main.output.classesDirs.getAsPath(), @@ -97,6 +99,7 @@ task headlessX(type: JavaExec, dependsOn: [':engine:build', ':internal-test-bots '-Dbc.server.map-path=maps', '-Dbc.server.debug=true', '-Dbc.engine.debug-methods=true', + '-Dbc.engine.enable-profiler='+project.property('profilerEnabled'), '-Dbc.game.team-a='+project.property('teamA'), '-Dbc.game.team-b='+project.property('teamB'), '-Dbc.game.team-a.url='+project(':internal-test-bots').sourceSets.main.output.classesDirs.getAsPath(), @@ -116,6 +119,7 @@ task debugX(type: JavaExec, dependsOn: [':engine:build', ':internal-test-bots:bu '-Dbc.server.map-path=maps', '-Dbc.server.debug=true', '-Dbc.engine.debug-methods=true', + '-Dbc.engine.enable-profiler='+project.property('profilerEnabled'), '-Dbc.game.team-a='+project.property('teamA'), '-Dbc.game.team-b='+project.property('teamB'), '-Dbc.game.team-a.url='+project(':internal-test-bots').sourceSets.main.output.classesDirs.getAsPath(), @@ -137,7 +141,7 @@ task runFromClient(type: JavaExec, dependsOn: [':engine:build', ':internal-test- '-Dbc.server.debug=false', '-Dbc.server.robot-player-to-system-out=false', '-Dbc.engine.debug-methods=true', - '-Dbc.engine.enable-profiler=false', + '-Dbc.engine.enable-profiler='+project.property('profilerEnabled'), '-Dbc.game.team-a='+project.property('teamA'), '-Dbc.game.team-b='+project.property('teamB'), '-Dbc.game.team-a.url='+project(':internal-test-bots').sourceSets.main.output.classesDirs.getAsPath(), @@ -222,7 +226,15 @@ task release_docs_zip(type: Zip, dependsOn: [':engine:javadoc']) { } task release_sources(type: Jar, dependsOn: classes) { - from sourceSets.main.allSource + classifier = 'sources' + from project(":engine").sourceSets.main.allSource + + baseName = "battlecode-source" + if (project.hasProperty("release_version")) + version = project.property("release_version"); + destinationDir = project.projectDir; + + // from new File(project(":engine").sourceDir, "source") } task prodClient { @@ -284,7 +296,8 @@ publishing { version project.findProperty('release_version') ?: 'NONSENSE' artifact release_main - + + artifact release_docs { classifier 'javadoc' } diff --git a/client/package.json b/client/package.json index 8ec1388a..1d6650d9 100644 --- a/client/package.json +++ b/client/package.json @@ -5,15 +5,15 @@ "main": "visualizer/electron-main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "install-all": "(cd playback && npm install) & (cd visualizer && npm install)", + "install-all": "(cd playback && npm install) && (cd visualizer && npm install)", "clean": "rm -rf dist/", - "build": "(cd playback && npm run build) & (cd visualizer && npm run build)", + "build": "(cd playback && npm run build) && (cd visualizer && npm run build)", "build-playback": "(cd playback && npm run build)", - "electron": "npm run build-playback & (cd visualizer && npm run electron)", - "watch": "npm run build-playback & (cd visualizer && npm run watch)", - "prod-electron": "npm run build-playback & (cd visualizer && npm run prod-electron)", - "prod-electron-no-sign": "npm run build-playback & (cd visualizer && npm run prod-electron-no-sign)", - "prod-test": "npm run build-playback & (cd visualizer && npm run prod-test)" + "electron": "npm run build-playback && (cd visualizer && npm run electron)", + "watch": "npm run build-playback && (cd visualizer && npm run watch)", + "prod-electron": "npm run build-playback && (cd visualizer && npm run prod-electron)", + "prod-electron-no-sign": "npm run build-playback && (cd visualizer && npm run prod-electron-no-sign)", + "prod-test": "npm run build-playback && (cd visualizer && npm run prod-test)" }, "repository": { "type": "git", @@ -24,5 +24,6 @@ "bugs": { "url": "https://github.com/battlecode/battlecode21/issues" }, - "homepage": "https://github.com/battlecode/battlecode21#readme" + "homepage": "https://github.com/battlecode/battlecode21#readme", + "dependencies": {} } diff --git a/client/playback/package-lock.json b/client/playback/package-lock.json index 8e33f33d..384eed44 100644 --- a/client/playback/package-lock.json +++ b/client/playback/package-lock.json @@ -139,8 +139,8 @@ "battlecode-schema": { "version": "file:../../schema", "requires": { - "@types/flatbuffers": "^1.10.0", - "flatbuffers": "^1.12.0" + "@types/flatbuffers": "^1.9.1", + "flatbuffers": "^1.11.0" }, "dependencies": { "@types/flatbuffers": { diff --git a/client/playback/src/game.ts b/client/playback/src/game.ts index beea2059..fe42cb36 100644 --- a/client/playback/src/game.ts +++ b/client/playback/src/game.ts @@ -4,6 +4,11 @@ import { flatbuffers, schema } from 'battlecode-schema'; import Match from './match'; import {ungzip} from 'pako'; +export type playbackConfig = { + processLogs: boolean; + doProfiling: boolean; +} + /** * Represents an entire game. * Contains a Match for every match in a game. @@ -36,13 +41,16 @@ export default class Game { get meta() { return this._meta; } private _meta: Metadata | null; + private config: playbackConfig; + /** * Create a Game with nothing inside. */ - constructor() { + constructor(config: playbackConfig) { this._winner = null; this._matches = new Array(); this._meta = null; + this.config = config; } /** @@ -74,7 +82,7 @@ export default class Game { case schema.Event.MatchHeader: const matchHeader = event.e(new schema.MatchHeader()) as schema.MatchHeader; if (gameStarted && (matchCount === 0 || lastMatchFinished)) { - this._matches.push(new Match(matchHeader, this._meta as Metadata)); + this._matches.push(new Match(matchHeader, this._meta as Metadata, this.config)); } else { throw new Error("Can't create new game when last hasn't finished"); } diff --git a/client/playback/src/gameworld.ts b/client/playback/src/gameworld.ts index f9fd31a1..84e63950 100644 --- a/client/playback/src/gameworld.ts +++ b/client/playback/src/gameworld.ts @@ -1,6 +1,7 @@ import StructOfArrays from './soa'; import Metadata from './metadata'; -import { schema } from 'battlecode-schema'; +import { flatbuffers, schema } from 'battlecode-schema'; +import {playbackConfig} from './game'; // necessary because victor doesn't use exports.default import Victor = require('victor'); @@ -13,6 +14,14 @@ export type DeadBodiesSchema = { y: Int32Array, } +export type EmpowerSchema = { + id: Int32Array, + x: Int32Array, + y: Int32Array + team: Int8Array, + radius: Int32Array +} + export type BodiesSchema = { id: Int32Array, team: Int8Array, @@ -21,9 +30,12 @@ export type BodiesSchema = { y: Int32Array, influence: Int32Array; conviction: Int32Array; - flag: Int8Array; + flag: Int32Array; bytecodesUsed: Int32Array, // TODO: is this needed? - ability: Int8Array + ability: Int8Array, + bid: Int32Array, + parent: Int32Array, + income: Int32Array }; // NOTE: consider changing MapStats to schema to use SOA for better performance, if it has large data @@ -44,7 +56,13 @@ export type TeamStats = { // An array of numbers corresponding to team stats, which map to RobotTypes // Corresponds to robot type (including NONE. length 5) robots: [number, number, number, number, number], - votes: number + votes: number, + influence: [number, number, number, number, number], + conviction: [number, number, number, number, number], + numBuffs: number, + bidderID: number, + bid: number, + income: number }; export type IndicatorDotsSchema = { @@ -67,6 +85,16 @@ export type IndicatorLinesSchema = { blue: Int32Array } +export type Log = { + team: string, // 'A' | 'B' + robotType: string, // All loggable bodies with team + id: number, + round: number, + text: string +}; + + + /** * A frozen image of the game world. * @@ -83,6 +111,11 @@ export default class GameWorld { */ bodies: StructOfArrays; + /** + * Bodies that empowered this round. + */ + empowered: StructOfArrays; + /* * Stats for each team */ @@ -130,6 +163,22 @@ export default class GameWorld { */ meta: Metadata; + /** + * Whether to process logs. + */ + config: playbackConfig; + + /** + * Recent logs, bucketed by round. + */ + logs: Log[][] = []; + + /** + * The ith index of this.logs corresponds to round (i + this.logsShift). + */ + logsShift: number = 1; + + // Cache fields // We pass these into flatbuffers functions to avoid allocations, // but that's it, they don't hold any state @@ -143,10 +192,19 @@ export default class GameWorld { * which should be removed in the current round. */ private abilityRobots: number[] = []; + private bidRobots: number[] = []; - constructor(meta: Metadata) { + constructor(meta: Metadata, config: playbackConfig) { this.meta = meta; + this.empowered = new StructOfArrays({ + id: new Int32Array(0), + x: new Int32Array(0), + y: new Int32Array(0), + team: new Int8Array(0), + radius: new Int32Array(0) + }, 'id'); + this.diedBodies = new StructOfArrays({ id: new Int32Array(0), x: new Int32Array(0), @@ -161,9 +219,12 @@ export default class GameWorld { y: new Int32Array(0), influence: new Int32Array(0), conviction: new Int32Array(0), - flag: new Int8Array(0), + flag: new Int32Array(0), bytecodesUsed: new Int32Array(0), - ability: new Int8Array(0) + ability: new Int8Array(0), + bid: new Int32Array(0), + parent: new Int32Array(0), + income: new Int32Array(0) }, 'id'); @@ -179,7 +240,13 @@ export default class GameWorld { 0, // MUCKRAKER 0, // NONE ], - votes: 0 + votes: 0, + influence: [0, 0, 0, 0, 0], + conviction: [0, 0, 0, 0, 0], + numBuffs: 0, + bidderID: -1, + bid: 0, + income: 0 }); } @@ -225,6 +292,8 @@ export default class GameWorld { this._vecTableSlot1 = new schema.VecTable(); this._vecTableSlot2 = new schema.VecTable(); this._rgbTableSlot = new schema.RGBTable(); + + this.config = config; } loadFromMatchHeader(header: schema.MatchHeader) { @@ -272,7 +341,7 @@ export default class GameWorld { * Create a copy of the world in its current state. */ copy(): GameWorld { - const result = new GameWorld(this.meta); + const result = new GameWorld(this.meta, this.config); result.copyFrom(this); return result; } @@ -291,6 +360,11 @@ export default class GameWorld { this.teamStats.set(key, deepcopy(value)); }); this.mapStats = deepcopy(source.mapStats); + this.empowered.copyFrom(source.empowered); + this.abilityRobots = Array.from(source.abilityRobots); + this.bidRobots = Array.from(source.bidRobots); + this.logs = Array.from(source.logs); + this.logsShift = source.logsShift; } /** @@ -301,12 +375,15 @@ export default class GameWorld { throw new Error(`Bad Round: this.turn = ${this.turn}, round.roundID() = ${delta.roundID()}`); } - // Process votes gained + // Process team info changes for (var i = 0; i < delta.teamIDsLength(); i++) { - var teamID = delta.teamIDs(i); - var statObj = this.teamStats.get(teamID); + let teamID = delta.teamIDs(i); + let statObj = this.teamStats.get(teamID); statObj.votes += delta.teamVotes(i); + statObj.numBuffs = delta.teamNumBuffs(i); + statObj.bidderID = delta.teamBidderIDs(i); + statObj.bid = 0; this.teamStats.set(teamID, statObj); } @@ -330,6 +407,11 @@ export default class GameWorld { // Remove abilities from previous round this.bodies.alterBulk({id: new Int32Array(this.abilityRobots), ability: new Int8Array(this.abilityRobots.length)}); this.abilityRobots = []; + this.empowered.clear(); + + // Remove bids from previous round + this.bodies.alterBulk({id: new Int32Array(this.bidRobots), bid: new Int32Array(this.bidRobots.length)}); + this.bidRobots = []; // Actions if(delta.actionsLength() > 0){ @@ -339,6 +421,8 @@ export default class GameWorld { const action = delta.actions(i); const robotID = delta.actionIDs(i); const target = delta.actionTargets(i); + const body = this.bodies.lookup(robotID); + const teamStatsObj: TeamStats = this.teamStats.get(body.team); switch (action) { // TODO: validate actions? // Actions list from battlecode.fbs enum Action @@ -346,7 +430,8 @@ export default class GameWorld { /// Politicians self-destruct and affect nearby bodies /// Target: none case schema.Action.EMPOWER: - this.bodies.alter({ id: robotID, ability: 1}); + //this.bodies.alter({ id: robotID, ability: 1}); + this.empowered.insert({'id': robotID, 'x': body.x, 'y': body.y, 'team': body.team, 'radius': target}); this.abilityRobots.push(robotID); break; /// Slanderers passively generate influence for the @@ -359,8 +444,23 @@ export default class GameWorld { /// Slanderers turn into Politicians. /// Target: none case schema.Action.CAMOUFLAGE: - const team = this.bodies.lookup(robotID).team; - this.bodies.alter({ id: robotID, ability: (team == 1 ? 4 : 5)}); + + if (body.type !== schema.BodyType.SLANDERER) { + throw new Error("non-slanderer camouflaged"); + } + this.bodies.alter({ id: robotID, type: schema.BodyType.POLITICIAN}); + this.bodies.alter({ id: robotID, ability: (body.team == 1 ? 4 : 5)}); + this.abilityRobots.push(robotID); + + teamStatsObj.robots[schema.BodyType.SLANDERER]--; + teamStatsObj.robots[schema.BodyType.POLITICIAN]++; + + teamStatsObj.conviction[schema.BodyType.SLANDERER] -= body.conviction; + teamStatsObj.conviction[schema.BodyType.POLITICIAN] += body.conviction; + + teamStatsObj.influence[schema.BodyType.SLANDERER] -= body.influence; + teamStatsObj.influence[schema.BodyType.POLITICIAN] += body.influence; + break; /// Muckrakers can expose a scandal. /// Target: an enemy body. case schema.Action.EXPOSE: @@ -375,28 +475,38 @@ export default class GameWorld { /// Builds a unit (enlightent center). /// Target: spawned unit case schema.Action.SPAWN_UNIT: + this.bodies.alter({id: target, parent: robotID}); break; /// Places a bid (enlightent center). /// Target: bid placed case schema.Action.PLACE_BID: + this.bodies.alter({id: robotID, bid: target}); + this.bidRobots.push(robotID); + + if (robotID === teamStatsObj.bidderID) teamStatsObj.bid = target; break; /// A robot can change team after being empowered /// Target: teamID case schema.Action.CHANGE_TEAM: // TODO remove the robot, don't alter it - this.bodies.alter({ id: robotID, team: target}); break; /// A robot's influence changes. /// Target: delta value case schema.Action.CHANGE_INFLUENCE: - const old_influence = this.bodies.lookup(robotID).influence; - this.bodies.alter({ id: robotID, influence: old_influence + target}); + this.bodies.alter({ id: robotID, influence: body.influence + target}); + + if(!teamStatsObj) {continue;} // In case this is a neutral bot + teamStatsObj.influence[body.type] += target; + this.teamStats.set(body.team, teamStatsObj); break; /// A robot's conviction changes. /// Target: delta value, i.e. red 5 -> blue 3 is -2 case schema.Action.CHANGE_CONVICTION: - const old_conviction = this.bodies.lookup(robotID).conviction; - this.bodies.alter({ id: robotID, influence: old_conviction + target}); + this.bodies.alter({ id: robotID, conviction: body.conviction + target}); + + if(!teamStatsObj) {continue;} // In case this is a neutral bot + teamStatsObj.conviction[body.type] += target; + this.teamStats.set(body.team, teamStatsObj); break; case schema.Action.DIE_EXCEPTION: @@ -410,21 +520,50 @@ export default class GameWorld { } } - // TODO Passive Changes, need game constants. - + for (let team in this.meta.teams) { + let teamID = this.meta.teams[team].teamID; + let teamStats = this.teamStats.get(teamID) as TeamStats; + teamStats.income = 0; + } + + // income + this.bodies.arrays.type.forEach((type, i) => { + let robotID = this.bodies.arrays.id[i]; + let team = this.bodies.arrays.team[i]; + let ability = this.bodies.arrays.ability[i]; + let influence = this.bodies.arrays.influence[i]; + let income = this.bodies.arrays.income[i]; + let parent = this.bodies.arrays.parent[i]; + var teamStatsObj = this.teamStats.get(team); + if (ability === 3) { + let delta = Math.floor((1/50 + 0.03 * Math.exp(-0.001 * influence)) * influence); + teamStatsObj.income += delta; + this.bodies.alter({id: parent, income: delta}); + } else if (type === schema.BodyType.ENLIGHTENMENT_CENTER && teamStatsObj) { + let delta = Math.ceil(0.2 * Math.sqrt(this.turn)); + teamStatsObj.income += delta; + this.bodies.alter({id: robotID, income: delta}); + } else if (income !== 0) { + this.bodies.alter({id: robotID, income: 0}); + } + this.teamStats.set(team, teamStatsObj); + }) + // Died bodies if (delta.diedIDsLength() > 0) { - // Update team stats var indices = this.bodies.lookupIndices(delta.diedIDsArray()); for(let i = 0; i < delta.diedIDsLength(); i++) { let index = indices[i]; - // console.log("robot died: " + this.bodies.arrays.id[index]); let team = this.bodies.arrays.team[index]; let type = this.bodies.arrays.type[index]; - var statObj = this.teamStats.get(team); + let statObj = this.teamStats.get(team); if(!statObj) {continue;} // In case this is a neutral bot statObj.robots[type] -= 1; + let influence = this.bodies.arrays.influence[index]; + let conviction = this.bodies.arrays.conviction[index]; + statObj.influence[type] -= influence; // cancel extra negative influence + statObj.conviction[type] -= conviction; // cancel extra negative conviction this.teamStats.set(team, statObj); } @@ -454,6 +593,16 @@ export default class GameWorld { bytecodesUsed: delta.bytecodesUsedArray() }); } + + // Process logs + if (this.config.processLogs) this.parseLogs(delta.roundID(), delta.logs() ? delta.logs(flatbuffers.Encoding.UTF16_STRING) : ""); + else this.logsShift++; + + while (this.logs.length >= 25) { + this.logs.shift(); + this.logsShift++; + } + // console.log(delta.roundID(), this.logsShift, this.logs[0]); } private insertDiedBodies(delta: schema.Round) { @@ -523,16 +672,19 @@ export default class GameWorld { // Store frequently used arrays var teams = bodies.teamIDsArray(); var types = bodies.typesArray(); - var influences = bodies.influencesArray() || new Int32Array(teams.length); + var influences = bodies.influencesArray(); + var convictions: Int32Array = influences.map((influence, i) => Math.ceil(influence * this.meta.types[types[i]].convictionRatio)); //new Int32Array(bodies.robotIDsLength()); // Update spawn stats for(let i = 0; i < bodies.robotIDsLength(); i++) { if(teams[i] == 0) continue; var statObj = this.teamStats.get(teams[i]); statObj.robots[types[i]] += 1; + statObj.influence[types[i]] += influences[i]; + statObj.conviction[types[i]] += convictions[i]; this.teamStats.set(teams[i], statObj); } - + const locs = bodies.locs(this._vecTableSlot1); // Note: this allocates 6 objects with each call. // (One for the container, one for each TypedArray.) @@ -542,7 +694,6 @@ export default class GameWorld { // let this slide for now. // Initialize convictions - var convictions: Int32Array = influences.map((influence, i) => influence * this.meta.types[types[i]].convictionRatio); //new Int32Array(bodies.robotIDsLength()); // Insert bodies this.bodies.insertBulk({ @@ -553,10 +704,83 @@ export default class GameWorld { conviction: convictions, x: locs.xsArray(), y: locs.ysArray(), - flag: new Int8Array(bodies.robotIDsLength()), + flag: new Int32Array(bodies.robotIDsLength()), bytecodesUsed: new Int32Array(bodies.robotIDsLength()), - ability: new Int8Array(bodies.robotIDsLength()) + ability: new Int8Array(bodies.robotIDsLength()), + bid: new Int32Array(bodies.robotIDsLength()), + parent: new Int32Array(bodies.robotIDsLength()), }); } + /** + * Parse logs for a round. + */ + private parseLogs(round: number, logs: string) { + // TODO regex this properly + // Regex + let lines = logs.split(/\r?\n/); + let header = /^\[(A|B):(ENLIGHTENMENT_CENTER|POLITICIAN|SLANDERER|MUCKRAKER)#(\d+)@(\d+)\] (.*)/; + + let roundLogs = new Array(); + + // Parse each line + let index: number = 0; + while (index < lines.length) { + let line = lines[index]; + let matches = line.match(header); + + // Ignore empty string + if (line === "") { + index += 1; + continue; + } + + // The entire string and its 5 parenthesized substrings must be matched! + if (matches === null || (matches && matches.length != 6)) { + // throw new Error(`Wrong log format: ${line}`); + console.log(`Wrong log format: ${line}`); + console.log('Omitting logs'); + return; + } + + let shortenRobot = new Map(); + shortenRobot.set("ENLIGHTENMENT_CENTER", "EC"); + shortenRobot.set("POLITICIAN", "P"); + shortenRobot.set("SLANDERER", "SL"); + shortenRobot.set("MUCKRAKER", "MCKR"); + + // Get the matches + let team = matches[1]; + let robotType = matches[2]; + let id = parseInt(matches[3]); + let logRound = parseInt(matches[4]); + let text = new Array(); + let mText = "[" + team + ":" + robotType + "#" + id + "@" + logRound + "]"; + let mText2 = "[" + team + ":" + shortenRobot.get(robotType) + "#" + id + "@" + logRound + "] "; + text.push(mText + mText2 + matches[5]); + index += 1; + + // If there is additional non-header text in the following lines, add it + while (index < lines.length && !lines[index].match(header)) { + text.push(lines[index]); + index +=1; + } + + if (logRound != round) { + console.warn(`Your computation got cut off while printing a log statement at round ${logRound}; the actual print happened at round ${round}`); + } + + // Push the parsed log + roundLogs.push({ + team: team, + robotType: robotType, + id: id, + round: logRound, + text: text.join('\n') + }); + } + this.logs.push(roundLogs); + } + + } diff --git a/client/playback/src/gen/create.ts b/client/playback/src/gen/create.ts index 83b395f7..27f84172 100644 --- a/client/playback/src/gen/create.ts +++ b/client/playback/src/gen/create.ts @@ -531,6 +531,8 @@ function createWanderGame(turns: number, unitCount: number, doActions: boolean = case schema.BodyType.MUCKRAKER: action = schema.Action.EXPOSE; break; + case schema.BodyType.SLANDERER: + action = schema.Action.EMBEZZLE; default: break; } diff --git a/client/playback/src/index.ts b/client/playback/src/index.ts index 52e45422..5d623409 100644 --- a/client/playback/src/index.ts +++ b/client/playback/src/index.ts @@ -5,10 +5,10 @@ import * as metadata from './metadata'; import StructOfArrays from './soa'; import * as soa from './soa'; import Match from './match'; -import { Log } from './match'; -import Game from './game'; +import { Log } from './gameworld'; +import Game, {playbackConfig} from './game'; import { flatbuffers, schema } from 'battlecode-schema'; -export {Game, Log, Match, GameWorld, gameworld, Metadata, metadata, StructOfArrays, soa, flatbuffers, schema}; +export {Game, Log, Match, GameWorld, gameworld, Metadata, metadata, StructOfArrays, soa, flatbuffers, schema, playbackConfig}; // TODO provide ergonomic main export diff --git a/client/playback/src/match.ts b/client/playback/src/match.ts index dc067b6e..31d2f614 100644 --- a/client/playback/src/match.ts +++ b/client/playback/src/match.ts @@ -1,14 +1,7 @@ import Metadata from './metadata'; import GameWorld from './gameworld'; import { flatbuffers, schema } from 'battlecode-schema' - -export type Log = { - team: string, // 'A' | 'B' - robotType: string, // All loggable bodies with team - id: number, - round: number, - text: string -}; +import {playbackConfig} from './game'; export type ProfilerEvent = { type: string, @@ -22,10 +15,11 @@ export type ProfilerProfile = { } export type ProfilerFile = { - frames: Array, - profiles: Array + frames: Array, + profiles: Array } + // Return a timestamp representing the _current time in ms, not necessarily from // any particular epoch. const timeMS: () => number = typeof window !== 'undefined' && window.performance && window.performance.now? @@ -54,7 +48,7 @@ export default class Match { * Snapshots of the game world. * [0] is round 0 (the one stored in the GameMap), [1] is round * snapshotEvery * 1, [2] is round snapshotEvery * 2, etc. - * + * * By this, we can quickly navigate to arbitrary time * Saving game world for all round will use too much memory */ @@ -68,11 +62,6 @@ export default class Match { */ readonly deltas: Array; - /** - * The logs of this match, bucketed by round. - */ - readonly logs: Array>; - /** * The profiler files belong to this match. * Contains 2 items (team A and team B) if profiling was enabled, empty otherwise. @@ -128,23 +117,26 @@ export default class Match { */ readonly maxTurn: number; + private config: playbackConfig; + + /** * Create a Timeline. */ - constructor(header: schema.MatchHeader, meta: Metadata) { - this._current = new GameWorld(meta); + constructor(header: schema.MatchHeader, meta: Metadata, config: playbackConfig) { + this._current = new GameWorld(meta, config); this._current.loadFromMatchHeader(header); this._farthest = this._current; this.snapshots = []; this.snapshotEvery = 64; this.snapshots.push(this._current.copy()); this.deltas = new Array(1); - this.logs = new Array(1); this.profilerFiles = []; this.maxTurn = header.maxRounds(); this._lastTurn = null; this._seekTo = 0; this._winner = null; + this.config = config; } /** @@ -155,82 +147,8 @@ export default class Match { throw new Error(`Can't store Round ${delta.roundID()}. Next Round should be Round ${this.deltas.length}`); } this.deltas.push(delta); - - if(delta.logs()){ - this.parseLogs(delta.roundID(), delta.logs(flatbuffers.Encoding.UTF16_STRING)); - } } - /** - * Parse logs for a round. - */ - parseLogs(round: number, logs: string) { - // TODO regex this properly - - // Regex - let lines = logs.split(/\r?\n/); - let header = /^\[(A|B):(ENLIGHTENMENT_CENTER|POLITICIAN|SLANDERER|MUCKRAKER)#(\d+)@(\d+)\] (.*)/; - - let roundLogs = new Array(); - - // Parse each line - let index: number = 0; - while (index < lines.length) { - let line = lines[index]; - let matches = line.match(header); - - // Ignore empty string - if (line === "") { - index += 1; - continue; - } - - // The entire string and its 5 parenthesized substrings must be matched! - if (matches === null || (matches && matches.length != 6)) { - // throw new Error(`Wrong log format: ${line}`); - console.log(`Wrong log format: ${line}`); - console.log('Omitting logs'); - return; - } - - let shortenRobot = new Map(); - shortenRobot.set("ENLIGHTENMENT_CENTER", "EC"); - shortenRobot.set("POLITICIAN", "P"); - shortenRobot.set("SLANDERER", "SL"); - shortenRobot.set("MUCKRAKER", "MCKR"); - - // Get the matches - let team = matches[1]; - let robotType = matches[2]; - let id = parseInt(matches[3]); - let logRound = parseInt(matches[4]); - let text = new Array(); - let mText = "[" + team + ":" + robotType + "#" + id + "@" + logRound + "]"; - let mText2 = "[" + team + ":" + shortenRobot.get(robotType) + "#" + id + "@" + logRound + "] "; - text.push(mText + mText2 + matches[5]); - index += 1; - - // If there is additional non-header text in the following lines, add it - while (index < lines.length && !lines[index].match(header)) { - text.push(lines[index]); - index +=1; - } - - if (logRound != round) { - console.warn(`Your computation got cut off while printing a log statement at round ${logRound}; the actual print happened at round ${round}`); - } - - // Push the parsed log - roundLogs.push({ - team: team, - robotType: robotType, - id: id, - round: logRound, - text: text.join('\n') - }); - } - this.logs.push(roundLogs); - } /** * Finish the timeline. @@ -243,37 +161,38 @@ export default class Match { this._lastTurn = footer.totalRounds(); this._winner = footer.winner(); - // for (let i = 0, iMax = footer.profilerFilesLength(); i < iMax; i++) { - // const file = footer.profilerFiles(i); - - // const frames: string[] = []; - // for (let j = 0, jMax = file.framesLength(); j < jMax; j++) { - // frames.push(file.frames(j)); - // } - - // const profiles: ProfilerProfile[] = []; - // for (let j = 0, jMax = file.profilesLength(); j < jMax; j++) { - // const profile = file.profiles(j); - - // const events: ProfilerEvent[] = []; - // for (let k = 0, kMax = profile.eventsLength(); k < kMax; k++) { - // const event = profile.events(k); - - // events.push({ - // type: event.isOpen() ? 'O' : 'C', - // at: event.at(), - // frame: event.frame(), - // }); - // } - - // profiles.push({ - // name: profile.name(), - // events, - // }); - // } - - // this.profilerFiles.push({ frames, profiles }); - // } + if (this.config.doProfiling) { + for (let i = 0, iMax = footer.profilerFilesLength(); i < iMax; i++) { + const file = footer.profilerFiles(i); + + const frames: string[] = []; + for (let j = 0, jMax = file.framesLength(); j < jMax; j++) { + frames.push(file.frames(j)); + } + + const profiles: ProfilerProfile[] = []; + for (let j = 0, jMax = file.profilesLength(); j < jMax; j++) { + const profile = file.profiles(j); + + const events: ProfilerEvent[] = []; + for (let k = 0, kMax = profile.eventsLength(); k < kMax; k++) { + const event = profile.events(k); + + events.push({ + type: event.isOpen() ? 'O' : 'C', + at: event.at(), + frame: event.frame(), + }); + } + + profiles.push({ + name: profile.name(), + events, + }); + } + this.profilerFiles.push({ frames, profiles }); + } + } } /** diff --git a/client/visualizer/index.html b/client/visualizer/index.html index 8d7de481..394f1370 100644 --- a/client/visualizer/index.html +++ b/client/visualizer/index.html @@ -8,13 +8,23 @@
diff --git a/client/visualizer/package-lock.json b/client/visualizer/package-lock.json index 3876ec0f..f5f89e84 100644 --- a/client/visualizer/package-lock.json +++ b/client/visualizer/package-lock.json @@ -88,6 +88,14 @@ "@types/tape": "*" } }, + "@types/chart.js": { + "version": "2.9.30", + "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.30.tgz", + "integrity": "sha512-EgjxUUZFvf6ls3kW2CwyrnSJhgyKxgwrlp/W5G9wqyPEO9iFatO63zAA7L24YqgMxiDjQ+tG7ODU+2yWH91lPg==", + "requires": { + "moment": "^2.10.2" + } + }, "@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", @@ -898,8 +906,8 @@ "battlecode-schema": { "version": "file:../../schema", "requires": { - "@types/flatbuffers": "^1.10.0", - "flatbuffers": "^1.12.0" + "@types/flatbuffers": "^1.9.1", + "flatbuffers": "^1.11.0" }, "dependencies": { "@types/flatbuffers": { @@ -2072,6 +2080,32 @@ } } }, + "chart.js": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz", + "integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==", + "requires": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "chartjs-color": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", + "integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==", + "requires": { + "chartjs-color-string": "^0.6.0", + "color-convert": "^1.9.3" + } + }, + "chartjs-color-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", + "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", + "requires": { + "color-name": "^1.0.0" + } + }, "chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", @@ -2199,7 +2233,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -2207,8 +2240,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "colorette": { "version": "1.2.1", @@ -4428,11 +4460,6 @@ } } }, - "is-docker": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", - "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==" - }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -4586,12 +4613,9 @@ "dev": true }, "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "requires": { - "is-docker": "^2.0.0" - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" }, "is-yarn-global": { "version": "0.3.0", @@ -5043,6 +5067,11 @@ "minimist": "^1.2.5" } }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -5333,15 +5362,6 @@ "wrappy": "1" } }, - "open": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-7.2.0.tgz", - "integrity": "sha512-4HeyhxCvBTI5uBePsAdi55C5fmqnWZ2e2MlmvWi5KW5tdH5rxoiv/aMtbeVxKZc3eWkT1GymMnLG8XC4Rq4TDQ==", - "requires": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - } - }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -6689,11 +6709,21 @@ } }, "speedscope": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/speedscope/-/speedscope-1.12.1.tgz", - "integrity": "sha512-NzCqVR274NVKWQ6OFHw6GIQEgnL63SWP0KUtjsw7L+bqnG77FeS7SdpnfX0pA24RuSv1d419Aa7s5OtUHxtBfQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/speedscope/-/speedscope-1.5.2.tgz", + "integrity": "sha512-oGFmFEbhqQawTlOJMhyLNyB0nl+PEZKnAPOXZiRc+o1Mb++MAP/wb5aB2OEUZhg/7FoFgYGft9qmPz78FRAu4A==", "requires": { - "open": "7.2.0" + "opn": "5.3.0" + }, + "dependencies": { + "opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "requires": { + "is-wsl": "^1.1.0" + } + } } }, "split-string": { diff --git a/client/visualizer/package.json b/client/visualizer/package.json index 4ded6a9a..401b8dbb 100644 --- a/client/visualizer/package.json +++ b/client/visualizer/package.json @@ -25,8 +25,10 @@ }, "homepage": "https://github.com/battlecode/battlecode21#readme", "dependencies": { + "@types/chart.js": "^2.9.30", "battlecode-playback": "file:../playback", - "speedscope": "^1.5.2", + "chart.js": "^2.9.4", + "speedscope": "1.5.2", "victor": "^1.1.0" }, "devDependencies": { diff --git a/client/visualizer/src/app.ts b/client/visualizer/src/app.ts index cbcd8c7e..7e95fff3 100644 --- a/client/visualizer/src/app.ts +++ b/client/visualizer/src/app.ts @@ -58,7 +58,7 @@ export default class Client { mapeditor: MapEditor; gamearea: GameArea; // Inner game area console: Console; // Console to display logs - profiler: Profiler; + profiler?: Profiler; matchqueue: MatchQueue; // Match queue runner: Runner; @@ -81,7 +81,7 @@ export default class Client { this.root.appendChild(this.loadSidebar()); this.root.appendChild(this.loadGameArea()); this.loadScaffold(); - this.runner.ready(this.controls, this.stats, this.gamearea, this.console, this.matchqueue); + this.runner.ready(this.controls, this.stats, this.gamearea, this.console, this.matchqueue, this.sidebar, this.profiler); }); } @@ -112,17 +112,7 @@ export default class Client { * Loads stats bar with team information */ private loadSidebar() { - let onkeydownControls = (event: KeyboardEvent) => { - switch (event.keyCode) { - case 80: // "p" - Pause/Unpause - this.controls.pause(); - break; - case 79: // "o" - Stop - this.controls.stop(); - break; - } - }; - this.sidebar = new Sidebar(this.conf, this.imgs, this.runner, onkeydownControls); + this.sidebar = new Sidebar(this.conf, this.imgs, this.runner); this.stats = this.sidebar.stats; this.console = this.sidebar.console; this.mapeditor = this.sidebar.mapeditor; @@ -135,10 +125,22 @@ export default class Client { * Loads canvas to display game world. */ private loadGameArea() { - this.gamearea = new GameArea(this.conf, this.imgs, this.mapeditor.canvas, this.profiler.iframe); + this.gamearea = new GameArea(this.conf, this.imgs, this.mapeditor.canvas, this.profiler ? this.profiler.iframe : undefined); + // Handles all non-sidebar changes (gamearea, controls, and key stroke processing) on mode switch. + // TODO: refactor. + document.onkeydown = (e) => this.runner.onkeydown(e); // default keydown this.sidebar.cb = () => { this.gamearea.setCanvas(); this.controls.setControls(); + if (this.conf.mode == config.Mode.MAPEDITOR) { + document.onkeydown = (e) => this.mapeditor.onkeydown(e); + } + else if (this.conf.mode != config.Mode.HELP) { + document.onkeydown = (e) => this.runner.onkeydown(e); + } + else { + // Canvas can be anything in help mode + } }; return this.gamearea.div; diff --git a/client/visualizer/src/config.ts b/client/visualizer/src/config.ts index 9e97ba0c..29c88572 100644 --- a/client/visualizer/src/config.ts +++ b/client/visualizer/src/config.ts @@ -56,10 +56,15 @@ export interface Config { interpolate: boolean; /** - * Whether or not to display indicator dots and lines + * Whether or not to display indicator dots and lines for clicked robot. */ indicators: boolean; + /** + * Whether or not to display all indicator lines. + */ + allIndicators: boolean; + /** * Whether or not to display the action radius. */ @@ -75,12 +80,6 @@ export interface Config { */ seeDetectionRadius: boolean; - /** - * Whether or not to draw a circle under each robot - */ - circleBots: boolean; //TODO: is this needed? - - /** * The mode of the game */ @@ -105,6 +104,31 @@ export interface Config { * Whether logs should show shorter header */ shorterLogHeader: boolean; + + /** + * Whether we should process a match's logs. + */ + processLogs: boolean; + + /** + * Whether to load the profiler. + */ + useProfiler: boolean; + + /** + * Whether to do profiling on profiled match files, assuming the profiler is loaded. + */ + doProfiling: boolean; + + /** + * Whether to rotate tall maps. + */ + doRotate: boolean; + + /** + * Whether the map is currently rotated. TODO: don't make this a global variable. + */ + doingRotate: boolean; } /** @@ -125,27 +149,32 @@ export enum Mode { */ export function defaults(supplied?: any): Config { let conf: Config = { - gameVersion: "2021.1.1.1", //TODO: Change this on each release! + gameVersion: "2021.3.0.5", //TODO: Change this on each release! fullscreen: false, width: 600, height: 600, - upscale: 1500, + upscale: 1800, defaultTPS: 20, websocketURL: null, matchFileURL: null, pollEvery: 500, tournamentMode: false, - interpolate: false, + interpolate: true, indicators: false, + allIndicators: false, mode: Mode.QUEUE, splash: true, seeActionRadius: false, seeSensorRadius: false, seeDetectionRadius: false, - circleBots: false, showGrid: false, viewSwamp: true, shorterLogHeader: false, + processLogs: true, + useProfiler: true, + doProfiling: true, + doRotate: false, + doingRotate: false }; return Object.assign(conf, supplied); } diff --git a/client/visualizer/src/constants.ts b/client/visualizer/src/constants.ts index 7ac2ec01..90ce47f3 100644 --- a/client/visualizer/src/constants.ts +++ b/client/visualizer/src/constants.ts @@ -1,5 +1,5 @@ -import {schema} from 'battlecode-playback'; -import {Symmetry} from './mapeditor/index'; +import { schema } from 'battlecode-playback'; +import { Symmetry } from './mapeditor/index'; // Body types export const ENLIGHTENMENT_CENTER = schema.BodyType.ENLIGHTENMENT_CENTER; @@ -12,22 +12,31 @@ export const initialBodyTypeList: number[] = [ENLIGHTENMENT_CENTER]; export const bodyTypePriority: number[] = []; // for guns, drones, etc. that should be drawn over other robots -// old colors for reference - -export const TILE_COLORS: Array[] = [ - [120, 0, 0], - [0, 120, 0] -]; - -// maps elevation to rgb values -export const SWAMP_COLORS: Map> = new Map>([ - [-5, [0, 147, 83]], // turquoise - [3, [29, 201, 2]], // green - [10, [254,205,54]], // yellow - [90, [222, 145, 1]], // brown - [500, [255, 0, 0]], // red - [2000, [242, 0, 252]] // pink -]); +export const TILE_COLORS: Array[] = [[119, 228, 88], [144, 230, 83], [166, 231, 79], [187, 232, 76], [206, 233, 76], [231, 207, 66], [245, 182, 72], [249, 158, 86], [219, 115, 109], [163, 90, 118], [99, 73, 103], [51, 52, 65]]; +// flashy colors +// [0, 147, 83], // turquoise +// [29, 201, 2], // green +// [254, 205, 54], // yellow +// [222, 145, 1], // brown +// [255, 0, 0], // red +// [242, 0, 252] // pink + +// Given passability, get index of tile to use. +export const getLevel = (x: number): number => { + const nLev = TILE_COLORS.length; + const floatLevel = ((1 - x) - 0.1) / 0.9 * nLev; + const level = Math.floor(floatLevel) + return Math.min(nLev - 1, Math.max(0, level)); +} + +export const passiveInfluenceRate = (round: number): number => { + //return Math.floor((1/50 + 0.03 * Math.exp(-0.001 * x)) * x); this one's for slanderers + return Math.ceil(0.2 * Math.sqrt(round)); +} + +export const buffFactor = (numBuffs: number): number => { + return 1 + 0.001 * numBuffs; +} export const ACTION_RADIUS_COLOR = "#46ff00"; export const SENSOR_RADIUS_COLOR = "#0000ff"; @@ -45,9 +54,12 @@ export const EFFECT_STEP = 200; //time change between effect animations // Map editor canvas parameters export const DELTA = .0001; -export const MIN_DIMENSION = 30; +export const MIN_DIMENSION = 15; export const MAX_DIMENSION = 100; +// Initial influence of enlightenment center, for map editor +export const INITIAL_INFLUENCE = 150; + // Server settings export const NUMBER_OF_TEAMS = 2; // export const VICTORY_POINT_THRESH = 1000; @@ -59,71 +71,104 @@ export const NUMBER_OF_TEAMS = 2; // The key is the map name and the value is the type export enum MapType { DEFAULT, - SPRINT, - SEEDING, - INTL_QUALIFYING, - US_QUALIFYING, - HS, - NEWBIE, + SPRINT_1, + SPRINT_2, + QUALIFYING, + HS_NEWBIE, FINAL, CUSTOM }; + +// Map types to filter in runner +export const mapTypes: MapType[] = [MapType.DEFAULT, + MapType.SPRINT_1, + MapType.SPRINT_2, + MapType.QUALIFYING, + MapType.HS_NEWBIE, + MapType.FINAL, + MapType.CUSTOM]; + export const SERVER_MAPS: Map = new Map([ - // ["Maze", MapType.INTL_QUALIFYING], - // ["Squares", MapType.INTL_QUALIFYING], - // ["RealArt", MapType.INTL_QUALIFYING], - // ["DoesNotExist", MapType.INTL_QUALIFYING], - // ["IceCream", MapType.INTL_QUALIFYING], - // ["Constriction", MapType.INTL_QUALIFYING], - // ["Islands2", MapType.INTL_QUALIFYING], - // ["Prison", MapType.INTL_QUALIFYING], - // ["DisproportionatelySmallGap", MapType.INTL_QUALIFYING], - // ["Climb", MapType.INTL_QUALIFYING], - // ["TwoLakeLand", MapType.INTL_QUALIFYING], - // ["Europe", MapType.INTL_QUALIFYING], - // ["AMaze", MapType.SEEDING], - // ["BeachFrontProperty", MapType.SEEDING], - // ["Egg", MapType.SEEDING], - // ["Hourglass", MapType.SEEDING], - // ["MtDoom", MapType.SEEDING], - // ["Sheet4", MapType.SEEDING], - // ["Showerhead", MapType.SEEDING], - // ["Spiral", MapType.SEEDING], - // ["Swirl", MapType.SEEDING], - // ["TheHighGround", MapType.SEEDING], - // ["Toothpaste", MapType.SEEDING], - // ["WhyDidntTheyUseEagles", MapType.SEEDING], - // ["NoU", MapType.SEEDING], - // ["MoreCowbell", MapType.SEEDING], - // ["FourLakeLand", MapType.DEFAULT], - // ["CentralLake", MapType.DEFAULT], - // ["ALandDivided", MapType.DEFAULT], - // ["SoupOnTheSide", MapType.DEFAULT], - // ["TwoForOneAndTwoForAll", MapType.DEFAULT], - // ["WaterBot", MapType.DEFAULT], - // ["CentralSoup", MapType.DEFAULT], - // ["ChristmasInJuly", MapType.SPRINT], - // ["CosmicBackgroundRadiation", MapType.SPRINT], - // ["ClearlyTwelveHorsesInASalad", MapType.SPRINT], - // ["CowFarm", MapType.SPRINT], - // ["DidAMonkeyMakeThis", MapType.SPRINT], - // ["GSF", MapType.SPRINT], - // ["Hills", MapType.SPRINT], - // ["InADitch", MapType.SPRINT], - // ["Infinity", MapType.SPRINT], - // ["Islands", MapType.SPRINT], - // ["IsThisProcedural", MapType.SPRINT], - // ["OmgThisIsProcedural", MapType.SPRINT], - // ["ProceduralConfirmed", MapType.SPRINT], - // ["RandomSoup1", MapType.SPRINT], - // ["RandomSoup2", MapType.SPRINT], - // ["Soup", MapType.SPRINT], - // ["Volcano", MapType.SPRINT], - // ["WateredDown", MapType.SPRINT] + ["maptestsmall", MapType.DEFAULT], + ["circle", MapType.DEFAULT], + ["quadrants", MapType.DEFAULT], + ["Andromeda", MapType.SPRINT_1], + ["Arena", MapType.SPRINT_1], + ["Bog", MapType.SPRINT_1], + ["Branches", MapType.SPRINT_1], + ["Chevron", MapType.SPRINT_1], + ["Corridor", MapType.SPRINT_1], + ["Cow", MapType.SPRINT_1], + ["CrossStitch", MapType.SPRINT_1], + ["CrownJewels", MapType.SPRINT_1], + ["ExesAndOhs", MapType.SPRINT_1], + ["FiveOfHearts", MapType.SPRINT_1], + ["Gridlock", MapType.SPRINT_1], + ["Illusion", MapType.SPRINT_1], + ["NotAPuzzle", MapType.SPRINT_1], + ["Rainbow", MapType.SPRINT_1], + ["SlowMusic", MapType.SPRINT_1], + ["Snowflake", MapType.SPRINT_1], + ["BadSnowflake", MapType.SPRINT_2], + ["CringyAsF", MapType.SPRINT_2], + ["FindYourWay", MapType.SPRINT_2], + ["GetShrekt", MapType.SPRINT_2], + ["Goldfish", MapType.SPRINT_2], + ["HexesAndOhms", MapType.SPRINT_2], + ["Licc", MapType.SPRINT_2], + ["MainCampus", MapType.SPRINT_2], + ["Punctuation", MapType.SPRINT_2], + ["Radial", MapType.SPRINT_2], + ["SeaFloor", MapType.SPRINT_2], + ["Sediment", MapType.SPRINT_2], + ["Smile", MapType.SPRINT_2], + ["SpaceInvaders", MapType.SPRINT_2], + ["Surprised", MapType.SPRINT_2], + ["VideoGames", MapType.SPRINT_2], + ["AmidstWe", MapType.QUALIFYING], + ["BattleCode", MapType.QUALIFYING], + ["BattleCodeToo", MapType.QUALIFYING], + ["BlobWithLegs", MapType.QUALIFYING], + ["ButtonsAndBows", MapType.QUALIFYING], + ["CowTwister", MapType.QUALIFYING], + ["Extensions", MapType.QUALIFYING], + ["Hourglass", MapType.QUALIFYING], + ["Maze", MapType.QUALIFYING], + ["NextHouse", MapType.QUALIFYING], + ["Superposition", MapType.QUALIFYING], + ["TicTacTie", MapType.QUALIFYING], + ["UnbrandedWordGame", MapType.QUALIFYING], + ["Z", MapType.QUALIFYING], + ["Zodiac", MapType.QUALIFYING], + ["Flawars", MapType.HS_NEWBIE], + ["FrogOrBath", MapType.HS_NEWBIE], + ["HappyBoba", MapType.HS_NEWBIE], + ["Networking", MapType.HS_NEWBIE], + ["NoInternet", MapType.HS_NEWBIE], + ["PaperWindmill", MapType.HS_NEWBIE], + ["Randomized", MapType.HS_NEWBIE], + ["Star", MapType.HS_NEWBIE], + ["Tiger", MapType.HS_NEWBIE], + ["WhatISeeInMyDreams", MapType.HS_NEWBIE], + ["Yoda", MapType.HS_NEWBIE], + ["Blotches", MapType.FINAL], + ["CToE", MapType.FINAL], + ["Circles", MapType.FINAL], + ["EggCarton", MapType.FINAL], + ["InaccurateBritishFlag", MapType.FINAL], + ["JerryIsEvil", MapType.FINAL], + ["Legends", MapType.FINAL], + ["Mario", MapType.FINAL], + ["Misdirection", MapType.FINAL], + ["OneCallAway", MapType.FINAL], + ["Saturn", MapType.FINAL], + ["Stonks", MapType.FINAL], + ["TheClientMapEditorIsSuperiorToGoogleSheetsEom", MapType.FINAL], + ["TheSnackThatSmilesBack", MapType.FINAL] ]); export function bodyTypeToString(bodyType: schema.BodyType) { - switch(bodyType) { + switch (bodyType) { case ENLIGHTENMENT_CENTER: return "enlightenmentCenter"; case POLITICIAN: @@ -132,21 +177,21 @@ export function bodyTypeToString(bodyType: schema.BodyType) { return "slanderer"; case MUCKRAKER: return "muckraker"; - default: throw new Error("invalid body type"); + default: throw new Error("invalid body type"); } } export function symmetryToString(symmetry: Symmetry) { - switch(symmetry) { + switch (symmetry) { case Symmetry.ROTATIONAL: return "Rotational"; case Symmetry.HORIZONTAL: return "Horizontal"; - case Symmetry.VERTICAL: return "Vertical"; - default: throw new Error("invalid symmetry"); + case Symmetry.VERTICAL: return "Vertical"; + default: throw new Error("invalid symmetry"); } } export function abilityToEffectString(effect: number): string | null { - switch(effect) { + switch (effect) { case 1: return "empower"; case 2: diff --git a/client/visualizer/src/cow.ts b/client/visualizer/src/cow.ts new file mode 100644 index 00000000..d2f4ecbe --- /dev/null +++ b/client/visualizer/src/cow.ts @@ -0,0 +1,3 @@ +export const cow_filled = [[false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, true, true, true, true, true, false, false, false, false, false, false, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, true, true, true, true, true, false, false, false, false, false, false, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false]]; + +export const cow_border = [[false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, true, true, true, true, true, false, false, false, false, false, false, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, true, true, true, true, true, false, false, false, false, false, false, true, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, true, false, true, false, false, false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, true, true, false, false, false, true, true, false, false, false, true, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, false, false, false, false, true, true, false, false, false, true, false, false, false, true, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false, false, true, false, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, false, false, false, false, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, false, false, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, false, true, false, false, false, false, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, false, false, false, false, true, true, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, true, false, false, false, false, true, false, true, true, false, false, false, false, true, false, false, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, false, false, true, true, true, true, false, false, false, false, false, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, false, false, true, true, true, true, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, false, false, true, true, true, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, false, false, false, false, true, false, false, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false]]; \ No newline at end of file diff --git a/client/visualizer/src/gamearea/gamearea.ts b/client/visualizer/src/gamearea/gamearea.ts index ced75e8d..2f4a149d 100644 --- a/client/visualizer/src/gamearea/gamearea.ts +++ b/client/visualizer/src/gamearea/gamearea.ts @@ -5,6 +5,7 @@ import Client from '../app'; import {GameWorld} from 'battlecode-playback'; import {http} from '../main/electron-modules'; +import { SSL_OP_NO_QUERY_MTU } from 'constants'; export default class GameArea { @@ -15,12 +16,12 @@ export default class GameArea { readonly splashDiv: HTMLDivElement; private readonly wrapper: HTMLDivElement; private readonly mapEditorCanvas: HTMLCanvasElement; - private readonly profilerIFrame: HTMLIFrameElement; + private readonly profilerIFrame?: HTMLIFrameElement; // Options private readonly conf: Config; - constructor(conf: Config, images: AllImages, mapEditorCanvas: HTMLCanvasElement, profilerIFrame: HTMLIFrameElement) { + constructor(conf: Config, images: AllImages, mapEditorCanvas: HTMLCanvasElement, profilerIFrame?: HTMLIFrameElement) { this.div = document.createElement("div"); this.div.id = "gamearea"; this.conf = conf; @@ -49,13 +50,17 @@ export default class GameArea { * Sets canvas size to maximum dimensions while maintaining the aspect ratio */ setCanvasDimensions(world: GameWorld): void { - const scale: number = this.conf.upscale; // scaling factor - - this.canvas.width = scale; - this.canvas.height = world.minCorner.absDistanceY(world.maxCorner) / world.minCorner.absDistanceX(world.maxCorner) * scale; - // TODO: transfer below to CSS - this.canvas.style.width = (this.div.clientWidth*0.8).toString() + 'px'; - this.canvas.style.width = (this.div.clientHeight*0.8).toString() + 'px'; + const width = world.minCorner.absDistanceX(world.maxCorner); + const height = world.minCorner.absDistanceY(world.maxCorner); + const scale = this.conf.upscale / Math.sqrt(width * height); + if (!this.conf.doingRotate) { + this.canvas.width = width * scale; + this.canvas.height = height * scale; + } + else { + this.canvas.width = height * scale; + this.canvas.height = width * scale; + } } /** @@ -64,15 +69,21 @@ export default class GameArea { loadSplashDiv() { let splashTitle = document.createElement("h1"); - splashTitle.id = "splashTitle"; - splashTitle.appendChild(document.createTextNode("Battlecode 2021 Client")); - this.splashDiv.appendChild(splashTitle); - let splashSubtitle = document.createElement("h3"); + splashTitle.id = "splashTitle"; splashSubtitle.id = "splashSubtitle"; - splashSubtitle.appendChild(document.createTextNode("v" + this.conf.gameVersion)); - this.splashDiv.appendChild(splashSubtitle); + + if (!this.conf.tournamentMode) { + splashTitle.appendChild(document.createTextNode("Battlecode 2021 Client")); + splashSubtitle.appendChild(document.createTextNode("v" + this.conf.gameVersion)); + } + else { + splashTitle.appendChild(document.createTextNode("Loading...")); + } + this.splashDiv.appendChild(splashTitle); + this.splashDiv.appendChild(splashSubtitle); + if (process.env.ELECTRON) { (async function (splashDiv, version) { @@ -134,7 +145,7 @@ export default class GameArea { this.wrapper.appendChild(this.mapEditorCanvas); break; case Mode.PROFILER: - this.wrapper.appendChild(this.profilerIFrame); + if (this.profilerIFrame) this.wrapper.appendChild(this.profilerIFrame); break; default: this.wrapper.appendChild(this.canvas); // TODO: Only append if a game is available in client.games diff --git a/client/visualizer/src/gamearea/renderer.ts b/client/visualizer/src/gamearea/renderer.ts index e310c913..75814f78 100644 --- a/client/visualizer/src/gamearea/renderer.ts +++ b/client/visualizer/src/gamearea/renderer.ts @@ -19,11 +19,11 @@ export default class Renderer { // For rendering robot information on click private lastSelectedID: number; // position of mouse cursor hovering - private hoverPos: {x: number, y: number} | null; + private hoverPos: {x: number, y: number} | null = null; constructor(readonly canvas: HTMLCanvasElement, readonly imgs: AllImages, private conf: config.Config, readonly metadata: Metadata, readonly onRobotSelected: (id: number) => void, - readonly onMouseover: (x: number, y: number, passability: number) => void) { + readonly onMouseover: (x: number, y: number, xrel: number, yrel: number, passability: number) => void) { let ctx = canvas.getContext("2d"); if (ctx === null) { @@ -48,11 +48,12 @@ export default class Renderer { // setup correct rendering const viewWidth = viewMax.x - viewMin.x const viewHeight = viewMax.y - viewMin.y - const scale = this.canvas.width / viewWidth; + const scale = this.canvas.width / (!this.conf.doingRotate ? viewWidth : viewHeight); this.ctx.save(); this.ctx.scale(scale, scale); - this.ctx.translate(-viewMin.x, -viewMin.y); + if (!this.conf.doingRotate) this.ctx.translate(-viewMin.x, -viewMin.y); + else this.ctx.translate(-viewMin.y, -viewMin.x); this.renderBackground(world); @@ -77,17 +78,18 @@ export default class Renderer { this.ctx.fillStyle = "white"; this.ctx.globalAlpha = 1; - const minX = world.minCorner.x; - const minY = world.minCorner.y; - const width = world.maxCorner.x - world.minCorner.x; - const height = world.maxCorner.y - world.minCorner.y; + let minX = world.minCorner.x; + let minY = world.minCorner.y; + let width = world.maxCorner.x - world.minCorner.x; + let height = world.maxCorner.y - world.minCorner.y; const scale = 20; this.ctx.scale(1/scale, 1/scale); // scale the background pattern - this.ctx.fillRect(minX*scale, minY*scale, width*scale, height*scale); + if (!this.conf.doingRotate) this.ctx.fillRect(minX*scale, minY*scale, width*scale, height*scale); + else this.ctx.fillRect(minY*scale, minX*scale, height*scale, width*scale); const map = world.mapStats; @@ -99,21 +101,18 @@ export default class Renderer { this.ctx.globalAlpha = 1; - // equally divde 0.1 - 1 - const getLevel = (x: number): number => { - const nLev = cst.TILE_COLORS.length; - const floatLevel = (x - 0.1) / 0.9 * nLev; - return Math.min(nLev - 1, Math.floor(floatLevel)); - } - - const swampLevel = getLevel(map.passability[idxVal]); + // Fetch and draw tile image + const swampLevel = cst.getLevel(map.passability[idxVal]); const tileImg = this.imgs.tiles[swampLevel]; - this.ctx.drawImage(tileImg, cx, cy, scale, scale); + if (!this.conf.doingRotate) this.ctx.drawImage(tileImg, cx, cy, scale, scale); + else this.ctx.drawImage(tileImg, cy, cx, scale, scale); + // Draw grid if (this.conf.showGrid) { this.ctx.strokeStyle = 'gray'; this.ctx.globalAlpha = 1; - this.ctx.strokeRect(cx, cy, scale, scale); + if (!this.conf.doingRotate) this.ctx.strokeRect(cx, cy, scale, scale); + else this.ctx.strokeRect(cy, cx, scale, scale); } } @@ -121,9 +120,11 @@ export default class Renderer { if (this.hoverPos != null) { const {x, y} = this.hoverPos; const cx = (minX+x)*scale, cy = (minY+(height-y-1))*scale; - this.ctx.strokeStyle = 'red'; + this.ctx.strokeStyle = 'purple'; + this.ctx.lineWidth *= 2; this.ctx.globalAlpha = 1; - this.ctx.strokeRect(cx, cy, scale, scale); + if (!this.conf.doingRotate) this.ctx.strokeRect(cx, cy, scale, scale); + else this.ctx.strokeRect(cy, cx, scale, scale); } this.ctx.restore(); @@ -134,6 +135,7 @@ export default class Renderer { const length = bodies.length; const types = bodies.arrays.type; const teams = bodies.arrays.team; + const convictions = bodies.arrays.conviction; const ids = bodies.arrays.id; const xs = bodies.arrays.x; const ys = bodies.arrays.y; @@ -168,19 +170,21 @@ export default class Renderer { // Render the robots // render images with priority last to have them be on top of other units. + const drawEffect = (effect: string, x: number, y: number) => { + const effectImgs: HTMLImageElement[] = this.imgs.effects[effect]; + const whichImg = (Math.floor(curTime / cst.EFFECT_STEP) % effectImgs.length); + const effectImg = effectImgs[whichImg]; + this.drawBot(effectImg, x, y, 0); + } + const renderBot = (i: number) => { const img: HTMLImageElement = this.imgs.robots[cst.bodyTypeToString(types[i])][teams[i]]; - this.drawBot(img, realXs[i], realYs[i]); + this.drawBot(img, realXs[i], realYs[i], convictions[i]); this.drawSightRadii(realXs[i], realYs[i], types[i], ids[i] === this.lastSelectedID); - // draw effec + // draw effect let effect: string | null = cst.abilityToEffectString(abilities[i]); - if (effect !== null) { - const effectImgs: HTMLImageElement[] = this.imgs.effects[effect]; - const whichImg = (Math.floor(curTime / cst.EFFECT_STEP) % effectImgs.length); - const effectImg = effectImgs[whichImg]; - this.drawBot(effectImg, realXs[i], realYs[i]); - } + if (effect !== null) drawEffect(effect, realXs[i], realYs[i]); } let priorityIndices: number[] = []; @@ -195,15 +199,17 @@ export default class Renderer { priorityIndices.forEach((i) => renderBot(i)); - // Render died bodies + // Render empowered bodies + const empowered = world.empowered; + const empowered_id = world.empowered.arrays.id; + const empowered_x = world.empowered.arrays.x; + const empowered_y = world.empowered.arrays.y; + const empowered_team = world.empowered.arrays.team; - const died = world.diedBodies; - const diedImg: HTMLImageElement = this.imgs.effects["death"]; - for (let i = 0; i < died.length; i++) { - this.drawBot(diedImg, died.arrays.x[i], this.flip(died.arrays.y[i], minY, maxY)); + for (let i = 0; i < empowered.length; i++) { + drawEffect(empowered_team[i] == 1 ? "empower_red" : "empower_blue", empowered_x[i], this.flip(empowered_y[i], minY, maxY)); } - this.setInfoStringEvent(world, xs, ys); } @@ -222,6 +228,7 @@ export default class Renderer { * Draws a cirlce centered at (x,y) with given squared radius and color. */ private drawBotRadius(x: number, y: number, radiusSquared: number, color: string) { + if (this.conf.doingRotate) [x,y] = [y,x]; this.ctx.beginPath(); this.ctx.arc(x+0.5, y+0.5, Math.sqrt(radiusSquared), 0, 2 * Math.PI); this.ctx.strokeStyle = color; @@ -236,7 +243,7 @@ export default class Renderer { // handle bots with no radius here, if necessary if (this.conf.seeActionRadius || single) { this.drawBotRadius(x, y, this.metadata.types[type].actionRadiusSquared, cst.ACTION_RADIUS_COLOR); - } + } if (this.conf.seeSensorRadius || single) { this.drawBotRadius(x, y, this.metadata.types[type].sensorRadiusSquared, cst.SENSOR_RADIUS_COLOR); @@ -244,23 +251,30 @@ export default class Renderer { if (this.conf.seeDetectionRadius || single) { this.drawBotRadius(x, y, this.metadata.types[type].detectionRadiusSquared, cst.SENSOR_RADIUS_COLOR); - } + } } /** * Draws an image centered at (x, y) with the given radius */ private drawImage(img: HTMLImageElement, x: number, y: number, radius: number) { + if (this.conf.doingRotate) [x,y] = [y,x]; this.ctx.drawImage(img, x-radius, y-radius, radius*2, radius*2); } /** * Draws an image centered at (x, y), such that an image with default size covers a 1x1 cell */ - private drawBot(img: HTMLImageElement, x: number, y: number) { + private drawBot(img: HTMLImageElement, x: number, y: number, c: number) { + if (this.conf.doingRotate) [x,y] = [y,x]; let realWidth = img.naturalWidth/cst.IMAGE_SIZE; let realHeight = img.naturalHeight/cst.IMAGE_SIZE; - this.ctx.drawImage(img, x+(1-realWidth)/2, y+(1-realHeight)/2, realWidth, realHeight); + const sigmoid = (x) => { + return 1 / (1 + Math.exp(-x)) + } + //this.ctx.filter = `brightness(${sigmoid(c - 100) * 30 + 90}%)`; + let size = sigmoid(c / 100) * 1 + 0.3; + this.ctx.drawImage(img, x+(1-realWidth * size)/2, y+(1-realHeight * size)/2, realWidth * size, realHeight * size); } private setInfoStringEvent(world: GameWorld, @@ -311,9 +325,11 @@ export default class Renderer { // Set the location of the mouseover const {x,y} = this.getIntegerLocation(event, world); - const idx = world.mapStats.getIdx(x, y); - onMouseover(x, y, world.mapStats.passability[idx]); - this.hoverPos = {x: x, y: y}; + const xrel = x - world.minCorner.x; + const yrel = y - world.minCorner.y; + const idx = world.mapStats.getIdx(xrel, yrel); + onMouseover(x, y, xrel, yrel, world.mapStats.passability[idx]); + this.hoverPos = {x: xrel, y: yrel}; }; this.canvas.onmouseout = (event) => { @@ -326,14 +342,22 @@ export default class Renderer { const height = world.maxCorner.y - world.minCorner.y; const minY = world.minCorner.y; const maxY = world.maxCorner.y - 1; - const x = width * event.offsetX / this.canvas.offsetWidth + world.minCorner.x; - const _y = height * event.offsetY / this.canvas.offsetHeight + world.minCorner.y; - const y = this.flip(_y, minY, maxY) - return {x: Math.floor(x), y: Math.floor(y+1)}; + var _x: number; + var _y: number; + if (!this.conf.doingRotate) { + _x = width * event.offsetX / this.canvas.offsetWidth + world.minCorner.x; + _y = height * event.offsetY / this.canvas.offsetHeight + world.minCorner.y; + _y = this.flip(_y, minY, maxY) + } + else { + _y = (world.maxCorner.y - world.minCorner.y - 1) - height * event.offsetX / this.canvas.offsetWidth + world.minCorner.y; + _x = width * event.offsetY / this.canvas.offsetHeight + world.minCorner.x; + } + return {x: Math.floor(_x), y: Math.floor(_y+1)}; } private renderIndicatorDotsLines(world: GameWorld) { - if (!this.conf.indicators) { + if (!this.conf.indicators && !this.conf.allIndicators) { return; } @@ -350,8 +374,10 @@ export default class Renderer { const minY = world.minCorner.y; const maxY = world.maxCorner.y - 1; + console.log(dots.length); + for (let i = 0; i < dots.length; i++) { - if (dotsID[i] === this.lastSelectedID) { + if (dotsID[i] === this.lastSelectedID || this.conf.allIndicators) { const red = dotsRed[i]; const green = dotsGreen[i]; const blue = dotsBlue[i]; @@ -377,7 +403,7 @@ export default class Renderer { this.ctx.lineWidth = cst.INDICATOR_LINE_WIDTH; for (let i = 0; i < lines.length; i++) { - if (linesID[i] === this.lastSelectedID) { + if (linesID[i] === this.lastSelectedID || this.conf.allIndicators) { const red = linesRed[i]; const green = linesGreen[i]; const blue = linesBlue[i]; diff --git a/client/visualizer/src/imageloader.ts b/client/visualizer/src/imageloader.ts index af06c6ab..7afbb524 100644 --- a/client/visualizer/src/imageloader.ts +++ b/client/visualizer/src/imageloader.ts @@ -1,4 +1,6 @@ +import { basename } from 'path'; import {Config} from './config'; +import * as cst from "./constants"; type Image = HTMLImageElement; export type AllImages = { @@ -34,7 +36,7 @@ export function loadAll(config: Config, callback: (arg0: AllImages) => void) { const RED: number = 1; const BLU: number = 2; - function loadImage(obj, slot, path) : void { + function loadImage(obj, slot, path, src?) : void { const f = loadImage; f.expected++; const image = new Image(); @@ -56,12 +58,13 @@ export function loadAll(config: Config, callback: (arg0: AllImages) => void) { obj[slot] = image; f.failure++; console.error(`CANNOT LOAD IMAGE: ${slot}, ${path}, ${image}`); + if(src) console.error(`Source: ${src}`); onFinish(); } // might want to use path library // webpack url loader triggers on require(".png"), so .png should be explicit - image.src = require(dirname + path + '.png').default; + image.src = (src ? src : require(dirname + path + '.png').default); } loadImage.expected = 0; loadImage.success = 0; @@ -79,7 +82,8 @@ export function loadAll(config: Config, callback: (arg0: AllImages) => void) { effects: { death: null, embezzle: [], - empower: [], + empower_red: [], + empower_blue: [], expose: [], camouflage_red: [], camouflage_blue: [] @@ -98,12 +102,58 @@ export function loadAll(config: Config, callback: (arg0: AllImages) => void) { goEnd: null } }; + // helper function to manipulate images + const htmlToData = (ele: HTMLImageElement): ImageData => { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + if(!context) throw new Error("Error while converting a tile image"); + canvas.width = ele.width; + canvas.height = ele.height; + context.drawImage(ele, 0, 0); + return context.getImageData(0, 0, ele.width, ele.height); + }; + const dataToSrc = (data: ImageData): String => { + var canvas = document.createElement("canvas"); + canvas.width = data.width; + canvas.height = data.height; + var context = canvas.getContext("2d"); + if(!context) throw new Error("Error while converting a tile images"); + context.putImageData(data, 0, 0); + + return canvas.toDataURL(`edited.png`); + }; loadImage(result, 'star', 'star'); // terrain tiles - loadImage(result.tiles, 0, 'tiles/DirtTerrain'); - loadImage(result.tiles, 1, 'tiles/SwampTerrain'); + { + const tintData = (data: ImageData, colors: Uint8Array): ImageData => { + const arr = new Uint8ClampedArray(data.data.length); + for(let i=0; i 128; + const factor = rock ? 1.5 : 1; + arr[i + 0] = colors[0] / factor; + arr[i + 1] = colors[1] / factor; + arr[i + 2] = colors[2] / factor; + arr[i + 3] = 240; + } + const result = new ImageData(arr, data.height); + return result; + } + + const baseTile: Image = new Image(); + baseTile.src = require(dirname + 'tiles/terrain.png').default; + + const nLev = cst.TILE_COLORS.length; + baseTile.onload = () => { + for(let i=0; icst.TILE_COLORS[i]); + const path: String = dataToSrc(tinted); + loadImage(result.tiles, i, "", path.slice(0, path.length-4)); + } + } + } // robot sprites loadImage(result.robots.enlightenmentCenter, RED, 'robots/center_red'); @@ -119,14 +169,64 @@ export function loadAll(config: Config, callback: (arg0: AllImages) => void) { loadImage(result.robots.enlightenmentCenter, NEUTRAL, 'robots/center'); // effects - loadImage(result.effects, 'death', 'effects/death/death_empty'); loadImage(result.effects.embezzle, 0, 'effects/embezzle/slanderer_embezzle_empty_1'); loadImage(result.effects.embezzle, 1, 'effects/embezzle/slanderer_embezzle_empty_2'); - loadImage(result.effects.empower, 0, 'effects/empower/polit_empower_empty_1'); - loadImage(result.effects.empower, 1, 'effects/empower/polit_empower_empty_2'); + { + const makeTransparent = (data: ImageData): ImageData => { + const arr = new Uint8ClampedArray(data.data.length); + for(let i=0; i { + const arr = new Uint8ClampedArray(data.data.length); + for(let i=0; i { + const arr = new Uint8ClampedArray(data.data.length); + for(let i=0; i { + const data: ImageData = htmlToData(base); + const trans: ImageData = makeTransparent(data); + const red: ImageData = makeRed(trans); + const blue: ImageData = makeBlue(trans); + const path_red: String = dataToSrc(red); + const path_blue: String = dataToSrc(blue); + loadImage(result.effects.empower_red, i, "", path_red.slice(0, path_red.length-4)); + loadImage(result.effects.empower_blue, i, "", path_blue.slice(0, path_blue.length-4)); + } + } + } loadImage(result.effects.expose, 0, 'effects/expose/expose_empty'); diff --git a/client/visualizer/src/main/controls.ts b/client/visualizer/src/main/controls.ts index 309d60cb..85360c3b 100644 --- a/client/visualizer/src/main/controls.ts +++ b/client/visualizer/src/main/controls.ts @@ -19,12 +19,12 @@ export default class Controls { div: HTMLDivElement; wrapper: HTMLDivElement; - readonly timeReadout: Text; + readonly timeReadout: HTMLSpanElement; readonly speedReadout: HTMLSpanElement; - readonly tileInfo: Text; + readonly tileInfo: HTMLSpanElement; readonly infoString: HTMLTableDataCellElement; - winnerDiv: HTMLDivElement; + //winnerDiv: HTMLDivElement; /** * Callbacks initialized from outside Controls @@ -57,11 +57,12 @@ export default class Controls { constructor(conf: Config, images: imageloader.AllImages, runner: Runner) { this.div = this.baseDiv(); - this.timeReadout = document.createTextNode('No match loaded'); - this.tileInfo = document.createTextNode('X | Y | Passability'); + this.timeReadout = document.createElement('span'); + this.tileInfo = document.createElement('span'); this.speedReadout = document.createElement('span'); this.speedReadout.style.cssFloat = 'right'; - this.speedReadout.textContent = 'UPS: 0 FPS: 0'; + + this.setDefaultText(); // initialize the images this.conf = conf; @@ -84,23 +85,16 @@ export default class Controls { // create the timeline let timeline = document.createElement("td"); - if (this.conf.tournamentMode) { - timeline.style.width = '400px'; - } + timeline.className = "timeline"; + timeline.vAlign = "top"; + timeline.appendChild(this.timeline()); - if (this.conf.tournamentMode) { - this.winnerDiv = document.createElement("div"); - timeline.append(this.winnerDiv); - } timeline.appendChild(document.createElement("br")); timeline.appendChild(this.timeReadout); timeline.appendChild(this.speedReadout); - this.curUPS = 16; - if (this.conf.tournamentMode) { - this.curUPS = 8; // for tournament!!! - } - + this.setDefaultUPS(); + // create the button controls let buttons = document.createElement("td"); buttons.vAlign = "top"; @@ -138,8 +132,8 @@ export default class Controls { // create the info string display let infoString = document.createElement("td"); - infoString.vAlign = "top"; - infoString.style.fontSize = "11px"; + infoString.vAlign = "middle"; + infoString.className = "info"; this.infoString = infoString; table.appendChild(tr); @@ -155,9 +149,10 @@ export default class Controls { function changeTime(dragEvent: MouseEvent) { // jump to a frame when clicking the controls timeline if (runner.looper) { + const loadedTime = !conf.tournamentMode ? runner.looper.match['_farthest'].turn : 1500; let width: number = (this).width; - let turn: number = dragEvent.offsetX / width * runner.looper.match['_farthest'].turn; - turn = Math.round(Math.min(runner.looper.match['_farthest'].turn, turn)); + let turn: number = dragEvent.offsetX / width * loadedTime; + turn = Math.round(Math.min(loadedTime, turn)); runner.looper.onSeek(turn); } @@ -212,17 +207,23 @@ export default class Controls { private timeline() { let canvas = document.createElement("canvas"); canvas.id = "timelineCanvas"; - canvas.width = 400; + canvas.width = 300; canvas.height = 1; this.ctx = canvas.getContext("2d"); this.ctx.fillStyle = "white"; this.canvas = canvas; if (this.conf.tournamentMode) { - canvas.style.display = 'none'; // we don't wanna reveal how many rounds there are! + //canvas.style.display = 'none'; // we don't wanna reveal how many rounds there are! } return canvas; } + setDefaultText() { + this.timeReadout.innerHTML = 'No match loaded'; + this.tileInfo.innerHTML = 'X | Y | Passability'; + this.speedReadout.textContent = 'UPS: FPS: '; + } + /** * Returns the UPS determined by the slider */ @@ -230,6 +231,13 @@ export default class Controls { return this.curUPS; } + setDefaultUPS() { + this.curUPS = 16; + if (this.conf.tournamentMode) { + this.curUPS = 8; // for tournament!!! + } + } + /** * Displays the correct controls depending on whether we are in game mode * or map editor mode @@ -345,38 +353,38 @@ export default class Controls { */ onFinish(match: Match, meta: Metadata) { if (this.runner.looper && !this.runner.looper.isPaused()) this.pause(); - if (this.conf.tournamentMode) { - // also update the winner text - this.setWinner(match, meta); - } + // if (this.conf.tournamentMode) { + // // also update the winner text + // this.setWinner(match, meta); + // } } - setWinner(match: Match, meta: Metadata) { - console.log('winner: ' + match.winner); - const matchWinner = this.winnerTeam(meta.teams, match.winner); - while (this.winnerDiv.firstChild) { - this.winnerDiv.removeChild(this.winnerDiv.firstChild); - } - this.winnerDiv.appendChild(matchWinner); - } + // setWinner(match: Match, meta: Metadata) { + // console.log('winner: ' + match.winner); + // const matchWinner = this.winnerTeam(meta.teams, match.winner); + // while (this.winnerDiv.firstChild) { + // this.winnerDiv.removeChild(this.winnerDiv.firstChild); + // } + // this.winnerDiv.appendChild(matchWinner); + // } - private winnerTeam(teams, winnerID: number | null): HTMLSpanElement { - const span = document.createElement("span"); - if (winnerID === null) { - return span; - } else { - // Find the winner - let teamNumber = 1; - for (let team in teams) { - if (teams[team].teamID === winnerID) { - span.className += team === "1" ? " red" : " blue"; - span.innerHTML = teams[team].name + " wins!"; - break; - } - } - } - return span; - } + // private winnerTeam(teams, winnerID: number | null): HTMLSpanElement { + // const span = document.createElement("span"); + // if (winnerID === null) { + // return span; + // } else { + // // Find the winner + // let teamNumber = 1; + // for (let team in teams) { + // if (teams[team].teamID === winnerID) { + // span.className += team === "1" ? " red" : " blue"; + // span.innerHTML = teams[team].name + " wins!"; + // break; + // } + // } + // } + // return span; + // } /** * Redraws the timeline and sets the current round displayed in the controls. @@ -384,10 +392,12 @@ export default class Controls { // TODO scale should be constant; should not depend on loadedTime setTime(time: number, loadedTime: number, upsUnpaused: number, paused: Boolean, fps: number, lagging: Boolean) { - if (!this.conf.tournamentMode) { + if (this.conf.tournamentMode) loadedTime = 1500; + + // if (!this.conf.tournamentMode) { // Redraw the timeline + const scale = this.canvas.width / loadedTime; - // const scale = this.canvas.width / cst.MAX_ROUND_NUM; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.fillStyle = "rgb(39, 39, 39)"; @@ -398,25 +408,25 @@ export default class Controls { this.ctx.fillStyle = 'rgb(255,0,0)'; this.ctx.fillRect(time * scale, 0, 2, this.canvas.height); - } + // } let speedText = (lagging ? '(Lagging) ' : '') + `UPS: ${upsUnpaused | 0}` + (paused ? ' (Paused)' : '') + ` FPS: ${fps | 0}`; speedText = speedText.padStart(32); this.speedReadout.textContent = speedText; - this.timeReadout.textContent = (this.conf.tournamentMode ? `Round: ${time}` : `Round: ${time}/${loadedTime}`); + this.timeReadout.innerHTML = (this.conf.tournamentMode ? `Round: ${time}` : `Round: ${time}/${loadedTime}`); } /** * Updates the location readout */ - setTileInfo(x: number, y: number, passability: number): void { + setTileInfo(x: number, y: number, xrel: number, yrel: number, passability: number): void { let content: string = ""; - content += 'X: ' + `${x}`.padStart(3); - content += ' | Y: ' + `${y}`.padStart(3); - content += ' | Passability: ' + `${passability.toFixed(3)}`; + content += 'X: ' + `${xrel}`.padStart(3) + ` (${x})`.padStart(3); + content += ' | Y: ' + `${yrel}`.padStart(3) + ` (${y})`.padStart(3); + content += ' | Passability: ' + `${passability.toFixed(3)}`; - this.tileInfo.textContent = content; + this.tileInfo.innerHTML = content; } /** @@ -427,13 +437,26 @@ export default class Controls { * Bytecodes Used: bytecodes" */ // TODO fix this (different stats) - setInfoString(id, x: number, y: number, influence: number, conviction: number, bodyType: string, bytecodes: number, flag: number): void { + setInfoString(id, x: number, y: number, influence: number, conviction: number, bodyType: string, bytecodes: number, flag: number, bid?: number, parent?: number): void { // console.log(carryDirt); - let infoString = `Robot ID ${id} (${bodyType})
- Location: (${x}, ${y})
- Influence: ${influence}, Conviction: ${conviction}
- Bytecodes Used: ${bytecodes}, Flag ${flag}`; + let infoString = `ID: ${id} | `; + infoString += `Location: (${x}, ${y})
`; + infoString += `Influence: ${influence} | `; + infoString += `Conviction: ${conviction}
`; + infoString += `Flag: ${flag} | `; + infoString += `Bytecodes Used: ${bytecodes}`; + if (bid !== undefined) infoString += ` | Bid: ${bid}`; + if (parent !== undefined) infoString += ` | Parent: ${parent}`; + + // (${bodyType})
+ // Location: (${x}, ${y})
+ //Influence: ${influence}, Conviction: ${conviction}
+ //Bytecodes Used: ${bytecodes}, Flag ${flag}`; this.infoString.innerHTML = infoString; } + + removeInfoString() { + this.infoString.innerHTML = ""; + } } diff --git a/client/visualizer/src/main/electron-modules.ts b/client/visualizer/src/main/electron-modules.ts index a2a25697..de49e9ac 100644 --- a/client/visualizer/src/main/electron-modules.ts +++ b/client/visualizer/src/main/electron-modules.ts @@ -19,6 +19,7 @@ if (!process.env.ELECTRON) { define('path', [], () => null); define('child_process', [], () => null); define('http', [], () => null); + define('clipboard', [], () => null); } // in electron, actually imports diff --git a/client/visualizer/src/main/looper.ts b/client/visualizer/src/main/looper.ts index 01bab8b5..ed665929 100644 --- a/client/visualizer/src/main/looper.ts +++ b/client/visualizer/src/main/looper.ts @@ -32,6 +32,7 @@ export default class Looper { private updatesPerSecond: TickCounter; private lastSelectedID: number | undefined; private renderer: Renderer; + private loadedProfiler: boolean; private console: Console; @@ -39,8 +40,10 @@ export default class Looper { private conf: config.Config, private imgs: imageloader.AllImages, private controls: Controls, private stats: Stats, private gamearea: GameArea, cconsole: Console, - private matchqueue: MatchQueue) { - + private matchqueue: MatchQueue, private profiler?: Profiler, + private mapinfo: string = "", + showTourneyUpload: boolean = true) { + this.console = cconsole; this.conf.mode = config.Mode.GAME; @@ -50,6 +53,10 @@ export default class Looper { // Cancel previous games if they're running this.clearScreen(); + // rotate tall maps + if (this.conf.doRotate) this.conf.doingRotate = (match.current.maxCorner.y - match.current.minCorner.y) > (match.current.maxCorner.x - match.current.minCorner.x); + else this.conf.doingRotate = false; + // Reset the canvas this.gamearea.setCanvasDimensions(match.current); @@ -61,7 +68,9 @@ export default class Looper { teamIDs.push(meta.teams[team].teamID); } this.stats.initializeGame(teamNames, teamIDs); - this.console.setLogsRef(match.logs); + const extraInfo = (this.mapinfo ? this.mapinfo + "\n" : "") + (this.conf.doingRotate ? " (Map rotated and flipped! Disable for new matches with 'Z'.)" : ""); + this.stats.setExtraInfo(extraInfo); + if (!showTourneyUpload) this.stats.hideTourneyUpload(); // keep around to avoid reallocating this.nextStep = new NextStep(); @@ -73,9 +82,9 @@ export default class Looper { this.lastSelectedID = id; this.console.setIDFilter(id); }; - const onMouseover = (x: number, y: number, passability: number) => { + const onMouseover = (x: number, y: number, xrel: number, yrel: number, passability: number) => { // Better make tile type and hand that over - controls.setTileInfo(x, y, passability); + controls.setTileInfo(x, y, xrel, yrel, passability); }; // Configure renderer for this match @@ -84,10 +93,11 @@ export default class Looper { this.conf, meta as Metadata, onRobotSelected, onMouseover); // How fast the simulation should progress - this.goalUPS = this.controls.getUPS(); - if (this.conf.tournamentMode) { - this.goalUPS = 0; // FOR TOURNAMENT - } + // this.goalUPS = this.controls.getUPS(); + // if (this.conf.tournamentMode) { + // Always pause on load. Mitigates funky behaviour like 100 rounds playing before any rendering occurs. + this.goalUPS = 0; + // } // A variety of stuff to track how fast the simulation is going this.rendersPerSecond = new TickCounter(.5, 100); @@ -103,6 +113,11 @@ export default class Looper { this.controls.updatePlayPauseButton(this.isPaused()); + if (this.profiler) + this.profiler.reset(); + + this.loadedProfiler = false; + this.loopID = window.requestAnimationFrame((curTime) => this.loop.call(this, curTime)); }; @@ -170,10 +185,13 @@ export default class Looper { this.clearScreen(); this.goalUPS = 0; this.controls.pause(); + this.controls.removeInfoString(); + this.controls.setDefaultText(); + this.controls.setDefaultUPS(); } private loop(curTime) { - + let delta = 0; if (this.lastTime === null) { // first simulation step @@ -212,8 +230,8 @@ export default class Looper { // run simulation // this may look innocuous, but it's a large chunk of the run time - this.match.compute(5 /* ms */); - + this.match.compute(30 /* ms */); // An ideal FPS is around 30 = 1000/30, so when compute takes its full time + // FPS is lowered significantly. But I think it's a worthwhile tradeoff. // update the info string in controls if (this.lastSelectedID !== undefined) { let bodies = this.match.current.bodies.arrays; @@ -230,14 +248,26 @@ export default class Looper { let type = bodies.type[index]; let bytecodes = bodies.bytecodesUsed[index]; let flag = bodies.flag[index]; + let parent = bodies.parent[index]; + let bid = bodies.bid[index]; - this.controls.setInfoString(id, x, y, influence, conviction, cst.bodyTypeToString(type), bytecodes, flag); + this.controls.setInfoString(id, x, y, influence, conviction, cst.bodyTypeToString(type), bytecodes, flag, + bid !== 0 ? bid : undefined, parent !== 0 ? parent : undefined); } } - this.console.seekRound(this.match.current.turn); + if (this.lastSelectedID === undefined) { + this.controls.removeInfoString(); + } + this.lastTime = curTime; - this.lastTurn = this.match.current.turn; + + if (this.match.current.turn != this.lastTurn) { + this.console.setLogsRef(this.match.current.logs, this.match.current.logsShift); + this.console.seekRound(this.match.current.turn); + this.lastTurn = this.match.current.turn; + this.updateStats(this.match.current, this.meta); + } // @ts-ignore // renderer.render(this.match.current, this.match.current.minCorner, this.match.current.maxCorner); @@ -255,7 +285,7 @@ export default class Looper { let lerp = Math.min(this.interpGameTime - this.match.current.turn, 1); // @ts-ignore - this.renderer.render(this.match.current, this.match.current.minCorner, this.match.current.maxCorner, curTime, this.nextStep, lerp); + this.renderer.render(this.match.current, this.match.current.minCorner, this.match.current.maxCorner, curTime, this.nextStep, this.isPaused() ? 0 : lerp); } else { //console.log('not interpolating!!'); // interpGameTime might be incorrect if we haven't computed fast enough @@ -263,7 +293,12 @@ export default class Looper { this.renderer.render(this.match.current, this.match.current.minCorner, this.match.current.maxCorner, curTime); } - this.updateStats(this.match.current, this.meta); + if (this.profiler && this.match.profilerFiles.length && !this.loadedProfiler) { + this.profiler.load(this.match); + this.loadedProfiler = true; + } + + //this.updateStats(this.match.current, this.meta); this.loopID = window.requestAnimationFrame((curTime) => this.loop.call(this, curTime)); } @@ -272,6 +307,28 @@ export default class Looper { * team in the current game world. */ private updateStats(world: GameWorld, meta: Metadata) { + let totalInfluence = 0; + let totalConviction = 0; + let teamIDs: number[] = []; + let teamNames: string[] = []; + + this.stats.resetECs(); + for (let i = 0; i < world.bodies.length; i++) { + const type = world.bodies.arrays.type[i]; + if (type === schema.BodyType.ENLIGHTENMENT_CENTER) { + this.stats.addEC(world.bodies.arrays.team[i]); + } + } + + for (let team in meta.teams) { + let teamID = meta.teams[team].teamID; + let teamStats = world.teamStats.get(teamID) as TeamStats; + totalInfluence += teamStats.influence.reduce((a, b) => a + b); + totalConviction += teamStats.conviction.reduce((a, b) => a + b); + teamIDs.push(teamID); + teamNames.push(meta.teams[team].name); + } + for (let team in meta.teams) { let teamID = meta.teams[team].teamID; let teamStats = world.teamStats.get(teamID) as TeamStats; @@ -279,10 +336,21 @@ export default class Looper { // Update each robot count this.stats.robots.forEach((type: schema.BodyType) => { this.stats.setRobotCount(teamID, type, teamStats.robots[type]); + this.stats.setRobotConviction(teamID, type, teamStats.conviction[type], totalConviction); + this.stats.setRobotInfluence(teamID, type, teamStats.influence[type]); }); // Set votes this.stats.setVotes(teamID, teamStats.votes); + this.stats.setTeamInfluence(teamID, teamStats.influence.reduce((a, b) => a + b), + totalInfluence); + this.stats.setBuffs(teamID, teamStats.numBuffs); + this.stats.setBid(teamID, teamStats.bid); + this.stats.setIncome(teamID, teamStats.income, world.turn); + } + + if (this.match.winner && this.match.current.turn == this.match.lastTurn) { + this.stats.setWinner(this.match.winner, teamNames, teamIDs); } } diff --git a/client/visualizer/src/main/scaffold.ts b/client/visualizer/src/main/scaffold.ts index a8ddb2e0..842bcb77 100644 --- a/client/visualizer/src/main/scaffold.ts +++ b/client/visualizer/src/main/scaffold.ts @@ -13,6 +13,7 @@ const GRADLE_WRAPPER = WINDOWS ? 'gradlew.bat' : 'gradlew'; */ export default class ScaffoldCommunicator { scaffoldPath: string; + procs: any[] = []; // pids of spawned child processes constructor(scaffoldPath: string) { if (!process.env.ELECTRON) throw new Error("Can't talk to scaffold in the browser!"); @@ -25,6 +26,10 @@ export default class ScaffoldCommunicator { if (!fs.existsSync(this.wrapperPath)) { throw new Error(`Can't find gradle wrapper: ${this.wrapperPath}`); } + + electron.remote.app.on('before-quit', function() { + this.killProcs(); + }); } get wrapperPath() { @@ -49,8 +54,8 @@ export default class ScaffoldCommunicator { console.log('app path: ' + appPath); - // npm run electron in client, if battlecode20-scaffold is located in same level as battlecode20 - const fromDev = path.join(path.dirname(path.dirname(path.dirname(appPath))), 'battlecode20-scaffold'); + // npm run electron in client, if battlecode21-scaffold is located in same level as battlecode21 + const fromDev = path.join(path.dirname(path.dirname(path.dirname(appPath))), 'battlecode21-scaffold'); // scaffold/client/Battlecode Client[.exe] // (May never happen?) const fromWin = path.dirname(path.dirname(appPath)); @@ -116,8 +121,8 @@ export default class ScaffoldCommunicator { } // paths are relative for readdir - return cb(null, new Set(files.filter((file) => file.endsWith('.map20')) - .map((file) => file.substring(0, file.length - 6)) + return cb(null, new Set(files.filter((file) => file.endsWith('.map21')) + .map((file) => file.substring(0, file.length - '.map21'.length)) .concat(Array.from(SERVER_MAPS.keys())))); }); }); @@ -132,7 +137,7 @@ export default class ScaffoldCommunicator { fs.mkdirSync(dir); } - fs.writeFile(path.join(this.scaffoldPath, 'maps', `${mapName}.map17`), + fs.writeFile(path.join(this.scaffoldPath, 'maps', `${mapName}.map21`), new Buffer(mapData), cb); } @@ -145,21 +150,24 @@ export default class ScaffoldCommunicator { * TODO what if the server hangs? */ runMatch(teamA: string, teamB: string, maps: string[], enableProfiler: boolean, + onStart: (cmd: string) => void, onErr: (err: Error) => void, onExitNoError: () => void, onStdout: (data: string) => void, onStderr: (data: string) => void) { + const options = [ + `runFromClient`, + `-x`, + `unpackClient`, + `-PteamA=${teamA}`, + `-PteamB=${teamB}`, + `-Pmaps=${maps.join(',')}`, + `-PprofilerEnabled=${enableProfiler}`, + ]; const proc = child_process.spawn( this.wrapperPath, - [ - `runFromClient`, - `-x`, - `unpackClient`, - `-PteamA=${teamA}`, - `-PteamB=${teamB}`, - `-Pmaps=${maps.join(',')}`, - `-PprofilerEnabled=${enableProfiler}`, - ], + options, {cwd: this.scaffoldPath} ); + onStart(this.wrapperPath + " " + options.join("\n")); const decoder = new window['TextDecoder'](); // @ts-ignore proc.stdout.on('data', (data) => onStdout(decoder.decode(data))); @@ -175,9 +183,21 @@ export default class ScaffoldCommunicator { proc.on('error', (err) => { onErr(err); }); + proc.on('pid-message', function(event, arg) { + console.log('Main:', arg); + this.pids.push(arg); + }); + this.procs.push(proc); + } + + killProcs() { + this.procs.forEach(function(proc) { + proc.kill(); + }); } } + /** * Walk a directory and return all the files found. */ diff --git a/client/visualizer/src/main/sidebar.ts b/client/visualizer/src/main/sidebar.ts index 96e87018..3b85d93e 100644 --- a/client/visualizer/src/main/sidebar.ts +++ b/client/visualizer/src/main/sidebar.ts @@ -28,7 +28,7 @@ export default class Sidebar { readonly mapeditor: MapEditor; readonly matchrunner: MatchRunner; readonly matchqueue: MatchQueue; - readonly profiler: Profiler; + readonly profiler?: Profiler; private readonly help: HTMLDivElement; // Options @@ -40,15 +40,13 @@ export default class Sidebar { // Update texts private updateText: HTMLDivElement; - // onkeydown event that uses the controls depending on the game mode - private readonly onkeydownControls: (event: KeyboardEvent) => void; + // Mode panel + private modePanel: HTMLTableElement; // Callback to update the game area when changing modes cb: () => void; - // onkeydownControls is an onkeydown event that uses the controls depending on the game mode - constructor(conf: Config, images: AllImages, runner: Runner, - onkeydownControls: (event: KeyboardEvent) => void) { + constructor(conf: Config, images: AllImages, runner: Runner) { // Initialize fields this.div = document.createElement("div"); this.innerDiv = document.createElement("div"); @@ -73,31 +71,36 @@ export default class Sidebar { // set callback for running a game, which should trigger the update check this.updateUpdate(); }); - this.profiler = new Profiler(); - this.matchqueue = new MatchQueue(conf, images, this.profiler, runner); + if (conf.useProfiler) this.profiler = new Profiler(conf); + this.matchqueue = new MatchQueue(conf, images, runner); this.stats = new Stats(conf, images, runner); - this.help = this.initializeHelp(); this.conf = conf; - this.onkeydownControls = onkeydownControls; + this.help = this.initializeHelp(); // Initialize div structure this.loadStyles(); this.div.appendChild(this.screamForUpdate()); this.div.appendChild(this.battlecodeLogo()); - const modePanel = document.createElement('table'); - modePanel.className = 'modepanel'; - const modePanelRow = document.createElement('tr'); + this.modePanel = document.createElement('table'); + this.modePanel.className = 'modepanel'; + + const modePanelRow1 = document.createElement('tr'); + const modePanelRow2 = document.createElement('tr'); + this.modeButtons = new Map(); - modePanelRow.appendChild(this.modeButton(Mode.GAME, "Game")); - modePanelRow.appendChild(this.modeButton(Mode.LOGS, "Logs")); - modePanelRow.appendChild(this.modeButton(Mode.QUEUE, "Queue")); - modePanelRow.appendChild(this.modeButton(Mode.RUNNER, "Runner")); - // modePanelRow.appendChild(this.modeButton(Mode.PROFILER, "Profiler")); - modePanelRow.appendChild(this.modeButton(Mode.MAPEDITOR, "Map Editor")); - modePanelRow.appendChild(this.modeButton(Mode.HELP, "Help")); - modePanel.appendChild(modePanelRow); - this.div.appendChild(modePanel); + modePanelRow1.appendChild(this.modeButton(Mode.GAME, "Game")); + modePanelRow1.appendChild(this.modeButton(Mode.LOGS, "Logs")); + modePanelRow1.appendChild(this.modeButton(Mode.QUEUE, "Queue")); + modePanelRow1.appendChild(this.modeButton(Mode.RUNNER, "Runner")); + if (this.conf.useProfiler) modePanelRow2.appendChild(this.modeButton(Mode.PROFILER, "Profiler")); + modePanelRow2.appendChild(this.modeButton(Mode.MAPEDITOR, "Map Editor")); + modePanelRow2.appendChild(this.modeButton(Mode.HELP, "Help")); + + this.modePanel.appendChild(modePanelRow1); + this.modePanel.appendChild(modePanelRow2); + + this.div.appendChild(this.modePanel); this.div.appendChild(this.innerDiv); @@ -107,7 +110,6 @@ export default class Sidebar { this.setSidebar(); } - /** * Sets a scaffold if a scaffold directory is found after everything is loaded */ @@ -120,15 +122,20 @@ export default class Sidebar { * Initializes the help div */ private initializeHelp(): HTMLDivElement { - const innerHTML: string = + var innerHTML: string = ` + Beware of too much logging! +
+ If your match has a significant amount of logging, please turn off log processing with + the L key.
+
Issues?
  1. Refresh (Ctrl-R or Command-R).
  2. Search Discord.
  3. Ask on Discord (attach a screenshot of console output using F12).
- Keyboard Shortcuts
+ Keyboard Shortcuts (Game)
LEFT - Step Back One Turn
RIGHT - Step Forward One Turn
UP - Double Playback Speed
@@ -136,67 +143,92 @@ export default class Sidebar { P - Pause/Unpause
O - Stop (Go to Start)
E - Go to End
- V - Toggle Indicator Dots/Lines
+ V - Toggle Indicator Dots/Lines for Selected Robot
+ C - Toggle All Indicator Dots/Lines
G - Toggle Grid
N - Toggle Action Radius
M - Toggle Sensor Radius
, - Toggle Detection Radius
H - Toggle Shorter Log Headers
B - Toggle Interpolation
- + L - Toggle whether to process logs.
+ Q - Toggle whether to profile matches.
+ Z - Toggle whether to rotate tall maps.
+ [ - Hide/unhide sidebar navigation.
+
+ Keyboard Shortcuts (Map Editor)
- How to Play a Match
- From the application: Click 'Runner' and follow the - instructions in the sidebar. Note that it may take a few seconds for - matches to be displayed.
- From the web client: If you are not running the client as a - stand-alone application, you can always upload a .bc21 file by - clicking upload button in the 'Queue' section.
+ S - Add
+ D - Delete
+ R - Reverse team

- Use the control buttons in 'Queue' and the top of the screen to - navigate the match.
+ How to Play a Match
+ From the application: Click Runner, select the bots and + your desired map, then press "Run Game". Note that it may take a few seconds for + matches to be displayed. To stop processing a match before it has finished, + press "Kill ongoing processes". Note that the part of the match that has already + loaded will remain in the client.
+
+ From the web client: You can always upload a .bc21 file by + clicking the upload button in the Queue section.
+
+ Use the control buttons at the top of the screen to + navigate the match. Click on different matches in the Queue section to switch between them.

How to Use the Console
- The console displays all System.out.println() data up to the current round. + The console displays all System.out.println() data up to the current round. You can filter teams by checking the boxes and robot IDs by clicking the robot. You can also change the maximum number of rounds displayed in the - input box. (WARNING: If you want to, say, suddenly display 3000 rounds - of data on round 2999, pause the client first to prevent freezing.)
+ input box. Beware of doing too much logging! This slows down the client. + (WARNING: If you want to, say, suddenly display 3000 rounds + of data, pause the client first to prevent freezing.)

- How to Use the Profiler
+ How to Use the Profiler
+ Be cautious of memory issues when profiling large games. To disable profiling + on a profiled match file, press "Q".
The profiler can be used to find out which methods are using a lot of bytecodes. To use it, tick the "Profiler enabled" checkbox in the Runner before running the game. Make sure that the runFromClient Gradle task sets bc.engine.enable-profiler to the value of the "profilerEnabled" property, as can be seen in the - scaffold player. + scaffold player. Make sure to add the "profilerEnabled" property to your - gradle.properties - file as well. - - Note that for games with a large number of units, it might be impossible - to run the profiler successfully (you might get an OutOfMemoryError). + gradle.properties + file as well. A maximum of 2,000,000 events are recorded per team per + match if profiling is enabled to prevent the replay file from becoming + enormous.

-
+
+ To set tiles' passability values, enter the "change tiles" mode, select the passability value, brush size, and brush style, + and then hold and drag your mouse across the map.

- Modify or delete existing units by clicking on them, making changes, then - clicking “Add/Update."
+ To save an intermediary version of your map, copy the map JSON. You can input this JSON later to retrieve your map in the map editor for further editing.

- Before exporting, click "Validate" to see if any changes need to be + + When you are happy with your map, click "Export". If you are directed to save your map, save it in the - /battlecode-scaffold-2017-master/maps directory of your scaffold. - (Note: the name of your .map17 file must be the same as the name of your - map.)-->`; + /battlecode-scaffold-2021/maps directory of your scaffold. + (Note: the name of your .map21 file must be the same as the name of your + map.)
+
+ Exported file name must be the same as the map name chosen above. For instance, DefaultMap.bc21.`; + + if (this.conf.tournamentMode) { + innerHTML += + `

+ Tournament Mode Keyboard Shortcuts
+ D - Next match
+ A - Previous match` + } const div = document.createElement("div"); div.id = "helpDiv"; @@ -329,7 +361,7 @@ export default class Sidebar { this.innerDiv.appendChild(this.mapeditor.div); break; case Mode.PROFILER: - this.innerDiv.append(this.profiler.div); + if (this.profiler) this.innerDiv.append(this.profiler.div); break; } @@ -337,4 +369,8 @@ export default class Sidebar { this.cb(); } } + + hidePanel() { + this.modePanel.style.display = (this.modePanel.style.display === "" ? "none" : ""); + } } diff --git a/client/visualizer/src/main/splash.ts b/client/visualizer/src/main/splash.ts index c238a0ae..4f9f0931 100644 --- a/client/visualizer/src/main/splash.ts +++ b/client/visualizer/src/main/splash.ts @@ -1,5 +1,5 @@ import {Config} from '../config'; -import {Tournament, TournamentGame, TournamentMatch} from './tournament'; +//import {Tournament, TournamentMatch} from './tournament_new'; /** * The splash screen for tournaments. Appears between every match @@ -14,9 +14,9 @@ export default class Splash { // Containers private static header: HTMLDivElement = document.createElement("div"); private static subHeader: HTMLDivElement = document.createElement("div"); - private static columnLeft: HTMLDivElement = document.createElement("div"); - private static columnRight: HTMLDivElement = document.createElement("div"); - private static columnCenter: HTMLDivElement = document.createElement("div"); + private static team1Div: HTMLDivElement = document.createElement("div"); + private static team2Div: HTMLDivElement = document.createElement("div"); + private static versusDiv: HTMLDivElement = document.createElement("div"); // Team elements to modify every time we change the screen private static avatarA: HTMLImageElement = document.createElement("img"); @@ -47,30 +47,24 @@ export default class Splash { Splash.subHeader.className = "tournament-subheader"; // Team A information (red) - Splash.columnLeft.className = "column-left"; - Splash.columnLeft.appendChild(Splash.avatarA); - Splash.avatarA.className = "avatar"; - Splash.columnLeft.appendChild(document.createElement("br")); - Splash.columnLeft.appendChild(Splash.nameAndIDA); + Splash.team1Div.id = "team1"; + Splash.team1Div.appendChild(Splash.nameAndIDA); // Center column (vs.) - Splash.columnCenter.className = "column-center"; - Splash.columnCenter.appendChild(document.createTextNode("vs.")); + Splash.versusDiv.id = "versus"; + Splash.versusDiv.appendChild(document.createTextNode("versus")); // Team B information (blue) - Splash.columnRight.className = "column-right"; - Splash.columnRight.appendChild(Splash.avatarB); - Splash.avatarB.className = "avatar"; - Splash.columnRight.appendChild(document.createElement("br")); - Splash.columnRight.appendChild(Splash.nameAndIDB); + Splash.team2Div.id = "team2"; + Splash.team2Div.appendChild(Splash.nameAndIDB); // Put everything together Splash.container.appendChild(Splash.header); Splash.container.appendChild(Splash.subHeader); Splash.container.appendChild(document.createElement("br")); - Splash.container.appendChild(Splash.columnLeft); - Splash.container.appendChild(Splash.columnCenter); - Splash.container.appendChild(Splash.columnRight); + Splash.container.appendChild(Splash.team1Div); + Splash.container.appendChild(Splash.versusDiv); + Splash.container.appendChild(Splash.team2Div); Splash.screen.appendChild(Splash.container); Splash.loaded = true; @@ -86,39 +80,40 @@ export default class Splash { Splash.winnerScreen.className = "blackout"; Splash.winnerContainer.className = "blackout-container"; - // Put everything together + // Put everything together'\ + Splash.winnerHeader.id = "winner"; Splash.winnerContainer.appendChild(Splash.winnerHeader); - Splash.winnerContainer.appendChild(document.createElement("br")); - Splash.winnerContainer.appendChild(Splash.winnerAvatar); - Splash.winnerContainer.appendChild(document.createElement('br')); - Splash.winnerContainer.appendChild(Splash.winnerExtra); + // Splash.winnerContainer.appendChild(document.createElement("br")); + // Splash.winnerContainer.appendChild(Splash.winnerAvatar); + // Splash.winnerContainer.appendChild(document.createElement('br')); + // Splash.winnerContainer.appendChild(Splash.winnerExtra); Splash.winnerScreen.appendChild(Splash.winnerContainer); Splash.winnerLoaded = true; } } - static addScreen(conf: Config, root: HTMLElement, game: TournamentGame, match: TournamentMatch, tournament: Tournament): void { + static addScreen(conf: Config, root: HTMLElement, team1: string, team2: string): void { this.loadScreen(); - this.header.innerText = match.description;//this.getBracketString(tournament); + // this.header.innerText = 'Round 1';//this.getBracketString(tournament); // this.subHeader.innerText = `Game ${tournament.gameIndex+1} of ${tournament.roundLengths[tournament.roundIndex]}`; - this.avatarA.src = tournament.getAvatar(match.team1_name); - this.nameAndIDA.innerText = `${match.team1_name} (#${match.team1_id})`; - this.avatarB.src = tournament.getAvatar(match.team2_name); - this.nameAndIDB.innerText = `${match.team2_name} (#${match.team2_id})`; + // this.avatarA.src = tournament.getAvatar(match.team1_name); + this.nameAndIDA.innerText = team1; + // this.avatarB.src = tournament.getAvatar(match.team2_name); + this.nameAndIDB.innerText = team2; root.appendChild(this.screen) } - static addWinnerScreen(conf: Config, root: HTMLElement, tournament: Tournament, match: TournamentMatch) { + static addWinnerScreen(conf: Config, root: HTMLElement, text: string) { this.loadWinnerScreen(); - this.winnerHeader.innerText = `${match.winner_name} (#${match.winner_id}) wins!`; + this.winnerHeader.innerText = text; this.winnerHeader.className = "tournament-header"; - this.winnerAvatar.className = "big-avatar"; - this.winnerAvatar.src = tournament.getAvatar(match.winner_name); + // this.winnerAvatar.className = "big-avatar"; + // this.winnerAvatar.src = tournament.getAvatar(match.winner_name); root.appendChild(this.winnerScreen); } diff --git a/client/visualizer/src/main/tournament.ts b/client/visualizer/src/main/tournament.ts index aeed8c1d..6a134262 100644 --- a/client/visualizer/src/main/tournament.ts +++ b/client/visualizer/src/main/tournament.ts @@ -1,6 +1,6 @@ import {path, fs} from './electron-modules'; -export function readTournament(jsonFile: File, cb: (err: Error | null, t: Tournament | null) => void) { +export function readTournament(jsonFile: File, cbTournament: (t: Tournament) => void, cbError: (err: Error) => void) { /*if (!process.env.ELECTRON) { cb(new Error("Can't read tournaments outside of electron"), null); return; @@ -11,18 +11,18 @@ export function readTournament(jsonFile: File, cb: (err: Error | null, t: Tourna console.log('reader RESULT'); console.log(reader.result); if (reader.error) { - cb(reader.error, null); + cbError(reader.error); return; } - var tournament; + var tournament: Tournament; try { tournament = new Tournament(JSON.parse(reader.result)); } catch (e) { - cb(e, null); + cbError(e); return; } - cb(null, tournament); + cbTournament(tournament); }; reader.readAsText(jsonFile); } diff --git a/client/visualizer/src/main/tournament_new.ts b/client/visualizer/src/main/tournament_new.ts new file mode 100644 index 00000000..31d0a536 --- /dev/null +++ b/client/visualizer/src/main/tournament_new.ts @@ -0,0 +1,158 @@ +import { path, fs } from './electron-modules'; + +// matches required to win a tourney game +const goal = 3; + +export function readTournament(jsonFile: File, cbTournament: (t: Tournament) => void, cbError: (err: Error) => void) { + const reader = new FileReader(); + reader.onload = () => { + console.log('reader RESULT'); + console.log(reader.result); + if (reader.error) { + cbError(reader.error); + return; + } + + try { + const data: any[][] = JSON.parse(reader.result); + const parseMatch: (arr: [string, string, string, number, number]) => TournamentMatch = ((arr) => ({ + team1: arr[0], + team2: arr[1], + map: arr[2], + winner: arr[3], + url: "https://2021.battlecode.org/replays/" + arr[4] + ".bc21" + })); + const desc: TournamentMatch[][] = data.filter(game => game != null).map((game) => (game.map(parseMatch))); + const tournament = new Tournament(desc); + cbTournament(tournament); + } catch (e) { + cbError(e); + return; + } + }; + reader.readAsText(jsonFile); +} + +// Use like a "cursor" into a tournament. +// It's a bit awkward. +export class Tournament { + readonly games: TournamentMatch[][]; + // cursors to games + // this is the index within a game + matchI: number; + // cursor to game index + gameI: number; + + constructor(games: TournamentMatch[][]) { + this.matchI = 0; + this.gameI = 0; + this.games = games; + } + + seek(gameIndex: number, matchIndex: number) { + if (gameIndex < this.games.length && matchIndex < this.games[gameIndex].length) { + this.matchI = matchIndex; + this.gameI = gameIndex; + } else { + throw new Error("Out of bounds: " + matchIndex + "," + gameIndex); + } + } + + hasNext(): boolean { + return this.gameI < this.games.length - 1 || this.matchI < this.games[this.gameI].length - 1; + } + + next() { + if (!this.hasNext()) { + throw new Error("No more matches!"); + } + if (this.isLastMatchInGame()) { + this.matchI = 0; + this.gameI++; + } + else this.matchI++; + console.log(`game index: ${this.gameI}\n match index: ${this.matchI}`); + } + + // getAvatar(name: string) { + // // convert the name into an ID + // // TODO: speed this up + // for (var i = 0; i < this.desc.teams.length; i++) { + // if (this.desc.teams[i].name === name) { + // return 'file://' + path.join(this.dir, this.desc.teams[i].avatarPath); + // } + // } + // return "not found :(("; + // } + + isLastMatchInGame(): boolean { + return this.matchI === this.games[this.gameI].length - 1 || (Math.max(this.wins()[this.current().team1], this.wins()[this.current().team2]) >= goal); + } + + isFirstMatchInGame(): boolean { + return this.matchI === 0; + } + + hasPrev(): boolean { + return this.matchI > 0 || this.gameI > 0; + } + + prev() { + if (!this.hasPrev()) { + throw new Error("No previous matches!"); + } + this.matchI--; + if (this.matchI < 0) { + this.gameI--; + this.matchI = 0; + while (!this.isLastMatchInGame()) this.matchI++; + } + console.log(`game index: ${this.gameI}\n match index: ${this.matchI}`); + } + + current(): TournamentMatch { + if (this.gameI >= this.games.length || this.matchI >= this.games[this.gameI].length) { + throw new Error(`BAD COMBO: match ${this.matchI}, ${this.gameI}`); + } + if (this.games[this.gameI][this.matchI] == undefined) { + throw new Error("Undefined game?? " + this.matchI); + } + return this.games[this.gameI][this.matchI]; + } + + currentGame(): TournamentMatch[] { + if (this.gameI > this.games.length) { + throw new Error(`game out of bounds: ${this.gameI}`); + } + if (this.games[this.gameI] == undefined) { + throw new Error("Undefined game?? " + this.matchI); + } + return this.games[this.gameI]; + } + + wins(uptoMatchI?: number) { + if (uptoMatchI === undefined) uptoMatchI = this.matchI; + const team1 = this.current().team1; + const team2 = this.current().team2; + const wins = {}; + wins[team1] = 0; + wins[team2] = 0; + for (let matchI = 0; matchI <= uptoMatchI; matchI++) { + const match = this.games[this.gameI][matchI]; + wins[match.winner == 1 ? match.team1 : match.team2]++; + } + return wins; + } + + totalWins() { + return this.wins(this.games[this.gameI].length - 1); + } +} + +export interface TournamentMatch { + team1: string, + team2: string, + map: string, + winner: number, + url: string +} \ No newline at end of file diff --git a/client/visualizer/src/main/websocket.ts b/client/visualizer/src/main/websocket.ts index 6b9774eb..96144918 100644 --- a/client/visualizer/src/main/websocket.ts +++ b/client/visualizer/src/main/websocket.ts @@ -17,7 +17,7 @@ export default class WebSocketListener { onFirstMatch: () => void; onOtherMatch: () => void; - constructor(url: string, pollEvery: number) { + constructor(url: string, pollEvery: number, readonly conf: Config) { this.url = url; this.pollEvery = pollEvery; this.firstMatch = true; @@ -61,7 +61,7 @@ export default class WebSocketListener { console.error("Skipping end of game from websocket."); } - this.currentGame = new Game(); + this.currentGame = new Game(this.conf); this.onGameReceived(this.currentGame); this.firstMatch = true; this.currentGame.applyEvent(event); diff --git a/client/visualizer/src/mapeditor/action/generator.ts b/client/visualizer/src/mapeditor/action/generator.ts index f2659cb6..e42badef 100644 --- a/client/visualizer/src/mapeditor/action/generator.ts +++ b/client/visualizer/src/mapeditor/action/generator.ts @@ -1,19 +1,26 @@ import * as cst from '../../constants'; -import {schema, flatbuffers} from 'battlecode-playback'; -import Victor = require('victor'); - -import {MapUnit, GameMap} from '../index'; +import { schema, flatbuffers } from 'battlecode-playback'; +import { MapUnit, GameMap } from '../index'; // Bodies information -export type BodiesSchema = { +type BodiesSchema = { robotIDs: number[], teamIDs: number[], types: schema.BodyType[], xs: number[], - ys: number[] + ys: number[], + influences: number[] }; +export type UploadedMap = { + name: string, + width: number, + height: number, + passability: number[], + bodies: MapUnit[] +} + /** * Generates a .map21 file from a GameMap. Assumes the given GameMap represents * a valid game map. @@ -55,26 +62,28 @@ export default class MapGenerator { /** * Adds a robot body to the internal array */ - private static addBody(robotID: number, teamID: number, type: schema.BodyType, x: number, y: number) { - this.bodiesArray.robotIDs.push(robotID); + private static addBody(robotID: number, teamID: number, type: schema.BodyType, x: number, y: number, influence: number) { + this.bodiesArray.robotIDs.push(robotID); // ignored by engine this.bodiesArray.teamIDs.push(teamID); this.bodiesArray.types.push(type); this.bodiesArray.xs.push(x); this.bodiesArray.ys.push(y); + this.bodiesArray.influences.push(influence); } /** * Adds multiple bodies to the internal array with the given teamID. */ - private static addBodies(bodies: Map, minCorner: Victor) { + private static addBodies(bodies: Map, minCornerX, minCornerY) { bodies.forEach((unit: MapUnit, id: number) => { this.addBody( id, unit.teamID || 0, // Must be set if not a neutral tree unit.type, - unit.loc.x + minCorner.x, - unit.loc.y + minCorner.y + unit.x, + unit.y, + unit.influence ); }); } @@ -91,38 +100,45 @@ export default class MapGenerator { teamIDs: [], types: [], xs: [], - ys: [] + ys: [], + influences: [] }; // Get header information from form let name: string = map.name; - let minCorner: Victor = new Victor(Math.random()*500, Math.random()*500); - let maxCorner: Victor = minCorner.clone(); - maxCorner.add(new Victor(map.width, map.height)); - let randomSeed: number = Math.round(Math.random()*1000); + const minCornerX = Math.random() * 20000 + 10000; + const minCornerY = Math.random() * 20000 + 10000; + let maxCornerX = minCornerX + map.width; + let maxCornerY = minCornerY + map.height; + let randomSeed: number = Math.round(Math.random() * 1000); // Get body information from form and convert to arrays - this.addBodies(this.combineBodies(map.originalBodies, map.symmetricBodies), minCorner); + this.addBodies(this.combineBodies(map.originalBodies, map.symmetricBodies), minCornerX, minCornerY); // Create the spawned bodies table let robotIDsVectorB = schema.SpawnedBodyTable.createRobotIDsVector(builder, this.bodiesArray.robotIDs); let teamIDsVectorB = schema.SpawnedBodyTable.createTeamIDsVector(builder, this.bodiesArray.teamIDs); - let typesVectorB = schema.SpawnedBodyTable.createTypesVector(builder, this.bodiesArray.types) + let typesVectorB = schema.SpawnedBodyTable.createTypesVector(builder, this.bodiesArray.types); let locsVecTableB = this.createVecTable(builder, this.bodiesArray.xs, this.bodiesArray.ys); + let influencesVectorB = schema.SpawnedBodyTable.createInfluencesVector(builder, this.bodiesArray.influences); schema.SpawnedBodyTable.startSpawnedBodyTable(builder) schema.SpawnedBodyTable.addRobotIDs(builder, robotIDsVectorB); schema.SpawnedBodyTable.addTeamIDs(builder, teamIDsVectorB); schema.SpawnedBodyTable.addTypes(builder, typesVectorB); schema.SpawnedBodyTable.addLocs(builder, locsVecTableB); + schema.SpawnedBodyTable.addInfluences(builder, influencesVectorB); const bodies = schema.SpawnedBodyTable.endSpawnedBodyTable(builder); + const passability = schema.GameMap.createPassabilityVector(builder, map.passability); + // Create the game map let nameP = builder.createString(name); schema.GameMap.startGameMap(builder); schema.GameMap.addName(builder, nameP); - schema.GameMap.addMinCorner(builder, schema.Vec.createVec(builder, minCorner.x, minCorner.y)); - schema.GameMap.addMaxCorner(builder, schema.Vec.createVec(builder, maxCorner.x, maxCorner.y)); + schema.GameMap.addMinCorner(builder, schema.Vec.createVec(builder, minCornerX, minCornerY)); + schema.GameMap.addMaxCorner(builder, schema.Vec.createVec(builder, maxCornerX, maxCornerY)); schema.GameMap.addBodies(builder, bodies); + schema.GameMap.addPassability(builder, passability); schema.GameMap.addRandomSeed(builder, randomSeed); const gameMap = schema.GameMap.endGameMap(builder); @@ -150,9 +166,47 @@ export default class MapGenerator { link.click(); link.remove(); - setTimeout(function() { + setTimeout(function () { return window.URL.revokeObjectURL(url); }, 30000); } } + + /** + * Reads a .map21 file. + */ + static readMap(file: ArrayBuffer): UploadedMap { + const data = new Uint8Array(file); + const map = schema.GameMap.getRootAsGameMap( + new flatbuffers.ByteBuffer(data) + ); + const minCorner = map.minCorner()!; + const maxCorner = map.maxCorner()!; + + const bodies = map.bodies()!; + const influences = bodies.influencesArray()!; + const types = bodies.typesArray()!; + const teamIDs = bodies.teamIDsArray()!; + const xs = bodies.locs()!.xsArray()!; + const ys = bodies.locs()!.ysArray()!; + + const mapUnits: MapUnit[] = []; + for (let i = 0; i < bodies.robotIDsLength(); i++) { + mapUnits.push({ + x: xs[i], + y: ys[i], + type: types[i], + teamID: teamIDs[i], + influence: influences[i], + radius: 0.5 + }); + } + return { + name: map.name()!, + width: maxCorner.x() - minCorner.x(), + height: maxCorner.y() - minCorner.y(), + passability: Array.from(map.passabilityArray()!), + bodies: mapUnits + }; + } } \ No newline at end of file diff --git a/client/visualizer/src/mapeditor/action/renderer.ts b/client/visualizer/src/mapeditor/action/renderer.ts index fc5739f9..62690b10 100644 --- a/client/visualizer/src/mapeditor/action/renderer.ts +++ b/client/visualizer/src/mapeditor/action/renderer.ts @@ -3,22 +3,8 @@ import * as cst from '../../constants'; import {GameWorld, schema} from 'battlecode-playback'; import {AllImages} from '../../imageloader'; -import Victor = require('victor'); -import {GameMap} from '../index'; - -export type MapUnit = { - loc: Victor, - radius: number, - type: schema.BodyType, - teamID?: number -}; - -export enum Symmetry { - ROTATIONAL, - HORIZONTAL, - VERTICAL -}; +import {GameMap, MapUnit} from '../index'; /** * Renders the world. @@ -34,20 +20,28 @@ export default class MapRenderer { // Callbacks for clicking robots and trees on the canvas readonly onclickUnit: (id: number) => void; - readonly onclickBlank: (loc: Victor) => void; + readonly onclickBlank: (x, y) => void; + readonly onMouseover: (x: number, y: number, passability: number) => void + readonly onDrag: (x, y) => void // Other useful values readonly bgPattern: CanvasPattern; private width: number; // in world units private height: number; // in world units + private map: GameMap; //the current map + constructor(canvas: HTMLCanvasElement, imgs: AllImages, conf: config.Config, - onclickUnit: (id: number) => void, onclickBlank: (loc: Victor) => void) { + onclickUnit: (id: number) => void, onclickBlank: (x: number, y: number) => void, + onMouseover: (x: number, y: number, passability: number) => void, + onDrag: (x: number, y: number) => void) { this.canvas = canvas; this.conf = conf; this.imgs = imgs; this.onclickUnit = onclickUnit; this.onclickBlank = onclickBlank; + this.onMouseover = onMouseover; + this.onDrag = onDrag; let ctx = canvas.getContext("2d"); if (ctx === null) { @@ -56,27 +50,29 @@ export default class MapRenderer { this.ctx = ctx; } - this.bgPattern = this.ctx.createPattern(imgs.tiles[0], 'repeat'); + //this.bgPattern = this.ctx.createPattern(imgs.tiles[0], 'repeat'); + this.setEventListeners(); } /** * Renders the game map. */ render(map: GameMap): void { + console.log("map:", map); const scale = this.canvas.width / map.width; this.width = map.width; this.height = map.height; + this.map = map; // setup correct rendering + this.ctx.restore(); this.ctx.save(); this.ctx.scale(scale, scale); - this.renderBackground(); + this.renderBackground(map); this.renderBodies(map); // restore default rendering - this.setEventListener(map); - this.ctx.restore(); } /** @@ -92,18 +88,25 @@ export default class MapRenderer { /** * Draw the background */ - private renderBackground(): void { - this.ctx.save(); - this.ctx.fillStyle = this.bgPattern; - - const scale = 20; - this.ctx.scale(1/scale, 1/scale); - + private renderBackground(map: GameMap): void { for(let i = 0; i < this.width; i++){ for(let j = 0; j < this.height; j++){ - this.ctx.drawImage(this.imgs.tiles[0], i*scale, j*scale, scale, scale); + const passability = map.passability[(map.height-j-1)*this.width + i]; + this.renderTile(i, j, passability); } } + } + + private renderTile(i: number, j: number, passability: number) { + this.ctx.save(); + const scale = 20; + this.ctx.scale(1/scale, 1/scale); + const swampLevel = cst.getLevel(passability); + const tileImg = this.imgs.tiles[swampLevel]; + this.ctx.drawImage(tileImg, i*scale, j*scale, scale, scale); + this.ctx.strokeStyle = 'gray'; + this.ctx.globalAlpha = 1; + this.ctx.strokeRect(i*scale, j*scale, scale, scale); this.ctx.restore(); } @@ -114,51 +117,54 @@ export default class MapRenderer { this.ctx.fillStyle = "#84bf4b"; map.originalBodies.forEach((body: MapUnit) => { - const x = body.loc.x; - const y = this.flip(body.loc.y, map.height); - const radius = body.radius; - const type = body.type; - let img: HTMLImageElement; - - this.drawCircleBot(x, y, radius); - const teamID = body.teamID || 0; - img = this.imgs.robots[cst.bodyTypeToString(body.type)][teamID]; - this.drawImage(img, x, y, radius); + this.renderBody(body); // this.drawGoodies(x, y, radius, body.containedBullets, body.containedBody); }); map.symmetricBodies.forEach((body: MapUnit) => { - const x = body.loc.x; - const y = this.flip(body.loc.y, map.height); - const radius = body.radius; - let img: HTMLImageElement; - - this.drawCircleBot(x, y, radius); - img = this.imgs.robots[cst.bodyTypeToString(body.type)][2]; - this.drawImage(img, x, y, radius); + this.renderBody(body); // this.drawGoodies(x, y, radius, body.containedBullets, body.containedBody); }); } + private renderBody(body: MapUnit) { + const x = body.x; + const y = this.flip(body.y, this.map.height); + const radius = body.radius; + let img: HTMLImageElement; + + const teamID = body.teamID || 0; + img = this.imgs.robots[cst.bodyTypeToString(body.type)][teamID]; + this.drawImage(img, x, y, radius); + } + /** * Sets the map editor display to contain of the information of the selected * tree, or on the selected coordinate if there is no tree. */ - private setEventListener(map: GameMap) { - this.canvas.onmousedown = (event: MouseEvent) => { - let x = map.width * event.offsetX / this.canvas.offsetWidth; - let y = this.flip(map.height * event.offsetY / this.canvas.offsetHeight, map.height); - let loc = new Victor(Math.floor(x), Math.floor(y)); + private setEventListeners() { + + let hoverPos: {x: number, y: number} | null = null; + + const whilemousedown = () => { + if (hoverPos !== null) { + const {x,y} = hoverPos; + this.onDrag(x, y); + } + }; + var interval: number; + this.canvas.onmousedown = (event: MouseEvent) => { + const {x,y} = this.getIntegerLocation(event, this.map); // Get the ID of the selected unit let selectedID; - map.originalBodies.forEach(function(body: MapUnit, id: number) { - if (loc.isEqualTo(body.loc)) { + this.map.originalBodies.forEach(function(body: MapUnit, id: number) { + if (x == body.x && y == body.y) { selectedID = id; } }); - map.symmetricBodies.forEach(function(body: MapUnit, id: number) { - if (loc.isEqualTo(body.loc)) { + this.map.symmetricBodies.forEach(function(body: MapUnit, id: number) { + if (x == body.x && y == body.y) { selectedID = id; } }); @@ -166,21 +172,32 @@ export default class MapRenderer { if (selectedID) { this.onclickUnit(selectedID); } else { - this.onclickBlank(loc); + this.onclickBlank(x, y); } + + interval = window.setInterval(whilemousedown, 50); }; - } - /** - * Draws a circle centered at (x, y) with the given radius - */ - private drawCircleBot(x: number, y: number, radius: number) { - if (!this.conf.circleBots) return; // skip if the option is turned off + this.canvas.onmouseup = () => { + clearInterval(interval); + }; + + this.canvas.onmousemove = (event) => { + const {x,y} = this.getIntegerLocation(event, this.map); + this.onMouseover(x, y, this.map.passability[(y)*this.width + x]); + hoverPos = {x: x, y: y}; + }; - this.ctx.beginPath(); - this.ctx.fillStyle = "#ddd"; - this.ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - this.ctx.fill(); + this.canvas.onmouseout = (event) => { + hoverPos = null; + clearInterval(interval); + }; + } + + private getIntegerLocation(event: MouseEvent, map: GameMap) { + let x = map.width * event.offsetX / this.canvas.offsetWidth; + let y = this.flip(map.height * event.offsetY / this.canvas.offsetHeight, map.height); + return {x: Math.floor(x), y: Math.floor(y)}; } /** @@ -191,4 +208,3 @@ export default class MapRenderer { this.ctx.drawImage(img, x, y-radius*2, radius*2, radius*2); } } - diff --git a/client/visualizer/src/mapeditor/action/validator.ts b/client/visualizer/src/mapeditor/action/validator.ts index 89d23fc8..91c35d8f 100644 --- a/client/visualizer/src/mapeditor/action/validator.ts +++ b/client/visualizer/src/mapeditor/action/validator.ts @@ -4,16 +4,15 @@ import {GameMap, MapUnit} from '../index'; /** * Validates a map created by the map editor. If a map is valid, then the map - * editor is ready to generate the .map17 file. + * editor is ready to generate the .map21 file. * * In a valid map: * - No units overlap * - No units are off the map - * - Neutral trees have radius >= the radius of the body they contain * * Additionally, in a valid starting map: - * - There are 1 to 3 archons - * - There are no other units than archons and neutral trees + * - There are 1 to 3 enlightenment centers + * - There are no other units than enlightenment centers */ export default class MapValidator { @@ -36,10 +35,9 @@ export default class MapValidator { // Invariant: bodies in originalBodies don't overlap with each other, and // bodies in symmetricBodies don't overlap with each other map.originalBodies.forEach((unit: MapUnit, id: number) => { - let x = unit.loc.x + 0.5; - let y = unit.loc.y + 0.5; - let distanceToWall = Math.min(x, y, map.width - x, map.height - y); - if (unit.radius > distanceToWall || x < 0 || y < 0 || x > map.width || y > map.height) { + let x = unit.x; + let y = unit.y; + if (x < 0 || y < 0 || x > map.width || y > map.height) { errors.push(`ID ${id} is off the map.`); } }); @@ -47,23 +45,12 @@ export default class MapValidator { // Bodies must not overlap map.originalBodies.forEach((unitA: MapUnit, idA: number) => { map.symmetricBodies.forEach((unitB: MapUnit, idB: number) => { - if (unitA.loc.distance(unitB.loc) < unitA.radius + unitB.radius) { + if (unitA.x === unitB.x && unitA.y === unitB.y) { errors.push (`IDs ${idA} and ${idB} are overlapping.`); } }); }); - // Neutral trees cannot have a smaller radius than the body they contain - // map.originalBodies.forEach((unit: MapUnit, id: number) => { - // if (unit.type === cst.TREE_NEUTRAL) { - // const treeRadius = unit.radius; - // const bodyRadius = cst.radiusFromBodyType(unit.containedBody); - // if (treeRadius < bodyRadius) { - // errors.push(`Tree ID ${id} with radius ${treeRadius.toFixed(2)} contains a body with radius ${bodyRadius}`); - // } - // } - // }); - if (errors.length > 0) { alert(errors.join("\n")); return false; @@ -87,8 +74,8 @@ export default class MapValidator { // Remove bodies that are off the map map.originalBodies.forEach((unit: MapUnit, id: number) => { - let x = unit.loc.x; - let y = unit.loc.y; + let x = unit.x; + let y = unit.y; let distanceToWall = Math.min(x, y, map.width - x, map.height - y); if (unit.radius > distanceToWall || x < 0 || y < 0 || x > map.width || y > map.height) { map.originalBodies.delete(id); @@ -101,7 +88,9 @@ export default class MapValidator { // bodies in symmetricBodies don't overlap with each other map.originalBodies.forEach((unitA: MapUnit, idA: number) => { map.symmetricBodies.forEach((unitB: MapUnit, idB: number) => { - if (unitA.loc.distance(unitB.loc) <= unitA.radius + unitB.radius) { + // no radii this year + // if (unitA.loc.distance(unitB.loc) <= unitA.radius + unitB.radius) { + if (unitA.x === unitB.x && unitA.y === unitB.y) { map.originalBodies.delete(idA); map.originalBodies.delete(idB); actions.push (`Removed IDs ${idA} and ${idB}. (overlapping)`); diff --git a/client/visualizer/src/mapeditor/form.ts b/client/visualizer/src/mapeditor/form.ts index 107ec5a6..867a81a0 100644 --- a/client/visualizer/src/mapeditor/form.ts +++ b/client/visualizer/src/mapeditor/form.ts @@ -1,18 +1,30 @@ import {Config} from '../config'; import * as cst from '../constants'; import {AllImages} from '../imageloader'; +import {cow_border as cow} from '../cow'; import {schema, flatbuffers} from 'battlecode-playback'; -import Victor = require('victor'); -import {MapRenderer, Symmetry, MapUnit, HeaderForm, SymmetryForm, RobotForm} from './index'; +import {MapRenderer, HeaderForm, SymmetryForm, RobotForm, TileForm, UploadedMap} from './index'; +import { SSL_OP_NO_QUERY_MTU } from 'constants'; + +export type MapUnit = { + x: number, + y: number, + type: schema.BodyType, + radius: 0.5, + teamID?: number, + influence: number +}; export type GameMap = { name: string, width: number, height: number, originalBodies: Map - symmetricBodies: Map + symmetricBodies: Map, + passability: number[], + symmetry: number }; /** @@ -28,10 +40,11 @@ export default class MapEditorForm { private readonly canvas: HTMLCanvasElement; private readonly renderer: MapRenderer; - // Forms + // Forms and text display private readonly header: HeaderForm; private readonly symmetry: SymmetryForm; private readonly robots: RobotForm; + private readonly tiles: TileForm; private robotsRadio: HTMLInputElement; private tilesRadio: HTMLInputElement; @@ -40,6 +53,11 @@ export default class MapEditorForm { readonly buttonAdd: HTMLButtonElement; readonly buttonDelete: HTMLButtonElement; + readonly buttonReverse: HTMLButtonElement; + readonly buttonRandomize: HTMLButtonElement; + readonly buttonInvert: HTMLButtonElement; + + readonly tileInfo: HTMLDivElement; // Options private readonly conf: Config @@ -48,6 +66,11 @@ export default class MapEditorForm { private lastID: number; // To give bodies unique IDs private originalBodies: Map; private symmetricBodies: Map; + private passability: number[]; + + randomMode: boolean = false; // if true, all squares are randomly painted. + randomHigh: number = 1; + randomLow: number = 0.1; constructor(conf: Config, imgs: AllImages, canvas: HTMLCanvasElement) { // Store the parameters @@ -67,49 +90,98 @@ export default class MapEditorForm { // callback functions for getting constants const cbWidth = () => {return this.header.getWidth()}; const cbHeight = () => {return this.header.getHeight()}; - const cbMaxRadius = (x, y, id) => {return this.maxRadius(x, y, id)}; // header (name, width, height) - this.header = new HeaderForm(() => {this.render()}); + this.header = new HeaderForm(() => { + this.reset(); + this.render(); + }); this.div.appendChild(this.header.div); - // TODO symmetry - this.symmetry = new SymmetryForm(() => {this.render()}); - // this.div.appendChild(this.symmetry.div); + // symmetry + this.symmetry = new SymmetryForm(() => {this.initPassibility(); this.render()}); + this.div.appendChild(document.createElement("br")); + this.div.appendChild(this.symmetry.div); + this.div.appendChild(document.createElement("br")); + this.div.appendChild(document.createElement("hr")); // radio buttons this.tilesRadio = document.createElement("input"); this.robotsRadio = document.createElement("input"); this.div.appendChild(this.createUnitOption()); - + this.div.appendChild(document.createElement("br")); // robot delete + add/update buttons this.forms = document.createElement("div"); - this.robots = new RobotForm(cbWidth, cbHeight, cbMaxRadius); // robot info (type, x, y, ...) + this.robots = new RobotForm(cbWidth, cbHeight); // robot info (type, x, y, ...) + this.tiles = new TileForm(cbWidth, cbHeight); this.buttonDelete = document.createElement("button"); this.buttonAdd = document.createElement("button"); + this.buttonReverse = document.createElement("button"); + this.buttonRandomize = document.createElement("button"); + this.buttonInvert = document.createElement("button"); this.div.appendChild(this.forms); + this.buttonDelete.style.display = "none"; + this.buttonAdd.style.display = "none"; + this.buttonReverse.style.display = "none"; + this.buttonRandomize.style.display = "none"; + this.buttonInvert.style.display = "none"; + // TODO add vertical filler to put form buttons at the bottom // validate, remove, reset buttons this.div.appendChild(this.createFormButtons()); + this.div.appendChild(document.createElement('hr')); + this.tileInfo = document.createElement("div"); + this.tileInfo.textContent = "X: | Y: | Passability:"; + this.div.appendChild(this.tileInfo); + this.div.appendChild(document.createElement('hr')); // Renderer settings const onclickUnit = (id: number) => { - if (this.originalBodies.has(id)) { + if (this.originalBodies.has(id) && this.getActiveForm() == this.robots) { // Set the corresponding form appropriately let body: MapUnit = this.originalBodies.get(id)!; this.robotsRadio.click(); - this.getActiveForm().setForm(body.loc, body, id); + this.robots.setForm(body.x, body.y, body, id); } }; - const onclickBlank = (loc: Victor) => { - this.getActiveForm().setForm(loc); + const onclickBlank = (x, y) => { + this.getActiveForm().setForm(x, y); + }; + + const onMouseover = (x: number, y: number, passability: number) => { + let content: string = ""; + content += 'X: ' + `${x}`.padStart(3); + content += ' | Y: ' + `${y}`.padStart(3); + content += ' | Passability: ' + `${passability.toFixed(3)}`; + this.tileInfo.textContent = content; + }; + + const onDrag = (x, y) => { + if (this.getActiveForm() === this.tiles && this.tiles.isValid()) { + let r: number = this.tiles.getBrush(); + let inBrush: (dx, dy) => boolean = () => true; + switch (this.tiles.getStyle()) { + case "Circle": + inBrush = (dx, dy) => dx*dx + dy*dy < r*r; + break; + case "Square": + inBrush = (dx, dy) => Math.max(Math.abs(dx), Math.abs(dy)) < r; + break; + case "Cow": + inBrush = (dx,dy) => (Math.abs(dx) < r && Math.abs(dy) < r && cow[Math.floor(20*(1+dx/r))][Math.floor(20*(1-dy/r))]); + } + this.setAreaPassability(x, y, this.tiles.getPass(), inBrush); + this.render(); + } } - this.renderer = new MapRenderer(canvas, imgs, conf, onclickUnit, onclickBlank); + this.renderer = new MapRenderer(canvas, imgs, conf, onclickUnit, onclickBlank, onMouseover, onDrag); + + this.initPassibility(); // Load callbacks and finally render this.loadCallbacks(); @@ -123,32 +195,45 @@ export default class MapEditorForm { this.tilesRadio.id = "tiles-radio"; this.tilesRadio.type = "radio"; this.tilesRadio.name = "edit-option"; // radio buttons with same name are mutually exclusive + this.tilesRadio.onchange = () => { - while (this.forms.firstChild) this.forms.removeChild(this.forms.firstChild); + // Change the displayed form + if (this.tilesRadio.checked) { + while (this.forms.firstChild) this.forms.removeChild(this.forms.firstChild); + this.forms.appendChild(this.tiles.div); + this.buttonDelete.style.display = "none"; + this.buttonAdd.style.display = "none"; + this.buttonReverse.style.display = "none"; + this.buttonRandomize.style.display = ""; + this.buttonInvert.style.display = ""; + } }; const tilesLabel = document.createElement("label"); tilesLabel.setAttribute("for", this.tilesRadio.id); tilesLabel.textContent = "Change Tiles"; - this.tilesRadio.disabled = true; // Radio button for placing units - this.robotsRadio.id = "robots-radio"; + this.robotsRadio.id = "robots-radio"; this.robotsRadio.type = "radio"; this.robotsRadio.name = "edit-option"; this.robotsRadio.onchange = () => { // Change the displayed form - while (this.forms.firstChild) this.forms.removeChild(this.forms.firstChild); if (this.robotsRadio.checked) { + while (this.forms.firstChild) this.forms.removeChild(this.forms.firstChild); this.forms.appendChild(this.robots.div); + this.buttonDelete.style.display = ""; + this.buttonAdd.style.display = ""; + this.buttonReverse.style.display = ""; + this.buttonRandomize.style.display = "none"; + this.buttonInvert.style.display = "none"; } }; const robotsLabel = document.createElement("label"); robotsLabel.setAttribute("for", this.robotsRadio.id); robotsLabel.textContent = "Place Robots"; - // Add radio buttons HTML element div.appendChild(this.tilesRadio); div.appendChild(tilesLabel); @@ -164,38 +249,116 @@ export default class MapEditorForm { const buttons = document.createElement("div"); buttons.appendChild(this.buttonDelete); buttons.appendChild(this.buttonAdd); + buttons.appendChild(this.buttonReverse); + buttons.appendChild(this.buttonRandomize); + buttons.appendChild(this.buttonInvert); // Delete and Add/Update buttons this.buttonDelete.type = "button"; - this.buttonDelete.className = "form-button"; + this.buttonDelete.className = "form-button custom-button"; this.buttonDelete.appendChild(document.createTextNode("Delete")); this.buttonAdd.type = "button"; - this.buttonAdd.className = "form-button"; + this.buttonAdd.className = "form-button custom-button"; this.buttonAdd.appendChild(document.createTextNode("Add/Update")); + this.buttonReverse.type = "button"; + this.buttonReverse.className = "form-button custom-button"; + this.buttonReverse.appendChild(document.createTextNode("Switch Team")); + this.buttonRandomize.type = "button"; + this.buttonRandomize.className = "form-button custom-button"; + this.buttonRandomize.appendChild(document.createTextNode("Randomize Tiles")); + this.buttonInvert.type = "button"; + this.buttonInvert.className = "form-button custom-button"; + this.buttonInvert.appendChild(document.createTextNode("Invert values")); return buttons; } private loadCallbacks() { this.buttonAdd.onclick = () => { - const form: RobotForm = this.getActiveForm() - const id: number = form.getID() || this.lastID; - const unit: MapUnit | undefined = form.getUnit(id); - - if (unit) { - // Create a new unit or update an existing unit - this.setUnit(id, unit); - form.resetForm(); + if (this.getActiveForm() == this.robots) { + const form: RobotForm = this.robots; + const id: number = form.getID() || this.lastID; + const unit: MapUnit | undefined = form.getUnit(id); + if (unit) { + // Create a new unit or update an existing unit + this.setUnit(id, unit); + form.resetForm(); + } } } this.buttonDelete.onclick = () => { - const id: number | undefined = this.getActiveForm().getID(); - if (id && !isNaN(id)) { - this.deleteUnit(id); - this.getActiveForm().resetForm(); + if (this.getActiveForm() == this.robots) { + const id: number | undefined = this.robots.getID(); + if (id && !isNaN(id)) { + this.deleteUnit(id); + this.getActiveForm().resetForm(); + } + } + } + + this.buttonReverse.onclick = () => { + if (this.getActiveForm() == this.robots) { + const form: RobotForm = this.robots; + const id: number = form.getID() || this.lastID - 1; + const unit: MapUnit = this.originalBodies.get(id)!; + if (unit) { + var teamID: number = unit.teamID === undefined? 0 : unit.teamID; + if(teamID > 0) { + teamID = 3 - teamID; + } + unit.teamID = teamID; + // Create a new unit or update an existing unit + this.setUnit(id, unit); + form.resetForm(); + } + } + } + + this.buttonRandomize.onclick = () => { + if (this.getActiveForm() == this.tiles) { + for(let x: number = 0; x < this.header.getWidth(); x++) { + for(let y:number = 0; y < this.header.getHeight(); y++) { + this.setPassability(x, y, Math.random() * 0.9 + 0.1); + } + } + this.render(); + } + } + + this.buttonInvert.onclick = () => { + if (this.getActiveForm() == this.tiles) { + for(let x: number = 0; x < this.header.getWidth(); x++) { + for(let y: number = 0; y < this.header.getHeight(); y++) { + this.passability[y*this.header.getWidth() + x] = 1.1 - this.getPassability(x,y); + } + } + this.render(); } } + + // this.buttonSmoothen.onclick = () => { + // if (this.getActiveForm() == this.tiles) { + // for(let x: number = 0; x < this.header.getWidth(); x++) { + // for(let y: number = 0; y < this.header.getHeight(); y++) { + // //let sum = 0, n = 0; + // let high = this.getPassability(x, y); + // let low = this.getPassability(x, y); + // for (let x2 = Math.max(0,x-1); x2 <= Math.min(x+1, this.header.getWidth()-1); x2++) { + // for (let y2 = Math.max(0,y-1); y2 <= Math.min(y+1, this.header.getWidth()-1); y2++) { + // // if (Math.abs(x-x2) + Math.abs(y-y2) > 1) continue; // bad code + // // sum += this.getPassability(x2, y2); + // //n++; + // high = Math.max(this.getPassability(x2, y2), high); + // low = Math.min(this.getPassability(x2, y2), high); + // } + // } + // this.setPassability(x,y, (high+low)/2); + // } + // } + // this.render(); + // } + // } } /** @@ -206,26 +369,25 @@ export default class MapEditorForm { * If an id is given, does not consider the body with the corresponding id to * overlap with the given coordinates. */ - private maxRadius(x: number, y: number, ignoreID?: number): number { - // Min distance to wall - let maxRadius = Math.min(x, y, this.header.getWidth()-x, this.header.getHeight()-y); - const loc = new Victor(x, y); - - // Min distance to tree or body - ignoreID = ignoreID || -1; - this.originalBodies.forEach((body: MapUnit, id: number) => { - if (id != ignoreID) { - maxRadius = Math.min(maxRadius, loc.distance(body.loc) - body.radius); - } - }); - this.symmetricBodies.forEach((body: MapUnit, id: number) => { - if (id != ignoreID) { - maxRadius = Math.min(maxRadius, loc.distance(body.loc) - body.radius); - } - }); - - return Math.max(0, maxRadius - cst.DELTA); - } + // private maxRadius(x: number, y: number, ignoreID?: number): number { + // // Min distance to wall + // let maxRadius = Math.min(x, y, this.header.getWidth()-x, this.header.getHeight()-y); + + // // Min distance to tree or body + // ignoreID = ignoreID || -1; + // this.originalBodies.forEach((body: MapUnit, id: number) => { + // if (id != ignoreID) { + // maxRadius = Math.min(maxRadius, loc.distance(body.loc) - body.radius); + // } + // }); + // this.symmetricBodies.forEach((body: MapUnit, id: number) => { + // if (id != ignoreID) { + // maxRadius = Math.min(maxRadius, loc.distance(body.loc) - body.radius); + // } + // }); + + // return Math.max(0, maxRadius - cst.DELTA); + // } /** * If a unit with the given ID already exists, updates the existing unit. @@ -251,20 +413,62 @@ export default class MapEditorForm { } } + /** + * Initialize passability array based on map dimensions. + */ + private initPassibility() { + this.passability = new Array(this.header.getHeight() * this.header.getWidth()); + this.passability.fill(1); + } + + private getPassability(x: number, y: number) { + return this.passability[y*this.header.getWidth() + x]; + } + + private setPassability(x: number, y: number, pass: number) { + if (this.randomMode) pass = this.randomLow + (this.randomHigh - this.randomLow) * Math.random(); + const {x: translated_x, y: translated_y} = this.symmetry.transformLoc(x, y, this.header.getWidth(), this.header.getHeight()); + this.passability[y*this.header.getWidth() + x] = this.passability[translated_y*this.header.getWidth() + translated_x] = pass; + } + + private setAreaPassability(x0: number, y0: number, pass: number, inBrush: (dx, dy) => boolean) { + const width = this.header.getWidth(); + const height = this.header.getHeight(); + + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + if (inBrush(x-x0, y-y0)) { + this.setPassability(x, y, pass); + } + } + } + } + + /** + * Set passability of all tiles from top-left to bottom-right. + */ + private setRectPassability(x1: number, y1: number, x2: number, y2: number, pass: number) { + for (let x = x1; x <= x2; x++) { + for (let y = y1; y <= y2; y++) { + this.setPassability(x, y, pass); + } + } + } + /** * @return the active form based on which radio button is selected */ - private getActiveForm(): RobotForm { - return this.robots; + private getActiveForm(): RobotForm | TileForm { + return (this.tilesRadio.checked ? this.tiles : this.robots) } /** * Re-renders the canvas based on the parameters of the map editor. */ render() { - const scale: number = 50; // arbitrary scaling factor const width: number = this.header.getWidth(); const height: number = this.header.getHeight(); + const scale: number = this.conf.upscale / Math.sqrt(width * height); // arbitrary scaling factor this.canvas.width = width * scale; this.canvas.height = height * scale; this.symmetricBodies = this.symmetry.getSymmetricBodies(this.originalBodies, width, height); @@ -280,14 +484,77 @@ export default class MapEditorForm { width: this.header.getWidth(), height: this.header.getHeight(), originalBodies: this.originalBodies, - symmetricBodies: this.symmetricBodies + symmetricBodies: this.symmetricBodies, + passability: this.passability, + symmetry: this.symmetry.getSymmetry() }; } + // getMapJSON(): string { + // // from https://stackoverflow.com/questions/29085197/how-do-you-json-stringify-an-es6-map/56150320 + // const map = this.getMap(); + // function replacer(key, value) { + // const originalObject = this[key]; + // if(originalObject instanceof Map) { + // return { + // dataType: 'Map', + // value: Array.from(originalObject.entries()), // or with spread: value: [...originalObject] + // }; + // } else { + // return value; + // } + // } + // return JSON.stringify(map, replacer); + // } + + // setMap(mapJSON) { + // // from https://stackoverflow.com/questions/29085197/how-do-you-json-stringify-an-es6-map/56150320 + // function reviver(key, value) { + // if(typeof value === 'object' && value !== null) { + // if (value.dataType === 'Map') { + // return new Map(value.value); + // } + // } + // return value; + // } + // const map = JSON.parse(mapJSON, reviver); + // this.header.setName(map.name); + // this.header.setWidth(map.width); + // this.header.setHeight(map.height); + + // this.originalBodies = map.originalBodies; + // this.symmetricBodies = map.symmetricBodies; + // this.symmetry.setSymmetry(map.symmetry); + // this.passability = map.passability; + // this.render(); + // } + + // TODO: types + setUploadedMap(map: UploadedMap) { + + const symmetryAndBodies = this.symmetry.discoverSymmetryAndBodies(map.bodies, map.passability, map.width, map.height); + console.log(symmetryAndBodies); + if (symmetryAndBodies === null) return; + + this.reset(); + this.header.setName(map.name); + this.header.setWidth(map.width); + this.header.setHeight(map.height); + this.symmetry.setSymmetry(symmetryAndBodies.symmetry); + this.originalBodies = symmetryAndBodies.originalBodies; + this.lastID = this.originalBodies.size + 1; + this.symmetricBodies = this.symmetry.getSymmetricBodies(this.originalBodies, map.width, map.height); + + this.passability = map.passability; + + this.render(); + } + reset(): void { this.lastID = 1; this.originalBodies = new Map(); this.symmetricBodies = new Map(); + this.initPassibility(); this.render(); } } diff --git a/client/visualizer/src/mapeditor/forms/header.ts b/client/visualizer/src/mapeditor/forms/header.ts index 080d53b8..7108d6d8 100644 --- a/client/visualizer/src/mapeditor/forms/header.ts +++ b/client/visualizer/src/mapeditor/forms/header.ts @@ -14,7 +14,7 @@ export default class HeaderForm { private readonly cb: () => void; // Constants - private readonly DEFAULT_DIM: string = "20"; + private readonly DEFAULT_DIM: string = "50"; constructor(cb: () => void) { @@ -57,29 +57,32 @@ export default class HeaderForm { const name: HTMLDivElement = document.createElement("div"); const width: HTMLDivElement = document.createElement("div"); const height: HTMLDivElement = document.createElement("div"); + form.style.textAlign = 'left'; form.appendChild(name); form.appendChild(width); form.appendChild(height); // Map name + this.name.style.width = "200px"; let nameLabel = document.createElement("label"); - nameLabel.innerHTML = "Map name: "; + nameLabel.innerHTML = "Name:"; name.appendChild(nameLabel); name.appendChild(this.name); // Map width + this.width.style.width = "200px"; let widthLabel = document.createElement("label"); - widthLabel.innerHTML = "Width: "; + widthLabel.innerHTML = "Width:"; width.appendChild(widthLabel); width.appendChild(this.width); // Map height + this.height.style.width = "200px"; let heightLabel = document.createElement("label"); - heightLabel.innerHTML = "Height: "; + heightLabel.innerHTML = "Height:"; height.appendChild(heightLabel); height.appendChild(this.height); - form.appendChild(document.createElement("br")); return form; } @@ -123,10 +126,22 @@ export default class HeaderForm { } getWidth(): number { - return parseFloat(this.width.value); + return parseInt(this.width.value); } getHeight(): number { - return parseFloat(this.height.value); + return parseInt(this.height.value); + } + + setName(name) { + this.name.value = name; + } + + setWidth(width) { + this.width.value = width; + } + + setHeight(height) { + this.height.value = height; } } \ No newline at end of file diff --git a/client/visualizer/src/mapeditor/forms/robots.ts b/client/visualizer/src/mapeditor/forms/robots.ts index db832a48..bc76c71f 100644 --- a/client/visualizer/src/mapeditor/forms/robots.ts +++ b/client/visualizer/src/mapeditor/forms/robots.ts @@ -1,7 +1,6 @@ import * as cst from '../../constants'; import {schema} from 'battlecode-playback'; -import Victor = require('victor'); import {MapUnit} from '../index'; @@ -16,11 +15,11 @@ export default class RobotForm { readonly team: HTMLSelectElement; readonly x: HTMLInputElement; readonly y: HTMLInputElement; + readonly influence: HTMLInputElement; // Callbacks on input change readonly width: () => number; readonly height: () => number; - readonly maxRadius: (x: number, y: number, ignoreID?: number) => number; // Constant private readonly ROBOT_TYPES: schema.BodyType[] = cst.initialBodyTypeList; @@ -31,13 +30,11 @@ export default class RobotForm { "2": "Blue" }; - constructor(width: () => number, height: () => number, - maxRadius: (x: number, y: number, ignoreID?: number) => number) { + constructor(width: () => number, height: () => number) { // Store the callbacks this.width = width; this.height = height; - this.maxRadius = maxRadius; // Create HTML elements this.div = document.createElement("div"); @@ -46,6 +43,9 @@ export default class RobotForm { this.team = document.createElement("select"); this.x = document.createElement("input"); this.y = document.createElement("input"); + this.influence = document.createElement("input"); + this.influence.value = String(cst.INITIAL_INFLUENCE); + this.influence.disabled = true; // Create the form this.loadInputs(); @@ -71,6 +71,9 @@ export default class RobotForm { option.value = String(team); option.appendChild(document.createTextNode(this.TEAMS[team])); this.team.appendChild(option); + if (this.TEAMS[team] === "Red") { + option.selected = true; + } } } @@ -85,33 +88,38 @@ export default class RobotForm { const team: HTMLDivElement = document.createElement("div"); const x: HTMLDivElement = document.createElement("div"); const y: HTMLDivElement = document.createElement("div"); - form.appendChild(id); + const influence: HTMLDivElement = document.createElement("div"); + //form.appendChild(id); form.appendChild(type); form.appendChild(team); form.appendChild(x); form.appendChild(y); + form.appendChild(influence); form.appendChild(document.createElement("br")); - // Robot ID - id.appendChild(document.createTextNode("ID:")); + id.appendChild(document.createTextNode("ID: ")); id.appendChild(this.id); // Robot type - type.appendChild(document.createTextNode("Type:")); + type.appendChild(document.createTextNode("Type: ")); type.appendChild(this.type); // Team - team.appendChild(document.createTextNode("Team:")); + team.appendChild(document.createTextNode("Team: ")); team.appendChild(this.team); // X coordinate - x.appendChild(document.createTextNode("X:")); + x.appendChild(document.createTextNode("X: ")); x.appendChild(this.x); // Y coordinate - y.appendChild(document.createTextNode("Y:")); + y.appendChild(document.createTextNode("Y: ")); y.appendChild(this.y); + // Influence + influence.appendChild(document.createTextNode("I: ")); + influence.appendChild(this.influence); + return form; } @@ -135,6 +143,22 @@ export default class RobotForm { value = Math.min(value, this.height()); this.y.value = isNaN(value) ? "" : String(value); }; + + this.influence.onchange = () => { + let value: number = this.getInfluence(); + value = Math.max(value, 50); + value = Math.min(value, 500); + this.influence.value = isNaN(value) ? "" : String(value); + } + + this.team.onchange = () => { + if (this.getTeam() !== 0) { + this.influence.disabled = true; + this.influence.value = String(cst.INITIAL_INFLUENCE); + } + else this.influence.disabled = false; + } + } @@ -154,6 +178,10 @@ export default class RobotForm { return parseInt(this.y.value); } + private getInfluence(): number { + return parseInt(this.influence.value); + } + getID(): number | undefined { const id = parseInt(this.id.textContent || "NaN"); return isNaN(id) ? undefined : id; @@ -164,33 +192,36 @@ export default class RobotForm { this.y.value = ""; } - setForm(loc: Victor, body?: MapUnit, id?: number): void { - this.x.value = String(loc.x); - this.y.value = String(loc.y); + setForm(x, y, body?: MapUnit, id?: number): void { + this.x.value = String(x); + this.y.value = String(y); this.id.textContent = id === undefined ? "" : String(id); if (body && id) { this.type.value = String(body.type); this.team.value = String(body.teamID); + this.influence.disabled = (this.getTeam() !== 0); + this.influence.value = String(body.influence); } } isValid(): boolean { const x = this.getX(); const y = this.getY(); - - return !(isNaN(x) || isNaN(y)); + const I = this.getInfluence(); + return !(isNaN(x) || isNaN(y) || isNaN(I)); } getUnit(id: number): MapUnit | undefined { if (!this.isValid()) { return undefined; } - return { - loc: new Victor(this.getX(), this.getY()), + x: this.getX(), + y: this.getY(), radius: 0.5, type: this.getType(), - teamID: this.getTeam() + teamID: this.getTeam(), + influence: this.getInfluence() } } } diff --git a/client/visualizer/src/mapeditor/forms/symmetry.ts b/client/visualizer/src/mapeditor/forms/symmetry.ts index b7b63d9a..0041d137 100644 --- a/client/visualizer/src/mapeditor/forms/symmetry.ts +++ b/client/visualizer/src/mapeditor/forms/symmetry.ts @@ -1,7 +1,5 @@ import * as cst from '../../constants'; -import Victor = require('victor'); - import {MapUnit} from '../index'; export enum Symmetry { @@ -9,7 +7,6 @@ export enum Symmetry { HORIZONTAL, VERTICAL }; - export default class SymmetryForm { // The public div @@ -21,12 +18,10 @@ export default class SymmetryForm { // Callback on input change to redraw the canvas private cb: () => void; - // Constants + // Constants. TODO: make these and assosciated methods static private readonly SYMMETRY_OPTIONS: Symmetry[] = [ Symmetry.ROTATIONAL, Symmetry.HORIZONTAL, Symmetry.VERTICAL ]; - private readonly NEUTRAL_TEAM_ID = 0; - private readonly BLUE_TEAM_ID = 2; constructor(cb: () => void) { @@ -60,10 +55,9 @@ export default class SymmetryForm { */ private createForm(): HTMLFormElement { const form = document.createElement("form"); - form.appendChild(document.createTextNode("Symmetry:")); + form.style.textAlign = 'left'; + form.appendChild(document.createTextNode("Symmetry: ")); form.appendChild(this.select); - form.appendChild(document.createElement("br")); - form.appendChild(document.createElement("br")); return form; } @@ -80,24 +74,39 @@ export default class SymmetryForm { /** * The symmetry of the map currently selected */ - private getSymmetry(): Symmetry { + getSymmetry(): Symmetry { return parseInt(this.select.options[this.select.selectedIndex].value); } + setSymmetry(symmetry) { + this.select.value = symmetry; + } + // Whether or not loc lies on the point or line of symmetry - private onSymmetricLine(loc: Victor, width: number, height: number): boolean { + private onSymmetricLine(x, y, width: number, height: number): boolean { + const midX = width / 2 - 0.5; + const midY = height / 2 - 0.5; switch(this.getSymmetry()) { case(Symmetry.ROTATIONAL): - return loc.x === width / 2 && loc.y === height / 2; + return x === midX && y === midY; case(Symmetry.HORIZONTAL): - return loc.y === height / 2; + return y === midY; case(Symmetry.VERTICAL): - return loc.x === width / 2; + return x === midX; } }; + flipTeamID(teamID: number) { + return teamID === 0 ? 0 : 3 - teamID; + } + // Returns the symmetric location on the canvas - private transformLoc (loc: Victor, width: number, height: number): Victor { + transformLoc (x, y, width: number, height: number) { + return this.transformLocStatic(x, y, width, height, this.getSymmetry()); + }; + + // TODO: make this actually static! + transformLocStatic(x, y, width, height, symmetry: Symmetry) { function reflect(x: number, mid: number): number { if (x > mid) { return mid - Math.abs(x - mid); @@ -106,17 +115,17 @@ export default class SymmetryForm { } } - const midX = width / 2; - const midY = height / 2; - switch(this.getSymmetry()) { + const midX = width / 2 - 0.5; + const midY = height / 2 - 0.5; + switch(symmetry) { case(Symmetry.ROTATIONAL): - return new Victor(reflect(loc.x, midX), reflect(loc.y, midY)); + return {x: reflect(x, midX), y: reflect(y, midY)}; case(Symmetry.HORIZONTAL): - return new Victor(loc.x, reflect(loc.y, midY)); + return {x: x, y: reflect(y, midY)}; case(Symmetry.VERTICAL): - return new Victor(reflect(loc.x, midX), loc.y); + return {x: reflect(x, midX), y: y}; } - }; + } /** * Uses the bodies stored internally to create a mapping of original body @@ -128,21 +137,58 @@ export default class SymmetryForm { // no symmetric (neutral) body in 2021 game const symmetricBodies: Map = new Map(); - // bodies.forEach((body: MapUnit, id: number) => { - // if (!this.onSymmetricLine(body.loc, width, height)) { - // const type = body.type; - // const teamID = type === cst.COW ? this.NEUTRAL_TEAM_ID : this.BLUE_TEAM_ID; - // if (type === cst.COW) { - // symmetricBodies.set(id, { - // loc: this.transformLoc(body.loc, width, height), - // radius: body.radius, - // type: type, - // teamID: teamID - // }); - // } - // } - // }); + bodies.forEach((body: MapUnit, id: number) => { + if (!this.onSymmetricLine(body.x, body.y, width, height)) { + const type = body.type; + const teamID = body.teamID === undefined? 0 : body.teamID; + const newLoc = this.transformLoc(body.x, body.y, width, height); + symmetricBodies.set(id, { + x: newLoc.x, + y: newLoc.y, + radius: body.radius, + type: type, + teamID: this.flipTeamID(teamID), + influence: body.influence + }); + } + }); return symmetricBodies; } -} \ No newline at end of file + + /** + * Given a list of units and passability, finds a compatible symmetry. + */ + discoverSymmetryAndBodies(mapUnits: MapUnit[], passability: number[], width: number, height: number): {symmetry: Symmetry, originalBodies: Map} | null { + for (const symmetry of this.SYMMETRY_OPTIONS) { + + var possible: boolean = true; + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + const newLoc = this.transformLocStatic(x, y, width, height, symmetry); + if (passability[y * width + x] !== passability[newLoc.y * width + newLoc.x]) { + possible = false; + } + } + } + + const originalBodies = new Map(); + const matched = new Array(originalBodies.size); + var id = 1; + for (let i = 0; i < mapUnits.length; i++) { + if (matched[i]) continue; + const unit1 = mapUnits[i]; + const newLoc = this.transformLocStatic(unit1.x, unit1.y, width, height, symmetry); + for (let j = i; j < mapUnits.length; j++) { + const unit2 = mapUnits[j]; + if (unit2.x == newLoc.x && unit2.y == newLoc.y) { + originalBodies.set(id++, unit1); + matched[i] = matched[j] = true; + } + } + } + if (possible) return {symmetry: symmetry, originalBodies: originalBodies} + } + return null; + } +} diff --git a/client/visualizer/src/mapeditor/forms/tiles.ts b/client/visualizer/src/mapeditor/forms/tiles.ts new file mode 100644 index 00000000..da8e780c --- /dev/null +++ b/client/visualizer/src/mapeditor/forms/tiles.ts @@ -0,0 +1,124 @@ +import * as cst from '../../constants'; + +import {schema} from 'battlecode-playback'; +import Victor = require('victor'); + +import {MapUnit} from '../index'; + +export default class TileForm { + + // The public div + readonly div: HTMLDivElement; + + // Form elements + readonly pass: HTMLInputElement; + readonly brush: HTMLInputElement; + readonly style: HTMLSelectElement; + + // Callbacks on input change + readonly width: () => number; + readonly height: () => number; + + constructor(width: () => number, height: () => number) { + + // Store the callbacks + this.width = width; + this.height = height; + + // Create HTML elements + this.div = document.createElement("div"); + this.pass = document.createElement("input"); + this.brush = document.createElement("input"); + this.style = document.createElement("select"); + + // Create the form + this.loadInputs(); + this.div.appendChild(this.createForm()); + this.loadCallbacks(); + } + + /** + * Initializes input fields. + */ + private loadInputs(): void { + this.pass.value = "0.5"; + this.brush.value = "3"; + + for (var styleString of ["Circle", "Square", "Cow"]) { + var option = document.createElement("option"); + option.value = styleString; + option.appendChild(document.createTextNode(styleString)); + this.style.appendChild(option); + } + } + + /** + * Creates the HTML form that collects archon information. + */ + private createForm(): HTMLFormElement { + // HTML structure + const form: HTMLFormElement = document.createElement("form"); + form.id = "change-tiles"; + const pass: HTMLDivElement = document.createElement("div"); + const brush: HTMLDivElement = document.createElement("div"); + const style: HTMLDivElement = document.createElement("div"); + + pass.appendChild(document.createTextNode("Passability:")); + pass.appendChild(this.pass); + form.appendChild(pass); + + brush.appendChild(document.createTextNode("Brush size:")); + brush.appendChild(this.brush); + form.appendChild(brush); + + style.appendChild(document.createTextNode("Brush style:")); + style.appendChild(this.style); + form.appendChild(style); + + form.appendChild(document.createElement("br")); + + + return form; + } + + /** + * Add callbacks to the form elements. + */ + private loadCallbacks(): void { + this.pass.onchange = () => { + this.pass.value = !isNaN(this.getPass()) ? this.validate(this.getPass(), 0.1, 1) : ""; + }; + this.brush.onchange = () => { + this.brush.value = !isNaN(this.getBrush()) ? this.validate(this.getBrush(), 1) : ""; + }; + } + + getPass(): number { + return parseFloat(this.pass.value); + } + + getBrush(): number { + return parseFloat(this.brush.value); + } + + getStyle(): String { + return this.style.value; + } + + resetForm(): void { + this.pass.value = ""; + } + + setForm(): void { + } + + private validate (value: number, min: number = 0, max: number = Infinity) { + value = Math.max(value, min); + value = Math.min(value, max); + return isNaN(value) ? "" : String(value); + } + + isValid(): boolean { + return !(isNaN(this.getPass())); + } +} diff --git a/client/visualizer/src/mapeditor/index.ts b/client/visualizer/src/mapeditor/index.ts index 17587b98..17f8090c 100644 --- a/client/visualizer/src/mapeditor/index.ts +++ b/client/visualizer/src/mapeditor/index.ts @@ -1,16 +1,18 @@ +import {MapUnit} from './form'; import MapGenerator from './action/generator'; -import {MapUnit, Symmetry} from './action/renderer'; +import {UploadedMap}from './action/generator'; import MapRenderer from './action/renderer'; import MapValidator from './action/validator'; import HeaderForm from './forms/header'; import RobotForm from './forms/robots'; -import SymmetryForm from './forms/symmetry'; +import SymmetryForm, {Symmetry} from './forms/symmetry'; +import TileForm from './forms/tiles'; import {GameMap} from './form'; import MapEditorForm from './form'; import MapEditor from './mapeditor'; -export {MapGenerator, MapUnit, Symmetry, MapRenderer, MapValidator} -export {HeaderForm, RobotForm, SymmetryForm} -export {GameMap, MapEditorForm, MapEditor}; \ No newline at end of file +export {MapGenerator, MapUnit, MapRenderer, MapValidator, UploadedMap} +export {HeaderForm, RobotForm, Symmetry, SymmetryForm, TileForm} +export {GameMap, MapEditorForm, MapEditor}; diff --git a/client/visualizer/src/mapeditor/mapeditor.ts b/client/visualizer/src/mapeditor/mapeditor.ts index 46442f36..30c858a6 100644 --- a/client/visualizer/src/mapeditor/mapeditor.ts +++ b/client/visualizer/src/mapeditor/mapeditor.ts @@ -2,14 +2,16 @@ import {Config} from '../config'; import * as cst from '../constants'; import {AllImages} from '../imageloader'; import ScaffoldCommunicator from '../main/scaffold'; +import {electron} from '../main/electron-modules'; import {schema, flatbuffers} from 'battlecode-playback'; import Victor = require('victor'); -import {MapUnit, MapValidator, MapGenerator, MapEditorForm, GameMap} from './index'; +import {MapUnit, MapValidator, MapGenerator, MapEditorForm, GameMap, UploadedMap} from './index'; +import { env } from 'process'; /** - * Allows the user to download a .map17 file representing the map generated + * Allows the user to download a .map21 file representing the map generated * in the map editor. */ export default class MapEditor { @@ -35,7 +37,6 @@ export default class MapEditor { this.images = images; this.conf = conf; this.div = this.basediv(); - } private basediv(): HTMLDivElement { @@ -47,26 +48,52 @@ export default class MapEditor { div.appendChild(document.createElement("br")); div.appendChild(this.form.div); - div.appendChild(this.validateButton()); + //div.appendChild(this.validateButton()); // TODO // div.appendChild(this.removeInvalidButton()); div.appendChild(this.resetButton()); - div.appendChild(document.createElement("br")); + //div.appendChild(document.createElement("br")); + // div.appendChild(this.getMapJSONButton()); + // div.appendChild(this.pasteMapJSONButton()); + div.appendChild(this.importMapButton()); div.appendChild(document.createElement("br")); div.appendChild(this.exportButton()); div.appendChild(document.createElement("br")); + div.appendChild(document.createElement("hr")); const helpDiv = document.createElement("div"); helpDiv.style.textAlign = "left"; + div.appendChild(document.createElement("br")); div.appendChild(helpDiv); - // helpDiv.innerHTML = `Help text is not yet written :p`; - // `
Tip: "S"=quick add, "D"=quick delete.

- // Note: In tournaments, a starting map consists only of neutral trees and - // ${cst.MIN_NUMBER_OF_ARCHONS} to ${cst.MAX_NUMBER_OF_ARCHONS} archons per - // team. The validator only checks for overlapping and off-map units.

- // Note: The map editor currently does not support bullet trees.
`; + helpDiv.innerHTML = `Keyboard Shortcuts (Map Editor)
+ S - Add
+ D - Delete
+ R - Reverse team
+
+ How to Use the Map Editor
+ Select the initial map settings: name, width, height, and symmetry.
+
+ To place enlightenment centers, enter the "change robots" mode, set the coordinates, set the initial influence of the + center (abbreviated as "I"), and click "Add/Update" or "Delete." The coordinates can also be set by clicking the map. +
+
+ To set tiles' passability values, enter the "change tiles" mode, select the passability value, brush size, and brush style, + and then hold and drag your mouse across the map.
+
+ To save an intermediary version of your map, copy the map JSON. You can input this JSON later to retrieve your map in the map editor for further editing.
+
+ + When you are happy with your map, click "Export". + If you are directed to save your map, save it in the + /battlecode-scaffold-2021/maps directory of your scaffold. + (Note: the name of your .map21 file must be the same as the name of your + map.)
+
+ Exported file name must be the same as the map name chosen above. For instance, DefaultMap.bc21.`; return div; } @@ -74,25 +101,21 @@ export default class MapEditor { /** * Quick add and delete units in the map editor */ - onkeydown(): (event: KeyboardEvent) => void { - return (event: KeyboardEvent) => { - var input = (document.activeElement).nodeName == "INPUT"; - if(!input) { - console.error(event.keyCode); - switch (event.keyCode) { - case 67: // "c" - Toggle Circle Bots - this.conf.circleBots = !this.conf.circleBots; - this.form.render(); - break; - case 83: // "s" - Set (Add/Update)c - this.form.buttonAdd.click(); - break; - case 68: // "d" - Delete - this.form.buttonDelete.click(); - break; - } + readonly onkeydown = (event: KeyboardEvent) => { + var input = (document.activeElement).nodeName == "INPUT"; + if(!input) { + switch (event.keyCode) { + case 83: // "s" - Set (Add/Update)c + this.form.buttonAdd.click(); + break; + case 68: // "d" - Delete + this.form.buttonDelete.click(); + break; + case 82: // "r" - Reverse team + this.form.buttonReverse.click(); + break; } - }; + } } private isValid(): boolean { @@ -110,7 +133,7 @@ export default class MapEditor { const button = document.createElement("button"); button.type = "button"; button.className = 'form-button'; - button.appendChild(document.createTextNode("Validate")); + button.appendChild(document.createTextNode("Validate Map")); button.onclick = () => { if (this.isValid()) { alert("Congratulations! Your map is valid. :)") @@ -144,23 +167,84 @@ export default class MapEditor { private resetButton(): HTMLButtonElement { const button = document.createElement("button"); button.type = "button"; - button.className = 'form-button'; - button.appendChild(document.createTextNode("RESET")); + button.className = 'form-button custom-button'; + button.style.backgroundColor = "mediumslateblue"; + button.appendChild(document.createTextNode("Reset Map")); button.onclick = () => { - let youAreSure = confirm( - "WARNING: you will lose all your data. Click OK to continue anyway."); - if (youAreSure) { - this.form.reset(); - } + this.form.reset(); }; return button; } + // private getMapJSONButton(): HTMLButtonElement { + // const button = document.createElement("button"); + // button.type = "button"; + // button.className = 'form-button custom-button'; + // button.appendChild(document.createTextNode(process.env.ELECTRON ? "Copy Map JSON to Clipboard" : "Get Map JSON")); + // button.onclick = () => { + // // from https://stackoverflow.com/questions/17591559/how-to-copy-text-of-alert-box + // if (!process.env.ELECTRON) { + // const newWin = window.open(); + // if (newWin) { + // newWin.document.write(this.form.getMapJSON()); + // newWin.document.close(); + // } + // } + // else { + // // prompt("Copy to clipboard: Ctrl+C, Enter", this.form.getMapJSON()); + // electron.clipboard.writeText(this.form.getMapJSON()); + // } + // }; + // return button; + // } + + // private pasteMapJSONButton(): HTMLButtonElement { + // const button = document.createElement("button"); + // button.type = "button"; + // button.className = 'form-button custom-button'; + // button.appendChild(document.createTextNode(process.env.ELECTRON ? "Input Map JSON from Clipboard" : "Input Map JSON")); + // button.onclick = () => { + // if (!process.env.ELECTRON) this.form.setMap(prompt("Paste Map JSON: Ctrl+V, Enter")); + // else this.form.setMap(electron.clipboard.readText()); + // }; + // return button; + // } + + private importMapButton() { + let uploadLabel = document.createElement("label"); + uploadLabel.setAttribute("for", "file-upload"); + uploadLabel.setAttribute("class", "custom-button"); + uploadLabel.innerText = 'Upload a .map21 file'; + uploadLabel.style.backgroundColor = "mediumslateblue"; // TODO: move to CSS + // create the functional button + let upload = document.createElement('input'); + upload.textContent = 'upload'; + upload.id = "file-upload"; + upload.setAttribute('type', 'file'); + upload.accept = '.map21'; + upload.onchange = () => { + if (upload.files) { + const reader = new FileReader(); + reader.onload = () => { + const map: UploadedMap = MapGenerator.readMap(reader.result); + console.log(map); + this.form.setUploadedMap(map); + }; + reader.readAsArrayBuffer(upload.files[0]); + } + } + upload.onclick = () => upload.value = ""; + uploadLabel.appendChild(upload); + + return uploadLabel; + } + private exportButton(): HTMLButtonElement { const button = document.createElement("button"); button.id = "export"; button.type = "button"; - button.appendChild(document.createTextNode("EXPORT!")); + button.innerText = "Export!"; + button.className = 'form-button custom-button'; button.onclick = () => { if (!this.isValid()) return; diff --git a/client/visualizer/src/profiler.ts b/client/visualizer/src/profiler.ts index 6f0442e4..28e721e5 100644 --- a/client/visualizer/src/profiler.ts +++ b/client/visualizer/src/profiler.ts @@ -1,34 +1,13 @@ // This script is loaded by speedscope in the iframe shown in the game area when the Profiler tab is visible // It listens for messages passed via window.postMessage -import { ProfilerFile } from 'battlecode-playback/out/match'; - function applyCSS(css: string): void { const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); } -function load(file: ProfilerFile, robot: number): void { - const frames = file.frames.map(frame => ({ name: frame })); - const profiles = file.profiles.map(profile => ({ - type: 'evented', - name: profile.name, - unit: 'none', - startValue: profile.events[0].at, - endValue: profile.events[profile.events.length - 1].at, - events: profile.events, - })); - - const data = { - $schema: 'https://www.speedscope.app/file-format-schema.json', - activeProfileIndex: robot, - shared: { - frames, - }, - profiles, - }; - +function load(data: any): void { (window as any).speedscope.loadFileFromBase64('data.json', btoa(JSON.stringify(data))); } @@ -40,7 +19,7 @@ window.addEventListener('message', event => { applyCSS(data.payload); break; case 'load': - load(data.payload.file, data.payload.robot); + load(data.payload); break; } }); diff --git a/client/visualizer/src/runner.ts b/client/visualizer/src/runner.ts index c2b19bfb..76349474 100644 --- a/client/visualizer/src/runner.ts +++ b/client/visualizer/src/runner.ts @@ -14,8 +14,9 @@ import WebSocketListener from './main/websocket'; // import { electron } from './main/electron-modules'; import { TeamStats } from 'battlecode-playback/out/gameworld'; -import { Tournament, readTournament } from './main/tournament'; +import { Tournament, readTournament } from './main/tournament_new'; import Looper from './main/looper'; +import Sidebar from './main/sidebar'; /** @@ -27,8 +28,11 @@ export default class Runner { private matchqueue: MatchQueue; private controls: Controls; private stats: Stats; - private gamearea: GameArea; + private gamearea: GameArea; private console: Console; + private profiler?: Profiler; + private asyncRequests: XMLHttpRequest[] = []; + private sidebar: Sidebar; looper: Looper | null; // Match logic @@ -38,6 +42,7 @@ export default class Runner { tournament?: Tournament; tournamentState: TournamentState; + showTourneyUpload: boolean = true; currentGame: number | null; currentMatch: number | null; @@ -46,174 +51,40 @@ export default class Runner { this.games = []; - // If it's in dev, we're not using server - if (this.conf.websocketURL !== null && process.env.NODE_ENV !== 'development') { - this.listener = new WebSocketListener( - this.conf.websocketURL, - this.conf.pollEvery - ); + // Only listen if run as an app. + if (this.conf.websocketURL !== null && process.env.ELECTRON) { //&& process.env.NODE_ENV !== 'development' <- useful to listen even in development. + this.listener = new WebSocketListener(this.conf.websocketURL, + this.conf.pollEvery, + this.conf); } } /** * Marks the client as fully loaded. */ - ready(controls: Controls, stats: Stats, gamearea: GameArea, - cconsole: Console, matchqueue: MatchQueue) { + ready(controls: Controls, stats: Stats, gamearea: GameArea, + cconsole: Console, matchqueue: MatchQueue, sidebar: Sidebar, + profiler?: Profiler) { this.controls = controls; this.stats = stats; this.gamearea = gamearea; this.console = cconsole; this.matchqueue = matchqueue; + this.profiler = profiler; + this.sidebar = sidebar; this.gamearea.setCanvas(); - let startGame = () => { - if (this.games.length === 1) { - // if only one game in queue, run its first match - this.setGame(0); - } - this.matchqueue.refreshGameList(this.games, this.currentGame ? this.currentGame : 0, this.currentMatch ? this.currentMatch : 0); - } - - let toMain = (msg) => { - console.log(msg); - alert('Error occurred. Check console on your browser'); - // window.location.assign('/visualizer.html'); + if (this.conf.tournamentMode) { + this.conf.processLogs = false; // in tournament mode, don't process logs by default } + this.console.setNotLoggingDiv(); if (this.conf.matchFileURL) { - // Load a match file console.log(`Loading provided match file: ${this.conf.matchFileURL}`); - - const req = new XMLHttpRequest(); - req.open('GET', this.conf.matchFileURL, true); - req.responseType = 'arraybuffer'; - req.onerror = (error) => { - toMain(`Can't load provided match file: ${error}`); - }; - - req.onload = (event) => { - const resp = req.response; - if (!resp || req.status !== 200) { - toMain(`Can't load file from URL: invalid URL(${req.status})`); - } - else { - let lastGame = this.games.length - this.games[lastGame] = new Game(); - - try { - this.games[lastGame].loadFullGameRaw(resp); - } catch (error) { - toMain(`Can't get file from URL: ${error}`); - return; - } - - console.log('Successfully loaded provided match file'); - startGame(); - } - }; - - req.send(); + this.loadGameFromURL(this.conf.matchFileURL); } - // not loading default match file - // else { - // console.log('Starting with a default match file (/client/default.bc21)'); - // if(_fs.readFile){ - // _fs.readFile('../default.bc21', (err, data: ArrayBuffer) => { - // if(err){ - // console.log('Error while loading default local file!'); - // console.log(err); - // console.log('Starting without any match files. Please upload via upload button in queue tab of sidebar'); - // return; - // } - - // let lastGame = this.games.length - // this.games[lastGame] = new Game(); - // // lastGame should be 0? - // try { - // this.games[lastGame].loadFullGameRaw(data); - // } catch (error) { - // console.log(`Error occurred! ${error}`); - // } - - // console.log('Running game!'); - // startGame(); - // }); - // } - // } - // set key options - document.onkeydown = (event) => { - // TODO: figure out what this is??? - if (document.activeElement == null) { - throw new Error('idk?????? i dont know what im doing document.actievElement is null??'); - } - - let input = document.activeElement.nodeName == "INPUT"; - if (!input) { - // TODO after touching viewoption buttons, the input (at least arrow keys) does not work - const keyCode = event.keyCode; - console.log(`Key pressed: ${keyCode} (${String.fromCharCode(keyCode)})`); - switch (keyCode) { - case 80: // "p" - Pause/Unpause - this.controls.pause(); - break; - case 79: // "o" - Stop - this.controls.stop(); - break; - case 69: // 'e' - go to end - this.controls.end(); - break; - case 37: // "LEFT" - Step Backward - this.controls.stepBackward(); - break; - case 39: // "RIGHT" - Step Forward - this.controls.stepForward(); - break; - case 38: // "UP" - Faster - this.controls.doubleUPS(); - break; - case 40: // "DOWN" - Slower - this.controls.halveUPS(); - break; - case 82: // "r" - reverse UPS - this.controls.reverseUPS(); - break; - case 86: // "v" - Toggle Indicator Dots and Lines - this.conf.indicators = !this.conf.indicators; - break; - case 66: // "b" - Toggle Interpolation - this.conf.interpolate = !this.conf.interpolate; - break; - case 78: // "n" - Toggle action radius - this.conf.seeActionRadius = !this.conf.seeActionRadius; - break; - case 77: // "m" - Toggle sensor radius - this.conf.seeSensorRadius = !this.conf.seeSensorRadius; - break; - case 188: // "," - Toggle detection radius - this.conf.seeDetectionRadius = !this.conf.seeDetectionRadius; - break; - case 71: // "g" - Toogle grid view - this.conf.showGrid = !this.conf.showGrid; - break; - case 72: // "h" - Toggle short log header - this.conf.shorterLogHeader = !this.conf.shorterLogHeader; - this.console.updateLogHeader(); - break; - case 65: // "a" - previous tournament Match - this.previousTournamentThing(); - this.updateTournamentState(); - break; - case 68: // 'd' - next tournament match - this.nextTournamentThing(); - this.updateTournamentState(); - break; - } - } - - }; if (this.listener != null) { this.listener.start( @@ -225,8 +96,7 @@ export default class Runner { // What to do with the websocket's first match in a given game () => { // switch to running this match - this.setGame(this.games.length - 1); - this.setMatch(0); + this.goToMatch(this.games.length - 1, 0); this.matchqueue.refreshGameList(this.games, this.currentGame ? this.currentGame : 0, this.currentMatch ? this.currentMatch : 0); }, // What to do with any other match @@ -304,12 +174,38 @@ export default class Runner { } } - onGameLoaded(data: ArrayBuffer) { - let lastGame = this.games.length - this.games[lastGame] = new Game(); - this.games[lastGame].loadFullGameRaw(data); + loadGameFromURL(url: string) { + // Load a match file + const req = new XMLHttpRequest(); + req.open('GET', url, true); + req.responseType = 'arraybuffer'; + req.onerror = (error) => { + throw new Error(`Can't load provided match file: ${error}`); + }; - this.startGame(); + req.onload = (event) => { + const resp = req.response; + if (!resp || req.status !== 200) { + throw new Error(`Can't load file from URL: invalid URL(${req.status})`); + } + else { + this.onGameLoaded(resp); + } + }; + + req.send(); + this.asyncRequests.push(req); + } + + onGameLoaded(data: ArrayBuffer) { + try { + const newGame = new Game(this.conf); + newGame.loadFullGameRaw(data); + this.games.push(newGame); + this.startGame(); + } catch { + throw new Error("game load failed."); + } }; startGame() { @@ -320,53 +216,43 @@ export default class Runner { this.matchqueue.refreshGameList(this.games, this.currentGame ? this.currentGame : 0, this.currentMatch ? this.currentMatch : 0); } - onTournamentLoaded(jsonFile: File) { - if (!process.env.ELECTRON) { - console.error("Can't load tournament outside of electron!"); - return; - } - readTournament(jsonFile, (err, tournament) => { - if (err) { - console.error(`Can't load tournament: ${err}`); - return; - } - if (tournament) { - this.tournament = tournament; - const t = this; - document.onkeydown = function (event) { - // TODO: figure out what this is??? - if (document.activeElement == null) { - throw new Error('idk?????? i dont know what im doing document.actievElement is null??'); - } - let input = document.activeElement.nodeName == "INPUT"; - if (!input) { - // TODO after touching viewoption buttons, the input (at least arrow keys) does not work - console.log(event.keyCode); - switch (event.keyCode) { - case 65: // "a" - previous tournament Match - t.previousTournamentThing(); - t.updateTournamentState(); - break; - case 68: // 'd' - next tournament match - console.log('next tournament d!'); - t.nextTournamentThing(); - t.updateTournamentState(); - break; - } - } + removeGame(game: number) { - }; - // CHOOSE STARTING ROUND? - tournament.seek(0, 0); - this.tournamentState = TournamentState.START_SPLASH; - this.updateTournamentState(); + if (game > (this.currentGame as number)) { + this.games.splice(game, 1); + } else if (this.currentGame == game) { + if (game == 0) { + // if games.length > 1, remove game, set game to 0, set match to 0 + if (this.games.length > 1) { + this.setGame(0); + this.games.splice(game, 1); + } else { + this.resetAllGames(); + } + } else { + this.setGame(game - 1); + this.games.splice(game, 1); } - }); + } else { + // remove game, set game to game - 1 + this.games.splice(game, 1); + this.currentGame = game - 1; + } + + this.showBlankCanvas(); + + this.matchqueue.refreshGameList(this.games, this.currentGame ? this.currentGame : 0, this.currentMatch ? this.currentMatch : 0); }; - goNextMatch() { - console.log("NEXT MATCH"); + showBlankCanvas() { + if (this.games.length == 0) { + this.conf.splash = true; + this.gamearea.setCanvas(); + } + } + + goNextMatch() { if (this.currentGame as number < 0) { return; // Special case when deleting games } @@ -384,8 +270,6 @@ export default class Runner { } goPreviousMatch() { - console.log("PREV MATCH"); - if (this.currentMatch as number > 0) { this.setMatch(this.currentMatch as number - 1); } else { @@ -398,64 +282,48 @@ export default class Runner { }; - seekTournament (num: number) { + onTournamentLoaded(jsonFile: File) { + readTournament(jsonFile, (tournament) => { + this.tournament = tournament; + + // Choose starting round + tournament.seek(0, 0); + this.tournamentState = TournamentState.START_SPLASH; + this.processTournamentState(); + }, (err) => { + console.error(`Can't load tournament: ${err}`); + return; + }); + }; + + seekTournament(num: number) { console.log('seek tournament'); this.tournament?.seek(num, 0); - this.updateTournamentState(); + this.processTournamentState(); }; - removeGame(game: number) { - - if (game > (this.currentGame as number)) { - this.games.splice(game, 1); - } else if (this.currentGame == game) { - if (game == 0) { - // if games.length > 1, remove game, set game to 0, set match to 0 - if (this.games.length > 1) { - this.setGame(0); - this.games.splice(game, 1); - } else { - this.games.splice(game, 1); - if (this.looper) { - this.looper.die(); - this.looper = null; - } - this.currentGame = -1; - this.currentMatch = 0; - } - } else { - this.setGame(game - 1); - this.games.splice(game, 1); - } - } else { - // remove game, set game to game - 1 - this.games.splice(game, 1); - this.currentGame = game - 1; - } - - if (this.games.length == 0) { - this.conf.splash = true; - this.gamearea.setCanvas(); - } - + resetAllGames() { + if (this.looper) this.looper.die(); + this.games = []; + this.currentGame = -1; + this.currentMatch = 0; this.matchqueue.refreshGameList(this.games, this.currentGame ? this.currentGame : 0, this.currentMatch ? this.currentMatch : 0); - }; + this.asyncRequests.forEach((req) => req.abort()); + } - private nextTournamentThing() { + /** + * Transitions tournament state from start splash, to mid game, + * to end splash (only if last match in game), to next game. + */ + private nextTournamentState() { console.log('actually next tournament thing!'); - // either displays a splash screen with who won, or displays the next game - // activated by some sort of hotkey, ideally - // so, we can be in a couple of different states - // either a splash screen is showing, in which case we should display the next game, - // or a game is showing, in which case we should display either a next game or a splash screen - if (this.tournament) { if (this.tournamentState === TournamentState.START_SPLASH) { // transition to mid game this.tournamentState = TournamentState.MID_GAME; } else if (this.tournamentState === TournamentState.MID_GAME) { // go to the next game - if (this.tournament.hasNext() && !this.tournament.isLastGameInMatch()) { + if (this.tournament.hasNext() && !this.tournament.isLastMatchInGame()) { this.tournament.next(); } else { // go to end splash @@ -474,13 +342,7 @@ export default class Runner { } } - private previousTournamentThing() { - // either displays a splash screen with who won, or displays the next game - // activated by some sort of hotkey, ideally - // so, we can be in a couple of different states - // either a splash screen is showing, in which case we should display the next game, - // or a game is showing, in which case we should display either a next game or a splash screen - + private previousTournamentState() { if (this.tournament) { if (this.tournamentState === TournamentState.START_SPLASH) { // transition to mid game @@ -492,7 +354,7 @@ export default class Runner { } } else if (this.tournamentState === TournamentState.MID_GAME) { // go to the previous game - if (this.tournament.hasPrev() && !this.tournament.isFirstGameInMatch()) { + if (this.tournament.hasPrev() && !this.tournament.isFirstMatchInGame()) { this.tournament.prev(); } else { // go to start splash @@ -506,31 +368,33 @@ export default class Runner { } } - private updateTournamentState() { + private processTournamentState() { console.log('update tour state!'); if (this.tournament) { console.log('real update tour state!'); // clear things Splash.removeScreen(); - if (this.looper) this.looper.die(); // simply updates according to the current tournament state + this.resetAllGames(); + this.showBlankCanvas(); if (this.tournamentState === TournamentState.START_SPLASH) { console.log('go from splash real update tour state!'); - Splash.addScreen(this.conf, this.root, this.tournament.current(), this.tournament.currentMatch(), this.tournament); + Splash.addScreen(this.conf, this.root, this.tournament.current().team1, this.tournament.current().team2); } else if (this.tournamentState === TournamentState.END_SPLASH) { - Splash.addWinnerScreen(this.conf, this.root, this.tournament, this.tournament.currentMatch()); + const wins = this.tournament.wins(); + const totalWins = this.tournament.totalWins(); + let result: string = ""; + const team1 = this.tournament.current().team1; + const team2 = this.tournament.current().team2; + if (wins[team1] > wins[team2]) result = `${team1} wins ${wins[team1]}-${wins[team2]}!`; + else result = `${team2} wins ${wins[team2]}-${wins[team1]}!`; + if (totalWins[team1] != wins[team1] || totalWins[team2] != wins[team2]) { + if (totalWins[team1] > totalWins[team2]) result += ` (Final score ${totalWins[team1]}-${totalWins[team2]})`; + else result += ` (Final score ${totalWins[team2]}-${totalWins[team1]})`; + } + Splash.addWinnerScreen(this.conf, this.root, result); } else if (this.tournamentState === TournamentState.MID_GAME) { - this.tournament.readCurrent((err, data) => { - if (err) throw err; - if (!data) throw new Error("No match loaded from tournament?"); - - // reset all games so as to save memory - // because things can be rough otherwise - this.games.pop(); - this.games = [new Game()]; - this.games[0].loadFullGameRaw(data); - this.setGame(0); - }); + this.loadGameFromURL(this.tournament.current().url); } } } @@ -549,14 +413,117 @@ export default class Runner { } const game = this.games[this.currentGame as number] as Game; - const match = game.getMatch(this.currentMatch as number) as Match; + const match = game.getMatch(this.currentMatch as number) as Match; const meta = game.meta as Metadata; if (this.looper) this.looper.die(); + var infoString = ""; + + if (this.tournament) { + const wins = this.tournament.wins(this.tournament.matchI - 1); + infoString += `Map: ${this.tournament?.current().map}`; + infoString += `
`; + infoString += `Score: ${wins[this.tournament.current().team1]} - ${wins[this.tournament.current().team2]}`; + } + this.looper = new Looper(match, meta, this.conf, this.imgs, - this.controls, this.stats, this.gamearea, this.console, this.matchqueue); + this.controls, this.stats, this.gamearea, this.console, this.matchqueue, this.profiler, infoString, + this.showTourneyUpload); + + // if (this.profiler) + // this.profiler.load(match); } + + readonly onkeydown = (event: KeyboardEvent) => { + // TODO: figure out what this is??? + if (document.activeElement == null) { + throw new Error('idk?????? i dont know what im doing document.actievElement is null??'); + } + + let input = document.activeElement.nodeName == "INPUT"; + if (!input) { + // TODO after touching viewoption buttons, the input (at least arrow keys) does not work + const keyCode = event.keyCode; + switch (keyCode) { + case 80: // "p" - Pause/Unpause + this.controls.pause(); + break; + case 79: // "o" - Stop + this.controls.stop(); + break; + case 69: // 'e' - go to end + this.controls.end(); + break; + case 37: // "LEFT" - Step Backward + this.controls.stepBackward(); + break; + case 39: // "RIGHT" - Step Forward + this.controls.stepForward(); + break; + case 38: // "UP" - Faster + this.controls.doubleUPS(); + break; + case 40: // "DOWN" - Slower + this.controls.halveUPS(); + break; + case 82: // "r" - reverse UPS + this.controls.reverseUPS(); + break; + case 86: // "v" - Toggle Indicator Dots and Lines + this.conf.indicators = !this.conf.indicators; + break; + case 67: // "c" - Toggle All Indicator Dots and Lines + this.conf.allIndicators = !this.conf.allIndicators; + break; + case 66: // "b" - Toggle Interpolation + this.conf.interpolate = !this.conf.interpolate; + break; + case 78: // "n" - Toggle action radius + this.conf.seeActionRadius = !this.conf.seeActionRadius; + break; + case 77: // "m" - Toggle sensor radius + this.conf.seeSensorRadius = !this.conf.seeSensorRadius; + break; + case 188: // "," - Toggle detection radius + this.conf.seeDetectionRadius = !this.conf.seeDetectionRadius; + break; + case 71: // "g" - Toogle grid view + this.conf.showGrid = !this.conf.showGrid; + break; + case 72: // "h" - Toggle short log header + this.conf.shorterLogHeader = !this.conf.shorterLogHeader; + this.console.updateLogHeader(); + break; + case 65: // "a" - previous tournament Match + this.previousTournamentState(); + this.processTournamentState(); + break; + case 68: // 'd' - next tournament match + this.nextTournamentState(); + this.processTournamentState(); + break; + case 219: // '[' - hide sidebar + this.sidebar.hidePanel(); + this.stats.hideTourneyUpload(); + this.showTourneyUpload = !this.showTourneyUpload; + break; + case 76: // 'l' - Toggle process logs + this.conf.processLogs = !this.conf.processLogs; + this.console.setNotLoggingDiv(); + break; + case 81: // 'q' - Toggle profiler + if (this.profiler) { + this.conf.doProfiling = !this.conf.doProfiling; + this.profiler.setNotProfilingDiv(); + } + break; + case 90: // 'z' - Toggle rotate + this.conf.doRotate = !this.conf.doRotate; + } + } + + }; } export enum TournamentState { diff --git a/client/visualizer/src/sidebar/console.ts b/client/visualizer/src/sidebar/console.ts index 222849f5..cc5ef944 100644 --- a/client/visualizer/src/sidebar/console.ts +++ b/client/visualizer/src/sidebar/console.ts @@ -19,6 +19,7 @@ export default class Console { private teamBInput: HTMLInputElement; private lengthInput: HTMLInputElement; private copyButton: HTMLButtonElement; + private notLoggingDiv: HTMLDivElement; // Filters // use teamA(), teamB(), minRound(), and maxRound() to get the other filters @@ -28,7 +29,8 @@ export default class Console { // Options private readonly MIN_ROUNDS: number = 1; private readonly MAX_ROUNDS: number = 3000; - private readonly DEFAULT_MAX_ROUNDS: number = 15; + private readonly MAX_ROUNDS_SHOW: number = 25; + private readonly NUM_ROUNDS_SHOW: number = 15; private readonly conf: Config; // Used to check if there are more logs to pull from the match. @@ -38,6 +40,7 @@ export default class Console { // more logs added behind our backs. // On the other hand, it may not have logs for a round at all. private logsRef: Array> | null; + private logsShift: number; // Invariants: // - consoleDivs are the div objects displayed in the console @@ -69,6 +72,13 @@ export default class Console { this.lengthInput = this.getHTMLInput(); this.copyButton = this.getHTMLCopyButton(); + this.notLoggingDiv = document.createElement("div"); + this.notLoggingDiv.className = "not-logging-div"; + this.notLoggingDiv.textContent = "Not processing logs (toggle with L)."; + this.notLoggingDiv.hidden = this.conf.processLogs; + + div.appendChild(this.notLoggingDiv); + // Add a tip const span = document.createElement("span"); const p = document.createElement('p'); @@ -93,7 +103,7 @@ export default class Console { div.appendChild(document.createElement("br")); // Add the round filter - div.appendChild(document.createTextNode("Max Number of Rounds:")); + div.appendChild(document.createTextNode(`Number of Rounds to Show (max ${this.MAX_ROUNDS_SHOW}):`)); div.appendChild(this.lengthInput); div.appendChild(document.createElement("br")); @@ -107,7 +117,6 @@ export default class Console { this.updateLogHeader(); - return div; } @@ -132,17 +141,17 @@ export default class Console { private getHTMLInput(): HTMLInputElement { const input = document.createElement("input"); input.type = "text"; - input.value = String(this.DEFAULT_MAX_ROUNDS); + input.value = String(this.NUM_ROUNDS_SHOW); input.onchange = () => { // Input validation, must be a number between 1 and 50, // Defaults to this.DEFAULT_MAX_ROUNDS otherwise. const value: number = parseInt(input.value); if (isNaN(value)) { - input.value = String(this.DEFAULT_MAX_ROUNDS); + input.value = String(this.NUM_ROUNDS_SHOW); } else if (value < this.MIN_ROUNDS) { input.value = String(this.MIN_ROUNDS); - } else if (value > this.MAX_ROUNDS) { - input.value = String(this.MAX_ROUNDS); + } else if (value > this.MAX_ROUNDS_SHOW) { + input.value = String(this.MAX_ROUNDS_SHOW); } // Then reapply the filter @@ -168,8 +177,9 @@ export default class Console { /** * Set the logs we should be checking. */ - setLogsRef(logsRef: Array>): void { + setLogsRef(logsRef: Array>, logsShift: number): void { this.logsRef = logsRef; + this.logsShift = logsShift; } /** @@ -183,11 +193,14 @@ export default class Console { if (this.currentRound === previousRound) { // We are in the same round, don't to anything return; - } else if (this.currentRound === previousRound + 1) { - // We went forward a single round; just push and shift for efficiency - this.shiftRound(); - this.pushRound(this.maxRound()); - } else { + } + // now that logs are in gameworld, shifting logic is obsolete. + // else if (this.currentRound === previousRound + 1) { + // // We went forward a single round; just push and shift for efficiency + // this.shiftRound(); + // this.pushRound(this.maxRound()); + // } + else { // Otherwise we need to reapply the entire filter this.applyFilter(); } @@ -201,6 +214,13 @@ export default class Console { this.applyFilter(); } + /** + * Sets indicator of whether logs are being processed. + */ + setNotLoggingDiv() { + this.notLoggingDiv.hidden = this.conf.processLogs; + } + /** * Removes all the logs from the minimum round, which may be at most the * earliest round currently displayed in the console. If it is less than the @@ -226,8 +246,8 @@ export default class Console { */ private pushRound(round: number): void { // If logs exist for this round - if (this.logsRef != null && this.logsRef[round] != undefined) { - const logs = this.logsRef[round]; + if (this.logsRef != null && this.logsRef[round-this.logsShift] != undefined) { + const logs = this.logsRef[round-this.logsShift]; // For each log in the round, add it to the console if it's good logs.forEach((log: Log) => { @@ -314,8 +334,10 @@ export default class Console { this.consoleLogs = new Array(); // Push all the logs from the defined rounds that match the filter - for (let round = this.minRound(); round <= this.maxRound(); round++) { - this.pushRound(round); + if (this.conf.processLogs) { + for (let round = this.minRound(); round <= this.maxRound(); round++) { + this.pushRound(round); + } } } diff --git a/client/visualizer/src/sidebar/mapfilter.ts b/client/visualizer/src/sidebar/mapfilter.ts index cc65fcb4..1d31feb8 100644 --- a/client/visualizer/src/sidebar/mapfilter.ts +++ b/client/visualizer/src/sidebar/mapfilter.ts @@ -20,9 +20,6 @@ export default class MapFilter { private readonly filterName: HTMLInputElement; private readonly filterType: Map; - // Map types available (NOTE: Update after each tournament) - private readonly types: MapType[] = [MapType.DEFAULT, MapType.CUSTOM, MapType.SPRINT, MapType.SEEDING, MapType.INTL_QUALIFYING]; - // All the maps displayed on the client private maps: Array; @@ -111,7 +108,7 @@ export default class MapFilter { this.filterName.onchange = () => { this.applyFilter() }; // Filter for map type - for (let type of this.types) { + for (let type of cst.mapTypes) { const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.value = String(type); @@ -146,12 +143,10 @@ export default class MapFilter { private mapTypeToString(type: MapType): string { switch(type) { case MapType.DEFAULT: return "Default"; - case MapType.SPRINT: return "Sprint"; - case MapType.SEEDING: return "Seeding"; - case MapType.INTL_QUALIFYING: return "Intl Quals"; - case MapType.US_QUALIFYING: return "US Quals"; - case MapType.HS: return "HS"; - case MapType.NEWBIE: return "Newbie"; + case MapType.SPRINT_1: return "Sprint 1"; + case MapType.SPRINT_2: return "Sprint 2"; + case MapType.QUALIFYING: return "Quals"; + case MapType.HS_NEWBIE: return "HS and Newbie"; case MapType.FINAL: return "Final"; default: return "Custom"; } diff --git a/client/visualizer/src/sidebar/matchqueue.ts b/client/visualizer/src/sidebar/matchqueue.ts index 005ed1db..098afef3 100644 --- a/client/visualizer/src/sidebar/matchqueue.ts +++ b/client/visualizer/src/sidebar/matchqueue.ts @@ -2,7 +2,6 @@ import {Config} from '../config'; import {AllImages} from '../imageloader'; import {Game} from 'battlecode-playback'; -import { Profiler } from "./index"; import Runner from '../runner'; export default class MatchQueue { @@ -16,18 +15,14 @@ export default class MatchQueue { // Options private readonly conf: Config; - // The profiler to initialize when switching matches - profiler: Profiler; - private runner: Runner; // Images private readonly images: AllImages; - constructor(conf: Config, images: AllImages, profiler: Profiler, runner: Runner) { + constructor(conf: Config, images: AllImages, runner: Runner) { this.conf = conf; this.images = images; - this.profiler = profiler; this.runner = runner; this.div = this.basediv(); } @@ -77,15 +72,6 @@ export default class MatchQueue { } refreshGameList(gameList: Array, activeGame: number, activeMatch: number) { - // Initialize the profiler with the active match - if (!this.conf.tournamentMode) { // disable the profiler in tournaments out of memory concerns - if (gameList[activeGame]) { - this.profiler.load(gameList[activeGame].getMatch(activeMatch)); - } else { - this.profiler.load(undefined); - } - } - // Remove all games from the list // TODO: this assumes there are exactly 3 HTML elements before the first match. that's bad while (this.div.childNodes[3]) { diff --git a/client/visualizer/src/sidebar/matchrunner.ts b/client/visualizer/src/sidebar/matchrunner.ts index 802f8c5f..8c7afd72 100644 --- a/client/visualizer/src/sidebar/matchrunner.ts +++ b/client/visualizer/src/sidebar/matchrunner.ts @@ -44,6 +44,7 @@ export default class MatchRunner { private runMatch: HTMLButtonElement; private refreshButton: HTMLButtonElement; private runMatchWithoutViewing: HTMLButtonElement; + private killProcs: HTMLButtonElement; constructor(conf: Config, cb: () => void, runCb: () => void) { this.conf = conf; @@ -116,6 +117,7 @@ export default class MatchRunner { this.selectAllMaps = document.createElement("button"); this.deselectAllMaps = document.createElement("button"); this.runMatchWithoutViewing = document.createElement("button"); + this.killProcs = document.createElement("button"); this.profilerEnabled = document.createElement("input"); this.profilerEnabled.type = 'checkbox'; @@ -123,7 +125,7 @@ export default class MatchRunner { const profilerLabel = document.createElement('label'); profilerLabel.setAttribute('for', 'profiler-enabled'); - profilerLabel.innerText = 'Profiler enabled (will be slower)'; + profilerLabel.innerText = 'Profiler enabled (will be more memory-intensive)'; div.appendChild(document.createElement("br")); // Team A selector @@ -144,7 +146,7 @@ export default class MatchRunner { const divProfiler = document.createElement("p"); divProfiler.appendChild(this.profilerEnabled); divProfiler.appendChild(profilerLabel); - div.appendChild(divProfiler); + if (this.conf.useProfiler) div.appendChild(divProfiler); // Map selector div.appendChild(document.createTextNode("Select maps: ")); @@ -210,6 +212,14 @@ export default class MatchRunner { this.compileLogs.innerHTML = "Compile messages..." div.appendChild(this.compileLogs); + // Kill procs button + this.killProcs.type = "button"; + this.killProcs.appendChild(document.createTextNode("Kill ongoing processes")); + this.killProcs.className = 'custom-button'; + this.killProcs.onclick = () => this.scaffold.killProcs(); + div.appendChild(this.killProcs); + div.appendChild(document.createElement("br")); + return div; } @@ -294,6 +304,7 @@ export default class MatchRunner { this.getTeamB(), this.getMaps(), this.isProfilerEnabled(), + (cmd: string) => this.makeLog("Running " + cmd, 'specialLog'), (err) => { console.log(err.stack); this.isLoadingMatch = false; @@ -302,21 +313,22 @@ export default class MatchRunner { this.isLoadingMatch = false; }, (stdoutdata) => { - const logs = document.createElement('p'); - logs.innerHTML = stdoutdata.split('\n').join('
'); - this.compileLogs.appendChild(logs); - this.compileLogs.scrollTop = this.compileLogs.scrollHeight; + this.makeLog(stdoutdata); }, (stderrdata) => { - const logs = document.createElement('p'); - logs.innerHTML = stderrdata.split('\n').join('
'); - logs.className = 'errorLog'; - this.compileLogs.appendChild(logs); - this.compileLogs.scrollTop = this.compileLogs.scrollHeight; + this.makeLog(stderrdata, 'errorLog'); } ); } + private makeLog(content: string, className?: string) { + const logs = document.createElement('p'); + logs.innerHTML = content.split('\n').join('
'); + if (className) logs.className = className; + this.compileLogs.appendChild(logs); + this.compileLogs.scrollTop = this.compileLogs.scrollHeight; + } + /** * Refresh the player list and maps */ diff --git a/client/visualizer/src/sidebar/profiler.ts b/client/visualizer/src/sidebar/profiler.ts index 0d22e096..256ff95e 100644 --- a/client/visualizer/src/sidebar/profiler.ts +++ b/client/visualizer/src/sidebar/profiler.ts @@ -1,5 +1,6 @@ import { Match } from 'battlecode-playback'; import { ProfilerFile } from 'battlecode-playback/out/match'; +import * as config from '../config'; enum Team { A, B @@ -11,34 +12,53 @@ export default class Profiler { private teamSelector: HTMLSelectElement; private robotSelector: HTMLSelectElement; + private notProfilingDiv: HTMLDivElement; private profilerFiles: ProfilerFile[]; private currentTeamIndex: number = -1; private currentRobotIndex: number = -1; - constructor() { + constructor(private conf: config.Config) { this.div = this.createSidebarDiv(); this.iframe = this.createIFrame(); } - public load(match: Match | undefined): void { - this.profilerFiles = match !== undefined ? (match.profilerFiles || []) : []; - + public reset() { this.clearSelect(this.teamSelector); this.clearSelect(this.robotSelector); this.currentTeamIndex = -1; this.currentRobotIndex = -1; + const win = this.iframe.contentWindow; + if (win !== null) { + win.location.reload(); + } + } - if (this.profilerFiles.length == 0) { - // Reload the iframe to prevent old data from being displayed - const win = this.iframe.contentWindow; - if (win !== null) { - win.location.reload(); - } + public load(match: Match | undefined): void { + this.profilerFiles = match !== undefined ? (match.profilerFiles || []) : []; + console.log(this.profilerFiles); - return; - } + this.profilerFiles = this.profilerFiles.map(file => { + const frames = file.frames.map(frame => ({ name: frame })); + + const profiles = file.profiles.map(profile => { + const hasEvents = profile.events.length > 0; + + return { + type: 'evented', + name: profile.name, + unit: 'none', + startValue: hasEvents ? profile.events[0].at : 0, + endValue: hasEvents ? profile.events[profile.events.length - 1].at : 0, + events: profile.events, + }; + }); + + return { frames, profiles }; + }); + + this.reset(); this.addSelectOption(this.teamSelector, 'Team A (red)', '0'); this.addSelectOption(this.teamSelector, 'Team B (blue)', '1'); @@ -67,8 +87,16 @@ export default class Profiler { } }; + this.notProfilingDiv = document.createElement("div"); + this.notProfilingDiv.className = "not-logging-div"; + this.notProfilingDiv.textContent = "Profiling is disabled."; + this.notProfilingDiv.hidden = this.conf.doProfiling; + base.appendChild(this.notProfilingDiv); + let p = document.createElement('p'); - p.innerText = 'If no teams are visible, make sure to run a game with profiling enabled by ticking the checkbox on the Runner tab or to load a replay of a game that had profiling enabled.'; + p.innerText = `If no teams are visible, make sure to run a game with profiling enabled by ticking the checkbox on the Runner tab or to load a replay of a game that had profiling enabled. \ + The match must completely load before profiling is visible. + `; base.appendChild(p); base.appendChild(this.createSidebarFormItem('Team', this.teamSelector)); @@ -107,10 +135,7 @@ export default class Profiler { `); if (this.currentTeamIndex > -1 && this.currentRobotIndex > -1) { - this.sendToIFrame('load', { - file: this.profilerFiles[this.currentTeamIndex], - robot: this.currentRobotIndex, - }); + this.onRobotChange(this.currentRobotIndex); } }; @@ -133,6 +158,8 @@ export default class Profiler { this.clearSelect(this.robotSelector); + // if (!this.profilerFiles[teamIndex]) return; + for (let i = 0; i < this.profilerFiles[teamIndex].profiles.length; i++) { const profile = this.profilerFiles[teamIndex].profiles[i]; @@ -145,10 +172,21 @@ export default class Profiler { private onRobotChange(newRobotId: number): void { this.currentRobotIndex = newRobotId; - this.sendToIFrame('load', { - file: this.profilerFiles[this.currentTeamIndex], - robot: this.currentRobotIndex, - }); + // if (!this.profilerFiles[this.currentTeamIndex]) return; + + const file = this.profilerFiles[this.currentTeamIndex]; + const robot = this.currentRobotIndex; + + if (this.conf.doProfiling) { + this.sendToIFrame('load', { + $schema: 'https://www.speedscope.app/file-format-schema.json', + activeProfileIndex: 0, + shared: { + frames: file.frames, + }, + profiles: [file.profiles[robot]], + }); + } } private sendToIFrame(type: string, payload: any) { @@ -157,4 +195,11 @@ export default class Profiler { frame.postMessage({ type, payload }, '*'); } } + + /** + * Sets indicator of whether logs are being processed. + */ + setNotProfilingDiv() { + this.notProfilingDiv.hidden = this.conf.doProfiling; + } } diff --git a/client/visualizer/src/sidebar/stats.ts b/client/visualizer/src/sidebar/stats.ts index 14b62e6f..3932d611 100644 --- a/client/visualizer/src/sidebar/stats.ts +++ b/client/visualizer/src/sidebar/stats.ts @@ -1,19 +1,30 @@ -import {Config} from '../config'; +import { Config } from '../config'; import * as cst from '../constants'; -import {AllImages} from '../imageloader'; -import {schema} from 'battlecode-playback'; +import { AllImages } from '../imageloader'; +import { schema } from 'battlecode-playback'; import Runner from '../runner'; +import Chart = require('chart.js'); const hex: Object = { 1: "#db3627", 2: "#4f7ee6" }; -export type StatBar = { +type VoteBar = { bar: HTMLDivElement, - label: HTMLSpanElement + vote: HTMLSpanElement, + bid: HTMLSpanElement }; +type BuffDisplay = { + numBuffs: HTMLSpanElement, + buff: HTMLSpanElement +} + +type IncomeDisplay = { + income: HTMLSpanElement +} + /** * Loads game stats: team name, votes, robot count * We make the distinction between: @@ -29,20 +40,37 @@ export default class Stats { private readonly tourIndexJump: HTMLInputElement; + private teamNameNodes: HTMLSpanElement[] = []; + // Key is the team ID - private robotTds: Object = {}; // Secondary key is robot type - private statBars: Map; - private statsTableElement: HTMLTableElement; + private robotImages: Map> = new Map(); // the robot image elements in the unit statistics display + private robotTds: Map>> = new Map(); - private relativeBarElement: HTMLElement; - private relBars: HTMLDivElement[]; + private voteBars: VoteBar[]; + private maxVotes: number; - private robotConsole: HTMLDivElement; + private incomeDisplays: IncomeDisplay[]; + private relativeBars: HTMLDivElement[]; + + private buffDisplays: BuffDisplay[]; + + private extraInfo: HTMLDivElement; + + private robotConsole: HTMLDivElement; + private runner: Runner; //needed for file uploading in tournament mode - + private conf: Config; + private tourneyUpload: HTMLDivElement; + + private incomeChart: Chart; + + private ECs: HTMLDivElement; + + private teamMapToTurnsIncomeSet: Map>; + // Note: robot types and number of teams are currently fixed regardless of // match info. Keep in mind if we ever change these, or implement this less // statically. @@ -52,13 +80,18 @@ export default class Stats { constructor(conf: Config, images: AllImages, runner: Runner) { this.conf = conf; this.images = images; + + for (const robot in this.images.robots) { + let robotImages: Array = this.images.robots[robot]; + this.robotImages[robot] = robotImages.map((image) => image.cloneNode() as HTMLImageElement); + } + this.div = document.createElement("div"); this.tourIndexJump = document.createElement("input"); this.runner = runner; let teamNames: Array = ["?????", "?????"]; let teamIDs: Array = [1, 2]; - this.statsTableElement = document.createElement("table"); this.initializeGame(teamNames, teamIDs); } @@ -69,9 +102,11 @@ export default class Stats { let teamHeader: HTMLDivElement = document.createElement("div"); teamHeader.className += ' teamHeader'; - let teamNameNode = document.createTextNode(teamName); + let teamNameNode = document.createElement('span'); + teamNameNode.innerHTML = teamName; teamHeader.style.backgroundColor = hex[inGameID]; teamHeader.appendChild(teamNameNode); + this.teamNameNodes[inGameID] = teamNameNode; return teamHeader; } @@ -85,115 +120,333 @@ export default class Stats { // Create the table row with the robot images let robotImages: HTMLTableRowElement = document.createElement("tr"); - + robotImages.appendChild(document.createElement("td")); // blank header + // Create the table row with the robot counts - let robotCounts: HTMLTableRowElement = document.createElement("tr"); + let robotCounts = {}; + + for (let value in this.robotTds[teamID]) { + robotCounts[value] = document.createElement("tr"); + const title = document.createElement("td"); + if (value === "conviction") title.innerHTML = "C"; + if (value === "influence") title.innerHTML = "I"; + robotCounts[value].appendChild(title); + } for (let robot of this.robots) { let robotName: string = cst.bodyTypeToString(robot); let tdRobot: HTMLTableCellElement = document.createElement("td"); tdRobot.className = "robotSpriteStats"; + tdRobot.style.height = "100px"; + tdRobot.style.width = "100px"; - const img = this.images.robots[robotName][inGameID]; - img.style.width = "64px"; - img.style.height = "64px"; + const img: HTMLImageElement = this.robotImages[robotName][inGameID]; + img.style.width = "60%"; + img.style.height = "60%"; tdRobot.appendChild(img); - robotImages.appendChild(tdRobot); - let tdCount: HTMLTableCellElement = this.robotTds[teamID][robot]; - robotCounts.appendChild(tdCount); + for (let value in this.robotTds[teamID]) { + let tdCount: HTMLTableCellElement = this.robotTds[teamID][value][robot]; + robotCounts[value].appendChild(tdCount); + if (robot === schema.BodyType.ENLIGHTENMENT_CENTER && value === "count") { + tdCount.style.fontWeight = "bold"; + tdCount.style.fontSize = "18px"; + } + } } table.appendChild(robotImages); - table.appendChild(robotCounts); + for (let value in this.robotTds[teamID]) { + table.appendChild(robotCounts[value]); + } return table; } - private statsTable(teamIDs: Array): HTMLTableElement { - const table = document.createElement("table"); - const bars = document.createElement("tr"); - const counts = document.createElement("tr"); - table.id = "stats-table"; - bars.id = "stats-bars"; - table.setAttribute("align", "center"); + private initVoteBars(teamIDs: Array) { + const voteBars: VoteBar[] = []; + teamIDs.forEach((teamID: number) => { + let votes = document.createElement("div"); + votes.className = "stat-bar"; + votes.style.backgroundColor = hex[teamID]; + let votesSpan = document.createElement("span"); + let bidSpan = document.createElement("span"); + votesSpan.innerHTML = "0"; + bidSpan.innerHTML = "0"; + // Store the stat bars + voteBars[teamID] = { + bar: votes, + vote: votesSpan, + bid: bidSpan + }; + }); + return voteBars; + } - const title = document.createElement('td'); - title.colSpan= 2; - const label = document.createElement('h3'); - label.innerText = 'Votes'; + private getVoteBarElement(teamIDs: Array): HTMLElement { + const votesDiv = document.createElement('div'); + + const box = document.createElement('div'); + box.className = "votes-box"; + + const title = document.createElement('div'); + title.className = "stats-header"; + + const bars = document.createElement('div'); + bars.id = "vote-bars"; + bars.appendChild(document.createElement('div')); + + const votes = document.createElement('div'); + votes.className = "votes-info"; + const bids = document.createElement('div'); + bids.className = "votes-info"; + + title.innerHTML = "Voting"; + + const votesTitle = document.createElement('div'); + votesTitle.innerHTML = "Votes"; + votes.appendChild(votesTitle); + + const bidsTitle = document.createElement('div'); + bidsTitle.innerHTML = "Bid"; + bids.appendChild(bidsTitle); + + // build table teamIDs.forEach((id: number) => { - const bar = document.createElement("td"); - bar.height = "150"; - bar.vAlign = "bottom"; - // TODO: figure out if statbars.get(id) can actually be null?? - bar.appendChild(this.statBars.get(id)!.votes.bar); - bars.appendChild(bar); - - const count = document.createElement("td"); - // TODO: figure out if statbars.get(id) can actually be null?? - count.appendChild(this.statBars.get(id)!.votes.label); - counts.appendChild(count); + + const vote = document.createElement('div'); + vote.appendChild(this.voteBars[id].vote); + votes.appendChild(vote); + + const bid = document.createElement('div'); + bid.appendChild(this.voteBars[id].bid); + bids.appendChild(bid); + + bars.appendChild(this.voteBars[id].bar); }); - title.appendChild(label); - table.appendChild(title); - table.appendChild(bars); - table.appendChild(counts); - return table; + votesDiv.appendChild(title); + box.appendChild(bids); + box.appendChild(votes); + box.appendChild(bars); + votesDiv.appendChild(box); + return votesDiv; } - private relativeBar(teamIds: Array): HTMLElement { + private initRelativeBars(teamIDs: Array) { + const relativeBars: HTMLDivElement[] = []; + teamIDs.forEach((id: number) => { + const bar = document.createElement("div"); + bar.style.backgroundColor = hex[id]; + bar.style.width = `50%`; + bar.className = "influence-bar"; + bar.innerText = "0"; + + relativeBars[id] = bar; + }); + return relativeBars; + } + + private getRelativeBarsElement(teamIDs: Array): HTMLElement { const div = document.createElement("div"); div.setAttribute("align", "center"); - this.relBars = []; + div.id = "relative-bars"; - const frame = document.createElement("div"); - frame.style.width = "250px"; - frame.style.height = "30px"; + const label = document.createElement('div'); + label.className = "stats-header"; + label.innerText = 'Total Influence'; - teamIds.forEach((id: number) => { - const bar = document.createElement("div"); - bar.style.backgroundColor = hex[id]; - bar.style.height = frame.style.height; - bar.style.width = `${100*id}px`; + const frame = document.createElement("div"); + frame.style.width = "90%"; - this.relBars[id] = bar; - frame.appendChild(bar); + teamIDs.forEach((id: number) => { + frame.appendChild(this.relativeBars[id]); }); + div.appendChild(label); div.appendChild(frame); return div; } + private initBuffDisplays(teamIDs: Array) { + const buffDisplays: BuffDisplay[] = []; + teamIDs.forEach((id: number) => { + const numBuffs = document.createElement("sup"); + const buff = document.createElement("span"); + numBuffs.style.color = hex[id]; + buff.style.color = hex[id]; + buff.style.fontWeight = "bold"; + numBuffs.textContent = "0"; + buff.textContent = "1.000"; + buffDisplays[id] = {numBuffs: numBuffs, buff: buff}; + }); + return buffDisplays; + } + private initIncomeDisplays(teamIDs: Array) { + const incomeDisplays: IncomeDisplay[] = []; + teamIDs.forEach((id: number) => { + const income = document.createElement("span"); + income.style.color = hex[id]; + income.style.fontWeight = "bold"; + income.textContent = "1"; + incomeDisplays[id] = {income: income}; + }); + return incomeDisplays; + } + + private getBuffDisplaysElement(teamIDs: Array): HTMLElement { + const div = document.createElement("div"); + div.id = "buffs"; + + const label = document.createElement('div'); + label.className = "stats-header"; + label.innerText = 'Buffs'; + div.appendChild(label); + + teamIDs.forEach((id: number) => { + const buffDiv = document.createElement("div"); + buffDiv.className = "buff-div"; + // cell.appendChild(document.createTextNode("1.001")); + // cell.appendChild(this.buffDisplays[id].numBuffs); + // cell.appendChild(document.createTextNode(" = ")); + buffDiv.appendChild(this.buffDisplays[id].buff); + div.appendChild(buffDiv); + }); + + return div; + } + + private getIncomeDisplaysElement(teamIDs: Array): HTMLElement { + const table = document.createElement("table"); + table.id = "income-table"; + table.style.width = "100%"; + + const title = document.createElement('td'); + title.colSpan = 2; + const label = document.createElement('div'); + label.className = "stats-header"; + label.innerText = 'Total Income Per Turn'; + + const row = document.createElement("tr"); + + teamIDs.forEach((id: number) => { + const cell = document.createElement("td"); + // cell.appendChild(document.createTextNode("1.001")); + // cell.appendChild(this.buffDisplays[id].numBuffs); + // cell.appendChild(document.createTextNode(" = ")); + cell.appendChild(this.incomeDisplays[id].income); + row.appendChild(cell); + }); + + title.appendChild(label); + table.appendChild(title); + table.appendChild(row); + + return table; + } + + private getIncomeDominationGraph() { + const canvas = document.createElement("canvas"); + canvas.id = "myChart"; + return canvas; + } + + private getECDivElement() { + const div = document.createElement('div'); + const label = document.createElement('div'); + label.className = "stats-header"; + label.innerText = 'EC Control'; + div.appendChild(label); + div.appendChild(this.ECs); + return div; + } + + // private drawBuffsGraph(ctx: CanvasRenderingContext2D, upto: number) { + // ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + // // draw axes + // ctx.save(); + // ctx.strokeStyle = "#000000"; + // ctx.lineWidth = 0.02; + // ctx.moveTo(0, 1); + // ctx.lineTo(0, 0); + // ctx.stroke(); + // ctx.moveTo(0, 1); + // ctx.lineTo(1, 1); + // ctx.stroke(); + + // const xscale = 1 / upto; + // const yscale = 1 / cst.buffFactor(upto); + + // for (let i = 0; i <= upto; i++) { + // ctx.moveTo(i * xscale, 1 - cst.buffFactor(i) * yscale); + // ctx.lineTo(i * xscale, 1 - cst.buffFactor(i + 1) * yscale); + // } + // ctx.stroke(); + + // ctx.restore(); + // } + + // private plotBuff(ctx: CanvasRenderingContext2D, upto: number, buff1: number, buff2: number) { + // const xscale = 1 / upto; + // const yscale = 1 / cst.buffFactor(upto); + + // ctx.save(); + + // ctx.fillStyle = hex[1]; + // ctx.font = "0.1px Comic Sans MS"; + // // ctx.moveTo(buff1*xscale, cst.buffFactor(buff1)*yscale); + // ctx.fillText("R", buff1 * xscale, 1 - cst.buffFactor(buff1) * yscale + 0.08); + // // ctx.arc(buff1*xscale, cst.buffFactor(buff1)*yscale, 0.02, 0, 2*Math.PI); + // // ctx.fill(); + + // ctx.fillStyle = hex[2]; + // ctx.fillText("B", buff2 * xscale, 1 - cst.buffFactor(buff2) * yscale - 0.04); + + // ctx.moveTo(buff2 * xscale, cst.buffFactor(buff2) * yscale - 0.05); + // // ctx.arc(buff1*xscale, cst.buffFactor(buff2)*yscale - 0.05, 0.02, 0, 2*Math.PI); + // // ctx.fill(); + // ctx.restore(); + // } + /** * Clear the current stats bar and reinitialize it with the given teams. */ - initializeGame(teamNames: Array, teamIDs: Array){ + initializeGame(teamNames: Array, teamIDs: Array) { // Remove the previous match info while (this.div.firstChild) { this.div.removeChild(this.div.firstChild); } - this.robotTds = {}; - this.statBars = new Map(); + this.voteBars = []; + this.relativeBars = []; + this.maxVotes = 750; + this.teamMapToTurnsIncomeSet = new Map(); - if(this.conf.tournamentMode){ + this.div.appendChild(document.createElement("br")); + if (this.conf.tournamentMode) { // FOR TOURNAMENT + this.tourneyUpload = document.createElement('div'); + let uploadButton = this.runner.getUploadButton(); let tempdiv = document.createElement("div"); tempdiv.className = "upload-button-div"; tempdiv.appendChild(uploadButton); - this.div.appendChild(tempdiv); + this.tourneyUpload.appendChild(tempdiv); // add text input field this.tourIndexJump.type = "text"; this.tourIndexJump.onkeyup = (e) => { this.tourIndexJumpFun(e) }; this.tourIndexJump.onchange = (e) => { this.tourIndexJumpFun(e) }; - this.div.appendChild(this.tourIndexJump); + this.tourneyUpload.appendChild(this.tourIndexJump); + + this.div.appendChild(this.tourneyUpload); } - + + this.extraInfo = document.createElement('div'); + this.extraInfo.className = "extra-info"; + this.div.appendChild(this.extraInfo); + // Populate with new info // Add a section to the stats bar for each team in the match for (var index = 0; index < teamIDs.length; index++) { @@ -201,39 +454,25 @@ export default class Stats { let teamID = teamIDs[index]; let teamName = teamNames[index]; let inGameID = index + 1; // teams start at index 1 + this.robotTds[teamID] = new Map(); // A div element containing all stats information about this team let teamDiv = document.createElement("div"); // Create td elements for the robot counts and store them in robotTds // so we can update these robot counts later; maps robot type to count - let initialRobotCount: Object = {}; - for (let robot of this.robots) { - let td: HTMLTableCellElement = document.createElement("td"); - td.innerHTML = "0"; - initialRobotCount[robot] = td; - } - this.robotTds[teamID] = initialRobotCount; - - // Create the stat bar for votes - let votes = document.createElement("div"); - votes.className = "stat-bar"; - votes.style.backgroundColor = hex[inGameID]; - let votesSpan = document.createElement("span"); - votesSpan.innerHTML = "0"; - - // Store the stat bars - this.statBars.set(teamID, { - votes: { - bar: votes, - label: votesSpan + for (let value of ["count", "conviction", "influence"]) { + this.robotTds[teamID][value] = new Map(); + for (let robot of this.robots) { + let td: HTMLTableCellElement = document.createElement("td"); + td.innerHTML = "0"; + this.robotTds[teamID][value][robot] = td; } - }); + } // Add the team name banner and the robot count table teamDiv.appendChild(this.teamHeaderNode(teamName, inGameID)); teamDiv.appendChild(this.robotTable(teamID, inGameID)); - teamDiv.appendChild(document.createElement("br")); this.div.appendChild(teamDiv); } @@ -241,20 +480,77 @@ export default class Stats { this.div.appendChild(document.createElement("hr")); // Add stats table - this.statsTableElement.remove(); - this.statsTableElement = this.statsTable(teamIDs); - this.div.appendChild(this.statsTableElement); - - // TODO relative bar - this.relativeBarElement = this.relativeBar(teamIDs); - // this.div.appendChild(this.relativeBarElement); - // console.log(this.relativeBarElement) + this.voteBars = this.initVoteBars(teamIDs); + const voteBarsElement = this.getVoteBarElement(teamIDs); + this.div.appendChild(voteBarsElement); + + this.relativeBars = this.initRelativeBars(teamIDs); + const relativeBarsElement = this.getRelativeBarsElement(teamIDs); + this.div.appendChild(relativeBarsElement); + + this.buffDisplays = this.initBuffDisplays(teamIDs); + const buffDivsElement = this.getBuffDisplaysElement(teamIDs); + this.div.appendChild(buffDivsElement); + + this.incomeDisplays = this.initIncomeDisplays(teamIDs); + const incomeElement = this.getIncomeDisplaysElement(teamIDs); + this.div.appendChild(incomeElement); + + const canvasElement = this.getIncomeDominationGraph(); + this.div.appendChild(canvasElement); + this.incomeChart = new Chart(canvasElement, { + type: 'line', + data: { + datasets: [{ + label: 'Red', + data: [], + backgroundColor: 'rgba(255, 99, 132, 0)', + borderColor: 'rgb(219, 54, 39)', + pointRadius: 0, + }, + { + label: 'Blue', + data: [], + backgroundColor: 'rgba(54, 162, 235, 0)', + borderColor: 'rgb(79, 126, 230)', + pointRadius: 0, + }] + }, + options: { + aspectRatio: 1.5, + scales: { + xAxes: [{ + type: 'linear', + ticks: { + beginAtZero: true + }, + scaleLabel: { + display: true, + labelString: "Turn" + } + }], + yAxes: [{ + type: 'linear', + ticks: { + beginAtZero: true + } + }] + } + } + }); + + this.ECs = document.createElement("div"); + this.ECs.style.height = "100px"; + this.ECs.style.display = "flex"; + this.div.appendChild(this.getECDivElement()); + + this.div.appendChild(document.createElement("br")); } tourIndexJumpFun(e) { - if (e.keyCode === 13){ - var h = +this.tourIndexJump.value.trim().toLowerCase(); - this.runner.seekTournament(h-1); + if (e.keyCode === 13) { + var h = +this.tourIndexJump.value.trim().toLowerCase(); + this.runner.seekTournament(h - 1); } } @@ -262,20 +558,93 @@ export default class Stats { * Change the robot count on the stats bar */ setRobotCount(teamID: number, robotType: schema.BodyType, count: number) { - let td: HTMLTableCellElement = this.robotTds[teamID][robotType]; + let td: HTMLTableCellElement = this.robotTds[teamID]["count"][robotType]; td.innerHTML = String(count); } + /** + * Change the robot conviction on the stats bar + */ + setRobotConviction(teamID: number, robotType: schema.BodyType, conviction: number, totalConviction: number) { + let td: HTMLTableCellElement = this.robotTds[teamID]["conviction"][robotType]; + td.innerHTML = String(conviction); + + const robotName: string = cst.bodyTypeToString(robotType); + let img = this.robotImages[robotName][teamID]; + + const size = (55 + 45 * conviction / totalConviction); + img.style.width = size + "%"; + img.style.height = size + "%"; + } + + /** + * Change the robot influence on the stats bar + */ + setRobotInfluence(teamID: number, robotType: schema.BodyType, influence: number) { + let td: HTMLTableCellElement = this.robotTds[teamID]["influence"][robotType]; + td.innerHTML = String(influence); + } + /** * Change the votes of the given team */ setVotes(teamID: number, count: number) { // TODO: figure out if statbars.get(id) can actually be null?? - const statBar: StatBar = this.statBars.get(teamID)!.votes; - statBar.label.innerText = String(count); - const maxVotes = 1500; - statBar.bar.style.height =`${Math.min(100 * count / maxVotes, 100)}%`; + const statBar: VoteBar = this.voteBars[teamID]; + statBar.vote.innerText = String(count); + this.maxVotes = Math.max(this.maxVotes, count); + statBar.bar.style.width = `${Math.min(100 * count / this.maxVotes, 100)}%`; + + // TODO add reactions to relative bars + // TODO get total votes to get ratio + // this.relBars[teamID].width; + + // TODO winner gets star? + // if (this.images.star.parentNode === statBar.bar) { + // this.images.star.remove(); + // } + } + + setTeamInfluence(teamID: number, influence: number, totalInfluence: number) { + const relBar: HTMLDivElement = this.relativeBars[teamID]; + relBar.innerText = String(influence); + if (totalInfluence == 0) relBar.style.width = '50%'; + else relBar.style.width = String(Math.round(influence * 100 / totalInfluence)) + "%"; + } + + setBuffs(teamID: number, numBuffs: number) { + //this.buffDisplays[teamID].numBuffs.textContent = String(numBuffs); + this.buffDisplays[teamID].buff.textContent = String(cst.buffFactor(numBuffs).toFixed(3)); + this.buffDisplays[teamID].buff.style.fontSize = 14 * Math.sqrt(Math.min(9, cst.buffFactor(numBuffs))) + "px"; + } + + setIncome(teamID: number, income: number, turn: number) { + this.incomeDisplays[teamID].income.textContent = String(income); + if (!this.teamMapToTurnsIncomeSet.has(teamID)) { + this.teamMapToTurnsIncomeSet.set(teamID, new Set()); + } + let teamTurnsIncomeSet = this.teamMapToTurnsIncomeSet.get(teamID); + + if (!teamTurnsIncomeSet!.has(turn)) { + //@ts-ignore + this.incomeChart.data.datasets![teamID - 1].data?.push({y:income, x: turn}); + this.incomeChart.data.datasets?.forEach((d) => { + d.data?.sort((a, b) => a.x - b.x); + }); + teamTurnsIncomeSet?.add(turn); + this.incomeChart.update(); + } + } + setWinner(teamID: number, teamNames: Array, teamIDs: Array) { + const name = teamNames[teamIDs.indexOf(teamID)]; + this.teamNameNodes[teamID].innerHTML = "" + name + " " + `🌟`; + } + + setBid(teamID: number, bid: number) { + // TODO: figure out if statbars.get(id) can actually be null?? + const statBar: VoteBar = this.voteBars[teamID]; + statBar.bid.innerText = String(bid); // TODO add reactions to relative bars // TODO get total votes to get ratio // this.relBars[teamID].width; @@ -285,4 +654,30 @@ export default class Stats { // this.images.star.remove(); // } } + + setExtraInfo(info: string) { + this.extraInfo.innerHTML = info; + } + + hideTourneyUpload() { + console.log(this.tourneyUpload); + this.tourneyUpload.style.display = this.tourneyUpload.style.display === "none" ? "" : "none"; + } + + resetECs() { + // while (this.ECs.lastChild) this.ECs.removeChild(this.ECs.lastChild); + // console.log(this.ECs); + this.ECs.innerHTML = ""; + } + + addEC(teamID: number) { + const div = document.createElement("div"); + div.style.width = "35px"; + div.style.height = "35px"; + const img = this.images.robots["enlightenmentCenter"][teamID].cloneNode() as HTMLImageElement; + img.style.width = "64px"; + img.style.height = "64px"; // update dynamically later + div.appendChild(img); + this.ECs.appendChild(div); + } } diff --git a/client/visualizer/src/static/css/style.css b/client/visualizer/src/static/css/style.css index 5c617de8..9cd53dec 100644 --- a/client/visualizer/src/static/css/style.css +++ b/client/visualizer/src/static/css/style.css @@ -103,15 +103,15 @@ form { } #gamearea canvas { - max-height: calc(100vh - 75px); - max-width: calc(100vw - 345px); + max-height: calc(100vh - 79px); + max-width: 99.5%; margin-top:auto; margin-bottom:auto; } #gamearea iframe { - height: calc(100vh - 75px); - width: calc(100vw - 345px); + height: 100%; + width: 100%; border: 0; } @@ -121,15 +121,23 @@ form { justify-content: center; /* Horizontal center alignment */ } +#gamearea:empty { + display: none; +} + #canvas-wrapper { - margin-top: -75px; - min-width: 569px; + width: 100%; + height: 100%; text-align: center; - margin-right: 295px; + display: flex; + align-items: center; + justify-content: center; + /* margin-right: 325px; + margin-left: 10px; */ } #export { - font-size: 24px; + font-size: 16px; width: 90%; } @@ -166,6 +174,17 @@ input[type='file'] { width: 100%; } +/* TODO: rename this class */ +.not-logging-div { + color: red; + margin-top: 0.5em; + font-size: 14px; +} + +.extra-info { + font-size: 17px; +} + .custom-button:active, button:target { background-color: #8c8c8c; } @@ -173,12 +192,37 @@ input[type='file'] { outline: none; } +.info { + font-size: 11px; + line-height: 20px; + position: relative; + top: -5px; +} + +.timeline { + position: relative; + top: 4px; +} + +.info-name { + padding-left: 1px; +} + +.info-num { + font-family: Monospace; + border-radius: 8px; + background-color: #333; + color: #46ff00; + padding: 3px; + margin: 0px 1px; +} + #gamearea { - width: 100%; - height: 100%; + width: calc(100vw - 405px); + height: calc(100vh - 75px); position: fixed; top: 75px; - left: 320px; + left: 405px; background: rgb(39, 39, 39); } @@ -207,6 +251,10 @@ input[type='file'] { margin-bottom: 6px; } +#export { + background-color: rebeccapurple; +} + .checkbox { position: absolute; visibility: hidden; @@ -233,8 +281,7 @@ label{ border-radius: 10px; } -input { - border: 2px solid rgb(0,123,255); +input, select { padding: 5px; margin: 5px; display: inline-block; @@ -242,8 +289,8 @@ input { #baseDiv { width: 100%; - height: 55px; - margin-left: 335px; + height: 65px; + margin-left: 410px; position: fixed; z-index: 0.5; top: 0; @@ -256,11 +303,22 @@ input { #timelineCanvas { height: 32px; - width: 400px; + width: 300px; background-color: #999; display: inline-block; } +.stats-header { + margin: 8px auto 8px auto; + width: fit-content; + font-size: 1.17em; + font-weight: bold; + padding: 5px; + border-style: solid; + border-radius: 5px; + border-width: 1px; +} + h4 { margin: 10px; } @@ -276,7 +334,7 @@ h4 { #sidebar { /* Positioning */ height: 100%; - width: 325px; + width: 400px; position: fixed; z-index: 1; top: 0; @@ -459,6 +517,10 @@ h4 { color: red; } +.specialLog{ + color: yellow; +} + .slide-control { width: 200px; height: 8px; @@ -499,24 +561,61 @@ h4 { text-align: center; z-index: -1; overflow: hidden; - margin-top: -39px; - margin-left: -161px; } -#stats-table { +.votes-box { + display: flex; + padding-right: 5px; + padding-left: 5px; +} + +.votes-box > div { + margin-left: 5px; + margin-right: 5px; +} + +.votes-box > div > div { + height: 25px; + line-height: 25px; +} + +#vote-bars { width: 100%; } -#stats-table td { - width: 25%; - padding: .2em; +#relative-bars { + width: 50%; + float: left; } -.stat-bar { - height: 100%; +.influence-bar { + height: 30px; + line-height: 30px; + color: white; + min-width: fit-content; +} + +#buffs-table { + border-collapse: collapse; } +#buffs { + width: 50%; + float: left; +} + +.buff-div { + height: 30px; + line-height: 30px +} + + + #star { margin-top: 1em; width: 40px; } + +#change-tiles input { + width: 30%; +} diff --git a/client/visualizer/src/static/css/tournament.css b/client/visualizer/src/static/css/tournament.css index c4845796..fb950086 100644 --- a/client/visualizer/src/static/css/tournament.css +++ b/client/visualizer/src/static/css/tournament.css @@ -5,7 +5,8 @@ top: 0px; left: 0px; - background-color: black; + background-color: rebeccapurple; + opacity: 100%; color: white; font-size: 50px; text-align: center; @@ -14,8 +15,7 @@ .blackout-container { margin-top: 10vh; - margin-left: 16vw; - margin-right: 16vw; + height: 100% } .tournament-header { @@ -61,3 +61,46 @@ font-size: 40px; margin-bottom: 32px; } + +#team1, #team2, #versus, #winner { + position: absolute; + width: fit-content; + text-align: center; + font-size: 80px; + font-family: "Luminari"; + transform: translateX(-50%); +} + +#team1 { + top: 36%; + left: 35%; + font-weight: bold; + transform: translateX(-1000px); + animation: slide-in 0.6s forwards; + -webkit-animation: slide-in 0.6s forwards; +} + +#team2 { + top: 54%; + left: 65%; + font-weight: bold; + transform: translateX(1000px); + animation: slide-in 0.6s forwards; + -webkit-animation: slide-in 0.6s forwards; +} + +#versus { + top: 45%; + left: 50%; + color: orange; +} + +#winner { + top: 45%; + left: 50%; + font-weight: bold; +} + +@keyframes slide-in { + 100% { transform: translateX(-50%); } +} \ No newline at end of file diff --git a/client/visualizer/src/static/img/effects/expose/expose_empty.png b/client/visualizer/src/static/img/effects/expose/expose_empty.png index c5f9a780..5971e70e 100644 Binary files a/client/visualizer/src/static/img/effects/expose/expose_empty.png and b/client/visualizer/src/static/img/effects/expose/expose_empty.png differ diff --git a/client/visualizer/src/static/img/effects/expose/expose_empty_small.png b/client/visualizer/src/static/img/effects/expose/expose_empty_small.png new file mode 100644 index 00000000..c5f9a780 Binary files /dev/null and b/client/visualizer/src/static/img/effects/expose/expose_empty_small.png differ diff --git a/client/visualizer/src/static/img/tiles/terrain.png b/client/visualizer/src/static/img/tiles/terrain.png new file mode 100644 index 00000000..00e7b3fa Binary files /dev/null and b/client/visualizer/src/static/img/tiles/terrain.png differ diff --git a/client/visualizer/webpack.config.js b/client/visualizer/webpack.config.js index 2d02d269..d900a3a2 100644 --- a/client/visualizer/webpack.config.js +++ b/client/visualizer/webpack.config.js @@ -58,7 +58,6 @@ module.exports = function(env) { mode: "development", devtool: 'source-map', plugins: [ - new webpack.HotModuleReplacementPlugin(), new webpack.LoaderOptionsPlugin({ minimize: false, debug: true diff --git a/engine/README.md b/engine/README.md index 24df430f..88e948f8 100644 --- a/engine/README.md +++ b/engine/README.md @@ -36,5 +36,6 @@ necessary updates. * `instrumenter`: handles instrumenting player code so that it is isolated, deterministic, and counts bytecodes. * `instrumenter/bytecode`: the actual bytecode-modification code. * `instrumenter/inject`: classes we replace various parts of java.lang with. Also contains RobotMonitor, which counts bytecodes. + * `instrumenter/profiler`: contains the bytecode profiler. * `server`: contains the main class that starts up the engine. * `serial`: contains some information that gets sent to the client as part of every match, such as who won. diff --git a/engine/build.gradle b/engine/build.gradle index d48c3301..28e9c605 100644 --- a/engine/build.gradle +++ b/engine/build.gradle @@ -42,7 +42,7 @@ dependencies { [group: 'org.ow2.asm', name: 'asm-tree', version: '5.0.4'], // Flatbuffers - [group: 'com.github.davidmoten', name: 'flatbuffers-java', version: '1.4.0.1'], + [group: 'com.google.flatbuffers', name: 'flatbuffers-java', version: '1.11.0'], // Websockets [group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.3.0'], diff --git a/engine/src/main/battlecode/common/GameConstants.java b/engine/src/main/battlecode/common/GameConstants.java index 5f81d177..a9d18d7e 100644 --- a/engine/src/main/battlecode/common/GameConstants.java +++ b/engine/src/main/battlecode/common/GameConstants.java @@ -37,8 +37,8 @@ public class GameConstants { /** The bytecode penalty that is imposed each time an exception is thrown. */ public static final int EXCEPTION_BYTECODE_PENALTY = 500; - /** Maximum ID a Robot will have */ - public static final int MAX_ROBOT_ID = 32000; + ///** Maximum ID a Robot will have */ + //public static final int MAX_ROBOT_ID = 32000; Cannot be guaranteed in Battlecode 2021. // ********************************* // ****** COOLDOWNS **************** @@ -53,7 +53,7 @@ public class GameConstants { public static final int EMPOWER_TAX = 10; /** The buff factor from exposing Slanderers. */ - public static final double EXPOSE_BUFF_FACTOR = 1.01; + public static final double EXPOSE_BUFF_FACTOR = 0.001; /** The number of rounds a buff is applied. */ public static final int EXPOSE_BUFF_NUM_ROUNDS = 50; @@ -61,6 +61,12 @@ public class GameConstants { /** The number of rounds Slanderers generate influence. */ public static final int EMBEZZLE_NUM_ROUNDS = 50; + /** The scale factor in the Slanderer embezzle influence formula. */ + public static final float EMBEZZLE_SCALE_FACTOR = 0.03f; + + /** The exponential decay factor in the Slanderer embezzle influence formula. */ + public static final float EMBEZZLE_DECAY_FACTOR = 0.001f; + /** The number of rounds before Slanderers turns into Politicians. */ public static final int CAMOUFLAGE_NUM_ROUNDS = 300; @@ -70,8 +76,8 @@ public class GameConstants { /** The passive influence ratio for Enlightenment Centers. To multiply by sqrt(roundNum). */ public static final float PASSIVE_INFLUENCE_RATIO_ENLIGHTENMENT_CENTER = 0.2f; - /** The passive influence ratio for Slanderers. To multiply by robot influence. */ - public static final float PASSIVE_INFLUENCE_RATIO_SLANDERER = 0.05f; + /** Maximum allowable robot influence. */ + public static final int ROBOT_INFLUENCE_LIMIT = 100000000; /** The minimum allowable flag value. */ public static final int MIN_FLAG_VALUE = 0; @@ -87,5 +93,5 @@ public class GameConstants { public static final int GAME_DEFAULT_SEED = 6370; /** The maximum number of rounds in a game. **/ - public static final int GAME_MAX_NUMBER_OF_ROUNDS = 3000; + public static final int GAME_MAX_NUMBER_OF_ROUNDS = 1500; } diff --git a/engine/src/main/battlecode/common/RobotController.java b/engine/src/main/battlecode/common/RobotController.java index b76369ce..07a93ec3 100644 --- a/engine/src/main/battlecode/common/RobotController.java +++ b/engine/src/main/battlecode/common/RobotController.java @@ -259,8 +259,8 @@ public strictfp interface RobotController { /** * Returns all robots of a given team that can be sensed within a certain - * radius of a specified location. The objects are returned in order of - * increasing distance from the specified center. + * radius of a specified location. The objects are returned in no particular + * order. * * @param center center of the given search radius * @param radiusSquared return robots this distance away from the center of @@ -302,8 +302,8 @@ public strictfp interface RobotController { /** * Returns all robots of a given team that can be detected within a certain - * radius of a specified location. The objects are returned in order of - * increasing distance from the specified center. + * radius of a specified location. The objects are returned in no particular + * order. * * @param center center of the given search radius * @param radiusSquared return robots this distance away from the center of @@ -477,6 +477,19 @@ public strictfp interface RobotController { */ boolean canExpose(MapLocation loc); + /** + * Tests whether the robot can expose a given robot ID. + * Checks that the robot is a muckraker, that the target robot is within + * action radius of the muckraker, that the target robot is an enemy + * slanderer, and that there are no cooldown turns remaining. + * + * @param id the robot ID being exposed + * @return whether it is possible to expose that robot on that round + * + * @battlecode.doc.costlymethod + */ + boolean canExpose(int id); + /** * Exposes a slanderer at a given location. * The slanderer will be destroyed, and all attempts to empower by friendly @@ -488,6 +501,17 @@ public strictfp interface RobotController { */ void expose(MapLocation loc) throws GameActionException; + /** + * Exposes a slanderer with given id. + * The slanderer will be destroyed, and all attempts to empower by friendly + * Politicians will be temporarily buffed by a multiplicative factor. + * + * @throws GameActionException if conditions for exposing are not all satisfied + * + * @battlecode.doc.costlymethod + */ + void expose(int id) throws GameActionException; + // ************************************** // **** ENLIGHTENMENT CENTER METHODS **** diff --git a/engine/src/main/battlecode/common/RobotType.java b/engine/src/main/battlecode/common/RobotType.java index 5a96a917..4e2e8952 100644 --- a/engine/src/main/battlecode/common/RobotType.java +++ b/engine/src/main/battlecode/common/RobotType.java @@ -5,7 +5,7 @@ */ public enum RobotType { - // spawnSource, convictionRatio, actionCooldown, actionRadiusSquared, sensorRadiusSquared, detectionRadiusSquared, bytecodeLimit + // spawnSource, convictionRatio, actionCooldown, initialCooldown, actionRadiusSquared, sensorRadiusSquared, detectionRadiusSquared, bytecodeLimit /** * Enlightenment Centers produce various types of robots, as well as * passively generate influence and bid for votes each round. Can be @@ -13,8 +13,8 @@ public enum RobotType { * * @battlecode.doc.robottype */ - ENLIGHTENMENT_CENTER (null, 1, 2, 2, 40, 40, 12000), - // SS CR AC AR SR DR BL + ENLIGHTENMENT_CENTER (null, 1, 2, 0, 2, 40, 40, 20000), + // SS CR AC IC AR SR DR BL /** * Politicians Empower adjacent units, strengthening friendly robots, * converting enemy Politicians and Enlightenment Centers, and destroying @@ -22,8 +22,8 @@ public enum RobotType { * * @battlecode.doc.robottype */ - POLITICIAN (ENLIGHTENMENT_CENTER, 1, 1, 9, 25, 25, 6000), - // SS CR AC AR SR DR BL + POLITICIAN (ENLIGHTENMENT_CENTER, 1, 1, 10, 9, 25, 25, 15000), + // SS CR AC IC AR SR DR BL /** * Slanderers passively generate influence for their parent Enlightenment * Center each round. They are camoflauged as Politicians to enemy units. @@ -31,16 +31,16 @@ public enum RobotType { * * @battlecode.doc.robottype */ - SLANDERER (ENLIGHTENMENT_CENTER, 1, 2, 0, 20, 20, 3000), - // SS CR AC AR SR DR BL + SLANDERER (ENLIGHTENMENT_CENTER, 1, 2, 0, 0, 20, 20, 7500), + // SS CR AC IC AR SR DR BL /** * Muckrakers search the map for enemy Slanderers to Expose, which destroys * the Slanderer and gives a buff to their team. * * @battlecode.doc.robottype */ - MUCKRAKER (ENLIGHTENMENT_CENTER, 0.7f, 1.5f, 12, 30, 40, 9000), - // SS CR AC AR SR DR BL + MUCKRAKER (ENLIGHTENMENT_CENTER, 0.7f, 1.5f, 10, 12, 30, 40, 15000), + // SS CR AC IC AR SR DR BL ; /** @@ -60,6 +60,11 @@ public enum RobotType { */ public final float actionCooldown; + /** + * Initial cooldown turns when a robot is built. + */ + public final float initialCooldown; + /** * Radius squared range of robots' abilities. For Politicians, this is * the AoE range of their Empower ability. For Muckrakers, this is @@ -200,19 +205,22 @@ public int getPassiveInfluence(int robotInfluence, int roundsAlive, int roundNum return (int) Math.ceil(GameConstants.PASSIVE_INFLUENCE_RATIO_ENLIGHTENMENT_CENTER * Math.sqrt(roundNum)); case SLANDERER: if (roundsAlive <= GameConstants.EMBEZZLE_NUM_ROUNDS) - return (int) (GameConstants.PASSIVE_INFLUENCE_RATIO_SLANDERER * robotInfluence); + return (int) (robotInfluence * + (1.0 / GameConstants.EMBEZZLE_NUM_ROUNDS + + GameConstants.EMBEZZLE_SCALE_FACTOR * Math.exp(-GameConstants.EMBEZZLE_DECAY_FACTOR * robotInfluence))); return 0; default: return 0; } } - RobotType(RobotType spawnSource, float convictionRatio, float actionCooldown, + RobotType(RobotType spawnSource, float convictionRatio, float actionCooldown, float initialCooldown, int actionRadiusSquared, int sensorRadiusSquared, int detectionRadiusSquared, int bytecodeLimit) { this.spawnSource = spawnSource; this.convictionRatio = convictionRatio; this.actionCooldown = actionCooldown; + this.initialCooldown = initialCooldown; this.actionRadiusSquared = actionRadiusSquared; this.sensorRadiusSquared = sensorRadiusSquared; this.detectionRadiusSquared = detectionRadiusSquared; diff --git a/engine/src/main/battlecode/doc/RobotTypeTaglet.java b/engine/src/main/battlecode/doc/RobotTypeTaglet.java index 28800332..17ee8ed1 100644 --- a/engine/src/main/battlecode/doc/RobotTypeTaglet.java +++ b/engine/src/main/battlecode/doc/RobotTypeTaglet.java @@ -190,7 +190,7 @@ public String docFor(String variant) { if (rt.spawnSource != null) { builder.append("
"); appendField(builder, rt, "spawnSource"); - appendField(builder, rt, "cost"); + // appendField(builder, rt, "cost"); Cost is variable in 2021 } if (rt.bytecodeLimit != 0) { diff --git a/engine/src/main/battlecode/instrumenter/SandboxedRobotPlayer.java b/engine/src/main/battlecode/instrumenter/SandboxedRobotPlayer.java index 1d74068a..18e22f6f 100644 --- a/engine/src/main/battlecode/instrumenter/SandboxedRobotPlayer.java +++ b/engine/src/main/battlecode/instrumenter/SandboxedRobotPlayer.java @@ -2,6 +2,7 @@ import battlecode.common.RobotController; import battlecode.common.Team; +import battlecode.instrumenter.profiler.Profiler; import battlecode.instrumenter.stream.RoboPrintStream; import battlecode.instrumenter.stream.SilencedPrintStream; import battlecode.server.ErrorReporter; @@ -109,7 +110,8 @@ public SandboxedRobotPlayer(String teamName, RobotController robotController, int seed, TeamClassLoaderFactory.Loader loader, - OutputStream robotOut) + OutputStream robotOut, + Profiler profiler) throws InstrumentationException { this.robotController = robotController; this.seed = seed; @@ -133,7 +135,7 @@ public SandboxedRobotPlayer(String teamName, setBytecodeLimitMethod = monitor.getMethod("setBytecodeLimit", int.class); getBytecodeNumMethod = monitor.getMethod("getBytecodeNum"); pauseMethod = monitor.getMethod("pause"); - initMethod = monitor.getMethod("init", Pauser.class, Killer.class, int.class); + initMethod = monitor.getMethod("init", Pauser.class, Killer.class, int.class, Profiler.class); // Note: loading this here also keeps any initialization we do in System // from inflicting its bytecode cost on the player. @@ -173,7 +175,7 @@ public SandboxedRobotPlayer(String teamName, mainThread = new Thread(() -> { try { // Init RobotMonitor - initMethod.invoke(null, pauser, killer, this.seed); + initMethod.invoke(null, pauser, killer, this.seed, profiler); // Pause immediately pauseMethod.invoke(null); // Run the robot! @@ -203,6 +205,12 @@ public SandboxedRobotPlayer(String teamName, // Ensure that we know we're terminated. this.terminated = true; + // Tell the profiler to close all open methods + // It cannot detect when the run(RobotController) method exits when a bot dies any other way + if (profiler != null) { + profiler.exitOpenMethods(); + } + // Unpause the main thread, which is waiting on the player thread. synchronized (notifier) { notifier.notifyAll(); diff --git a/engine/src/main/battlecode/instrumenter/TeamClassLoaderFactory.java b/engine/src/main/battlecode/instrumenter/TeamClassLoaderFactory.java index 20fcc968..cfb66f31 100644 --- a/engine/src/main/battlecode/instrumenter/TeamClassLoaderFactory.java +++ b/engine/src/main/battlecode/instrumenter/TeamClassLoaderFactory.java @@ -156,8 +156,8 @@ public URL getResource(String name) { * Create a loader for a new robot. * @return */ - public Loader createLoader() { - return new Loader(); + public Loader createLoader(boolean profilerEnabled) { + return new Loader(profilerEnabled); } /** @@ -364,12 +364,17 @@ public class Loader extends ClassLoader { */ private final Map> loadedCache; + /** + * Whether bytecode profiling is enabled or not. + */ + private final boolean profilerEnabled; + /** * Create a loader. * * @throws InstrumentationException if we fail to create a loader for some reason. */ - private Loader() throws InstrumentationException { + private Loader(boolean profilerEnabled) throws InstrumentationException { // use our classloader as a parent, rather than the default // system classloader @@ -391,6 +396,7 @@ private Loader() throws InstrumentationException { }*/ this.loadedCache = new HashMap<>(); + this.profilerEnabled = profilerEnabled; } public TeamClassLoaderFactory getFactory() { @@ -522,7 +528,8 @@ public byte[] instrument(ClassReader reader, this, false, checkDisallowed, - debugMethodsEnabled + debugMethodsEnabled, + profilerEnabled ); reader.accept(cv, 0); //passing false lets debug info be included in the transformation, so players get line numbers in stack traces return cw.toByteArray(); diff --git a/engine/src/main/battlecode/instrumenter/Verifier.java b/engine/src/main/battlecode/instrumenter/Verifier.java index daf2ef02..b07eac19 100644 --- a/engine/src/main/battlecode/instrumenter/Verifier.java +++ b/engine/src/main/battlecode/instrumenter/Verifier.java @@ -30,7 +30,7 @@ public static void main(String[] args) { public static boolean verify(String teamPackageName, String teamURL) { try { - TeamClassLoaderFactory.Loader loader = new TeamClassLoaderFactory(teamURL).createLoader(); + TeamClassLoaderFactory.Loader loader = new TeamClassLoaderFactory(teamURL).createLoader(false); // Has teamPackageName/RobotPlayer.java loader.loadClass(teamPackageName + ".RobotPlayer"); diff --git a/engine/src/main/battlecode/instrumenter/bytecode/InstrumentingClassVisitor.java b/engine/src/main/battlecode/instrumenter/bytecode/InstrumentingClassVisitor.java index b405f6a9..0d0ffae1 100644 --- a/engine/src/main/battlecode/instrumenter/bytecode/InstrumentingClassVisitor.java +++ b/engine/src/main/battlecode/instrumenter/bytecode/InstrumentingClassVisitor.java @@ -21,6 +21,7 @@ public class InstrumentingClassVisitor extends ClassVisitor implements Opcodes { private String className; private final boolean silenced; private final boolean debugMethodsEnabled; + private final boolean profilerEnabled; // Used to find other class files, which is occasionally necessary. private TeamClassLoaderFactory.Loader loader; @@ -40,12 +41,14 @@ public InstrumentingClassVisitor(final ClassVisitor cv, final TeamClassLoaderFactory.Loader loader, boolean silenced, boolean checkDisallowed, - boolean debugMethodsEnabled) throws InstrumentationException { + boolean debugMethodsEnabled, + boolean profilerEnabled) throws InstrumentationException { super(Opcodes.ASM5, cv); this.loader = loader; this.silenced = silenced; this.checkDisallowed = checkDisallowed; this.debugMethodsEnabled = debugMethodsEnabled; + this.profilerEnabled = profilerEnabled; } /** @@ -106,7 +109,8 @@ public MethodVisitor visitMethod( exceptions, silenced, checkDisallowed, - debugMethodsEnabled + debugMethodsEnabled, + profilerEnabled ); } diff --git a/engine/src/main/battlecode/instrumenter/bytecode/InstrumentingMethodVisitor.java b/engine/src/main/battlecode/instrumenter/bytecode/InstrumentingMethodVisitor.java index 2abedd9e..5f350673 100644 --- a/engine/src/main/battlecode/instrumenter/bytecode/InstrumentingMethodVisitor.java +++ b/engine/src/main/battlecode/instrumenter/bytecode/InstrumentingMethodVisitor.java @@ -31,6 +31,7 @@ public class InstrumentingMethodVisitor extends MethodNode implements Opcodes { private final String className; // the class to which this method belongs private final boolean checkDisallowed; private final boolean debugMethodsEnabled; + private final boolean profilerEnabled; // used to load other class files private final TeamClassLoaderFactory.Loader loader; @@ -64,7 +65,8 @@ public InstrumentingMethodVisitor(final MethodVisitor mv, final String[] exceptions, boolean silenced, boolean checkDisallowed, - boolean debugMethodsEnabled) { + boolean debugMethodsEnabled, + boolean profilerEnabled) { super(ASM5, access, methodName, methodDesc, signature, exceptions); this.methodWriter = mv; @@ -72,6 +74,7 @@ public InstrumentingMethodVisitor(final MethodVisitor mv, this.className = className; this.checkDisallowed = checkDisallowed; this.debugMethodsEnabled = debugMethodsEnabled; + this.profilerEnabled = profilerEnabled; } protected String classReference(String name) { @@ -141,8 +144,8 @@ public void visitMaxs(int maxStack, int maxLocals) { endOfBasicBlock(node); break; case INT_INSN: - visitIntInsnNode((IntInsnNode) node); - break; + visitIntInsnNode((IntInsnNode) node); + break; case IINC_INSN: bytecodeCtr++; break; @@ -153,6 +156,11 @@ public void visitMaxs(int maxStack, int maxLocals) { boolean anyTryCatch = tryCatchBlocks.size() > 0; + if (profilerEnabled) { + // must be called before addDebugHandler() so debug methods are profiled properly + addEnterMethodHandler(); + } + if (debugMethodsEnabled && name.startsWith(DEBUG_PREFIX) && desc.endsWith("V")) { addDebugHandler(); } @@ -185,6 +193,44 @@ private static AbstractInsnNode nextInstruction(AbstractInsnNode n) { return n; } + private void addEnterMethodHandler() { + if (!profilerEnabled) { + return; + } + + // call "enterMethod" at the beginning of the method + instructions.insertBefore( + nextInstruction(instructions.getFirst()), + new MethodInsnNode( + INVOKESTATIC, + "battlecode/instrumenter/inject/RobotMonitor", + "enterMethod", + "(" + Type.getDescriptor(String.class) + ")V", + false + ) + ); + instructions.insertBefore( + nextInstruction(instructions.getFirst()), + new LdcInsnNode(className.replaceAll("/", ".") + "." + name) + ); + } + + private void addExitMethodHandler(AbstractInsnNode n) { + if (!profilerEnabled) { + return; + } + + // call "exitMethod" at every exit point of a method (return, implicit return and throw) + instructions.insertBefore(n, new LdcInsnNode(className.replaceAll("/", ".") + "." + name)); + instructions.insertBefore(n, new MethodInsnNode( + INVOKESTATIC, + "battlecode/instrumenter/inject/RobotMonitor", + "exitMethod", + "(" + Type.getDescriptor(String.class) + ")V", + false + )); + } + @SuppressWarnings("unchecked") private void addDebugHandler() { // will be injected at the end of the method @@ -262,6 +308,7 @@ private void visitInsnNode(InsnNode n) { case ARETURN: case RETURN: endOfBasicBlock(n); + addExitMethodHandler(n); if (name.startsWith("debug_") && desc.endsWith("V")) { instructions.insertBefore(n, new MethodInsnNode( INVOKESTATIC, @@ -273,6 +320,7 @@ private void visitInsnNode(InsnNode n) { break; case ATHROW: endOfBasicBlock(n); + addExitMethodHandler(n); break; case MONITORENTER: case MONITOREXIT: diff --git a/engine/src/main/battlecode/instrumenter/inject/RobotMonitor.java b/engine/src/main/battlecode/instrumenter/inject/RobotMonitor.java index 29aeb783..28070db2 100644 --- a/engine/src/main/battlecode/instrumenter/inject/RobotMonitor.java +++ b/engine/src/main/battlecode/instrumenter/inject/RobotMonitor.java @@ -1,6 +1,7 @@ package battlecode.instrumenter.inject; import battlecode.instrumenter.SandboxedRobotPlayer; +import battlecode.instrumenter.profiler.Profiler; import battlecode.server.ErrorReporter; import java.io.PrintStream; @@ -31,6 +32,8 @@ public final class RobotMonitor { private static SandboxedRobotPlayer.Pauser pauser; private static SandboxedRobotPlayer.Killer killer; + private static Profiler profiler; + // Methods called from SandboxedRobotPlayer /** @@ -39,12 +42,16 @@ public final class RobotMonitor { * * Called in the robot thread from SandboxedRobotPlayer. * - * @param thePauser pauser to use to pause the thread + * @param thePauser pauser to use to pause the thread + * @param theKiller killer to use to kill the thread + * @param seed seed to use for new Random instances + * @param theProfiler profiler to log bytecode usage per method to (profiling is disabled if null) */ @SuppressWarnings("unused") public static void init(SandboxedRobotPlayer.Pauser thePauser, SandboxedRobotPlayer.Killer theKiller, - int seed) { + int seed, + Profiler theProfiler) { shouldDie = false; bytecodesLeft = 0; debugLevel = 0; @@ -52,6 +59,8 @@ public static void init(SandboxedRobotPlayer.Pauser thePauser, randomSeed = seed; pauser = thePauser; killer = theKiller; + + profiler = theProfiler; } /** @@ -129,6 +138,12 @@ public static void incrementBytecodes(int numBytecodes) { bytecodesLeft = Integer.MIN_VALUE; } + if (profiler != null) { + // profiler.incrementBytecodes uses Math.addExact to prevent against integer overflow + profiler.incrementBytecodes(numBytecodes); + profiler.incrementBytecodes(bytecodesToRemove); + } + while (bytecodesLeft <= 0) { pause(); } @@ -235,6 +250,34 @@ public static long getRandomSeed() { return randomSeed; } + /** + * Called at the start of a method. Used by the profiler. + * + * THIS METHOD IS CALLED BY THE INSTRUMENTER. + * + * @param methodName the name of the method that is being entered + */ + @SuppressWarnings("unused") + public static void enterMethod(String methodName) { + if (debugLevel == 0 && profiler != null) { + profiler.enterMethod(methodName); + } + } + + /** + * Called at all exit points of a method. Used by the profiler. + * + * THIS METHOD IS CALLED BY THE INSTRUMENTER. + * + * @param methodName the name of the method that is being exited + */ + @SuppressWarnings("unused") + public static void exitMethod(String methodName) { + if (debugLevel == 0 && profiler != null) { + profiler.exitMethod(methodName); + } + } + /** * Pauses the run of the current robot. * diff --git a/engine/src/main/battlecode/instrumenter/profiler/Profiler.java b/engine/src/main/battlecode/instrumenter/profiler/Profiler.java new file mode 100644 index 00000000..2a3dbcd2 --- /dev/null +++ b/engine/src/main/battlecode/instrumenter/profiler/Profiler.java @@ -0,0 +1,85 @@ +package battlecode.instrumenter.profiler; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +/** + * The Profiler class profiles bytecode usage in a sandboxed robot player. + * It is called by the instrumenter through RobotMonitor and only profiles + * down to methods created by the player (e.g. it won't show the amount of + * bytecode an ArrayList.add() call costs). + *

+ * Data is stored in such a way that it is easy to convert it to a file + * compatible with speedscope (https://github.com/jlfwong/speedscope) + * which is used in the client to show the profiling data. See + * https://github.com/jlfwong/speedscope/wiki/Importing-from-custom-sources + * for more information on speedscope's file format. + */ +public class Profiler { + private final ProfilerCollection collection; + private final String name; + + private int bytecodeCounter = 0; + + private final List events = new ArrayList<>(); + private final Deque openFrameIds = new ArrayDeque<>(); + + public Profiler(ProfilerCollection collection, String name) { + this.collection = collection; + this.name = name; + } + + public void incrementBytecodes(int amount) { + try { + bytecodeCounter = Math.addExact(bytecodeCounter, amount); + } catch (ArithmeticException e) { + bytecodeCounter = Integer.MAX_VALUE; + } + } + + public void enterMethod(String methodName) { + if (!collection.isRecordingEvents()) { + return; + } + + if (methodName.startsWith("instrumented.")) { + return; + } + + collection.recordEvent(); + + int frameId = collection.getFrameId(methodName); + + events.add(new ProfilerEvent(ProfilerEventType.OPEN, bytecodeCounter, frameId)); + openFrameIds.addFirst(frameId); + } + + public void exitMethod(String methodName) { + if (openFrameIds.isEmpty() && !collection.isRecordingEvents()) { + return; + } + + if (methodName.startsWith("instrumented.")) { + return; + } + + events.add(new ProfilerEvent(ProfilerEventType.CLOSE, bytecodeCounter, collection.getFrameId(methodName))); + openFrameIds.pop(); + } + + public void exitOpenMethods() { + while (!openFrameIds.isEmpty()) { + events.add(new ProfilerEvent(ProfilerEventType.CLOSE, bytecodeCounter, openFrameIds.pop())); + } + } + + public String getName() { + return name; + } + + public List getEvents() { + return events; + } +} diff --git a/engine/src/main/battlecode/instrumenter/profiler/ProfilerCollection.java b/engine/src/main/battlecode/instrumenter/profiler/ProfilerCollection.java new file mode 100644 index 00000000..c3d4d444 --- /dev/null +++ b/engine/src/main/battlecode/instrumenter/profiler/ProfilerCollection.java @@ -0,0 +1,63 @@ +package battlecode.instrumenter.profiler; + +import battlecode.common.RobotType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A ProfilerCollection is a collection of all Profiler instances for a team for a match. + */ +public class ProfilerCollection { + /** + * We record a maximum of 2,000,000 events per team per match. + * This equals a rough maximum of 50MB of profiling data per match, + * which should prevent the client from hanging when opening a replay + * of a match in which profiling was enabled. + */ + private static final int MAX_EVENTS_TO_RECORD = 2_000_000; + + private List profilers = new ArrayList<>(); + + private List frames = new ArrayList<>(); + private Map frameIds = new HashMap<>(); + + private int recordedEvents = 0; + + public Profiler createProfiler(int robotId, RobotType robotType) { + // The name has to be display-friendly + String name = String.format("#%s (%s)", robotId, robotType.toString()); + + Profiler profiler = new Profiler(this, name); + profilers.add(profiler); + + return profiler; + } + + public List getFrames() { + return frames; + } + + public List getProfilers() { + return profilers; + } + + public int getFrameId(String methodName) { + if (!frameIds.containsKey(methodName)) { + frames.add(methodName); + frameIds.put(methodName, frames.size() - 1); + } + + return frameIds.get(methodName); + } + + public void recordEvent() { + recordedEvents++; + } + + public boolean isRecordingEvents() { + return recordedEvents < MAX_EVENTS_TO_RECORD; + } +} diff --git a/engine/src/main/battlecode/instrumenter/profiler/ProfilerEvent.java b/engine/src/main/battlecode/instrumenter/profiler/ProfilerEvent.java new file mode 100644 index 00000000..826fb3a8 --- /dev/null +++ b/engine/src/main/battlecode/instrumenter/profiler/ProfilerEvent.java @@ -0,0 +1,25 @@ +package battlecode.instrumenter.profiler; + +public class ProfilerEvent { + private ProfilerEventType type; + private int at; + private int frameId; + + public ProfilerEvent(ProfilerEventType type, int at, int frameId) { + this.type = type; + this.at = at; + this.frameId = frameId; + } + + public ProfilerEventType getType() { + return type; + } + + public int getAt() { + return at; + } + + public int getFrameId() { + return frameId; + } +} diff --git a/engine/src/main/battlecode/instrumenter/profiler/ProfilerEventType.java b/engine/src/main/battlecode/instrumenter/profiler/ProfilerEventType.java new file mode 100644 index 00000000..0f785b3d --- /dev/null +++ b/engine/src/main/battlecode/instrumenter/profiler/ProfilerEventType.java @@ -0,0 +1,15 @@ +package battlecode.instrumenter.profiler; + +public enum ProfilerEventType { + OPEN("O"), CLOSE("C"); + + private final String value; + + ProfilerEventType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/engine/src/main/battlecode/schema/Action.java b/engine/src/main/battlecode/schema/Action.java index 6bf137da..a21287a3 100644 --- a/engine/src/main/battlecode/schema/Action.java +++ b/engine/src/main/battlecode/schema/Action.java @@ -14,7 +14,7 @@ public final class Action { private Action() { } /** * Politicians self-destruct and affect nearby bodies. - * Target: none + * Target: radius squared */ public static final byte EMPOWER = 0; /** diff --git a/engine/src/main/battlecode/schema/BodyTypeMetadata.java b/engine/src/main/battlecode/schema/BodyTypeMetadata.java index 9fe2f7e5..8b7e63ba 100644 --- a/engine/src/main/battlecode/schema/BodyTypeMetadata.java +++ b/engine/src/main/battlecode/schema/BodyTypeMetadata.java @@ -13,8 +13,9 @@ */ public final class BodyTypeMetadata extends Table { public static BodyTypeMetadata getRootAsBodyTypeMetadata(ByteBuffer _bb) { return getRootAsBodyTypeMetadata(_bb, new BodyTypeMetadata()); } - public static BodyTypeMetadata getRootAsBodyTypeMetadata(ByteBuffer _bb, BodyTypeMetadata obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public BodyTypeMetadata __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static BodyTypeMetadata getRootAsBodyTypeMetadata(ByteBuffer _bb, BodyTypeMetadata obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public BodyTypeMetadata __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * The relevant type. diff --git a/engine/src/main/battlecode/schema/EventWrapper.java b/engine/src/main/battlecode/schema/EventWrapper.java index f525b5a5..3eb73a91 100644 --- a/engine/src/main/battlecode/schema/EventWrapper.java +++ b/engine/src/main/battlecode/schema/EventWrapper.java @@ -13,8 +13,9 @@ */ public final class EventWrapper extends Table { public static EventWrapper getRootAsEventWrapper(ByteBuffer _bb) { return getRootAsEventWrapper(_bb, new EventWrapper()); } - public static EventWrapper getRootAsEventWrapper(ByteBuffer _bb, EventWrapper obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public EventWrapper __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static EventWrapper getRootAsEventWrapper(ByteBuffer _bb, EventWrapper obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public EventWrapper __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } public byte eType() { int o = __offset(4); return o != 0 ? bb.get(o + bb_pos) : 0; } public Table e(Table obj) { int o = __offset(6); return o != 0 ? __union(obj, o) : null; } diff --git a/engine/src/main/battlecode/schema/GameFooter.java b/engine/src/main/battlecode/schema/GameFooter.java index 037626bf..b5512bb5 100644 --- a/engine/src/main/battlecode/schema/GameFooter.java +++ b/engine/src/main/battlecode/schema/GameFooter.java @@ -13,8 +13,9 @@ */ public final class GameFooter extends Table { public static GameFooter getRootAsGameFooter(ByteBuffer _bb) { return getRootAsGameFooter(_bb, new GameFooter()); } - public static GameFooter getRootAsGameFooter(ByteBuffer _bb, GameFooter obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public GameFooter __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static GameFooter getRootAsGameFooter(ByteBuffer _bb, GameFooter obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public GameFooter __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * The ID of the winning team of the game. diff --git a/engine/src/main/battlecode/schema/GameHeader.java b/engine/src/main/battlecode/schema/GameHeader.java index b051e614..77363711 100644 --- a/engine/src/main/battlecode/schema/GameHeader.java +++ b/engine/src/main/battlecode/schema/GameHeader.java @@ -13,25 +13,27 @@ */ public final class GameHeader extends Table { public static GameHeader getRootAsGameHeader(ByteBuffer _bb) { return getRootAsGameHeader(_bb, new GameHeader()); } - public static GameHeader getRootAsGameHeader(ByteBuffer _bb, GameHeader obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public GameHeader __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static GameHeader getRootAsGameHeader(ByteBuffer _bb, GameHeader obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public GameHeader __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * The version of the spec this game complies with. */ public String specVersion() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; } public ByteBuffer specVersionAsByteBuffer() { return __vector_as_bytebuffer(4, 1); } + public ByteBuffer specVersionInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); } /** * The teams participating in the game. */ public TeamData teams(int j) { return teams(new TeamData(), j); } - public TeamData teams(TeamData obj, int j) { int o = __offset(6); return o != 0 ? obj.__init(__indirect(__vector(o) + j * 4), bb) : null; } + public TeamData teams(TeamData obj, int j) { int o = __offset(6); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; } public int teamsLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; } /** * Information about all body types in the game. */ public BodyTypeMetadata bodyTypeMetadata(int j) { return bodyTypeMetadata(new BodyTypeMetadata(), j); } - public BodyTypeMetadata bodyTypeMetadata(BodyTypeMetadata obj, int j) { int o = __offset(8); return o != 0 ? obj.__init(__indirect(__vector(o) + j * 4), bb) : null; } + public BodyTypeMetadata bodyTypeMetadata(BodyTypeMetadata obj, int j) { int o = __offset(8); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; } public int bodyTypeMetadataLength() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; } public static int createGameHeader(FlatBufferBuilder builder, diff --git a/engine/src/main/battlecode/schema/GameMap.java b/engine/src/main/battlecode/schema/GameMap.java index f6014a19..09b78818 100644 --- a/engine/src/main/battlecode/schema/GameMap.java +++ b/engine/src/main/battlecode/schema/GameMap.java @@ -13,14 +13,16 @@ */ public final class GameMap extends Table { public static GameMap getRootAsGameMap(ByteBuffer _bb) { return getRootAsGameMap(_bb, new GameMap()); } - public static GameMap getRootAsGameMap(ByteBuffer _bb, GameMap obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public GameMap __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static GameMap getRootAsGameMap(ByteBuffer _bb, GameMap obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public GameMap __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * The name of a map. */ public String name() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; } public ByteBuffer nameAsByteBuffer() { return __vector_as_bytebuffer(4, 1); } + public ByteBuffer nameInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); } /** * The bottom corner of the map. */ @@ -35,7 +37,7 @@ public final class GameMap extends Table { * The bodies on the map. */ public SpawnedBodyTable bodies() { return bodies(new SpawnedBodyTable()); } - public SpawnedBodyTable bodies(SpawnedBodyTable obj) { int o = __offset(10); return o != 0 ? obj.__init(__indirect(o + bb_pos), bb) : null; } + public SpawnedBodyTable bodies(SpawnedBodyTable obj) { int o = __offset(10); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; } /** * The random seed of the map. */ @@ -46,6 +48,7 @@ public final class GameMap extends Table { public double passability(int j) { int o = __offset(14); return o != 0 ? bb.getDouble(__vector(o) + j * 8) : 0; } public int passabilityLength() { int o = __offset(14); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer passabilityAsByteBuffer() { return __vector_as_bytebuffer(14, 8); } + public ByteBuffer passabilityInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 14, 8); } public static void startGameMap(FlatBufferBuilder builder) { builder.startObject(6); } public static void addName(FlatBufferBuilder builder, int nameOffset) { builder.addOffset(0, nameOffset, 0); } diff --git a/engine/src/main/battlecode/schema/GameWrapper.java b/engine/src/main/battlecode/schema/GameWrapper.java index ee17ada1..25a0c03e 100644 --- a/engine/src/main/battlecode/schema/GameWrapper.java +++ b/engine/src/main/battlecode/schema/GameWrapper.java @@ -18,14 +18,15 @@ */ public final class GameWrapper extends Table { public static GameWrapper getRootAsGameWrapper(ByteBuffer _bb) { return getRootAsGameWrapper(_bb, new GameWrapper()); } - public static GameWrapper getRootAsGameWrapper(ByteBuffer _bb, GameWrapper obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public GameWrapper __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static GameWrapper getRootAsGameWrapper(ByteBuffer _bb, GameWrapper obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public GameWrapper __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * The series of events comprising the game. */ public EventWrapper events(int j) { return events(new EventWrapper(), j); } - public EventWrapper events(EventWrapper obj, int j) { int o = __offset(4); return o != 0 ? obj.__init(__indirect(__vector(o) + j * 4), bb) : null; } + public EventWrapper events(EventWrapper obj, int j) { int o = __offset(4); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; } public int eventsLength() { int o = __offset(4); return o != 0 ? __vector_len(o) : 0; } /** * The indices of the headers of the matches, in order. @@ -33,12 +34,14 @@ public final class GameWrapper extends Table { public int matchHeaders(int j) { int o = __offset(6); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int matchHeadersLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer matchHeadersAsByteBuffer() { return __vector_as_bytebuffer(6, 4); } + public ByteBuffer matchHeadersInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 4); } /** * The indices of the footers of the matches, in order. */ public int matchFooters(int j) { int o = __offset(8); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int matchFootersLength() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer matchFootersAsByteBuffer() { return __vector_as_bytebuffer(8, 4); } + public ByteBuffer matchFootersInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 4); } public static int createGameWrapper(FlatBufferBuilder builder, int eventsOffset, diff --git a/engine/src/main/battlecode/schema/MatchFooter.java b/engine/src/main/battlecode/schema/MatchFooter.java index b3f40b25..8777c9ea 100644 --- a/engine/src/main/battlecode/schema/MatchFooter.java +++ b/engine/src/main/battlecode/schema/MatchFooter.java @@ -13,8 +13,9 @@ */ public final class MatchFooter extends Table { public static MatchFooter getRootAsMatchFooter(ByteBuffer _bb) { return getRootAsMatchFooter(_bb, new MatchFooter()); } - public static MatchFooter getRootAsMatchFooter(ByteBuffer _bb, MatchFooter obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public MatchFooter __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static MatchFooter getRootAsMatchFooter(ByteBuffer _bb, MatchFooter obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public MatchFooter __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * The ID of the winning team. @@ -24,19 +25,30 @@ public final class MatchFooter extends Table { * The number of rounds played. */ public int totalRounds() { int o = __offset(6); return o != 0 ? bb.getInt(o + bb_pos) : 0; } + /** + * Profiler data for team A and B if profiling is enabled. + */ + public ProfilerFile profilerFiles(int j) { return profilerFiles(new ProfilerFile(), j); } + public ProfilerFile profilerFiles(ProfilerFile obj, int j) { int o = __offset(8); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; } + public int profilerFilesLength() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; } public static int createMatchFooter(FlatBufferBuilder builder, byte winner, - int totalRounds) { - builder.startObject(2); + int totalRounds, + int profilerFilesOffset) { + builder.startObject(3); + MatchFooter.addProfilerFiles(builder, profilerFilesOffset); MatchFooter.addTotalRounds(builder, totalRounds); MatchFooter.addWinner(builder, winner); return MatchFooter.endMatchFooter(builder); } - public static void startMatchFooter(FlatBufferBuilder builder) { builder.startObject(2); } + public static void startMatchFooter(FlatBufferBuilder builder) { builder.startObject(3); } public static void addWinner(FlatBufferBuilder builder, byte winner) { builder.addByte(0, winner, 0); } public static void addTotalRounds(FlatBufferBuilder builder, int totalRounds) { builder.addInt(1, totalRounds, 0); } + public static void addProfilerFiles(FlatBufferBuilder builder, int profilerFilesOffset) { builder.addOffset(2, profilerFilesOffset, 0); } + public static int createProfilerFilesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); } + public static void startProfilerFilesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); } public static int endMatchFooter(FlatBufferBuilder builder) { int o = builder.endObject(); return o; diff --git a/engine/src/main/battlecode/schema/MatchHeader.java b/engine/src/main/battlecode/schema/MatchHeader.java index 621a3a39..b167ea98 100644 --- a/engine/src/main/battlecode/schema/MatchHeader.java +++ b/engine/src/main/battlecode/schema/MatchHeader.java @@ -13,14 +13,15 @@ */ public final class MatchHeader extends Table { public static MatchHeader getRootAsMatchHeader(ByteBuffer _bb) { return getRootAsMatchHeader(_bb, new MatchHeader()); } - public static MatchHeader getRootAsMatchHeader(ByteBuffer _bb, MatchHeader obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public MatchHeader __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static MatchHeader getRootAsMatchHeader(ByteBuffer _bb, MatchHeader obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public MatchHeader __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * The map the match was played on. */ public GameMap map() { return map(new GameMap()); } - public GameMap map(GameMap obj) { int o = __offset(4); return o != 0 ? obj.__init(__indirect(o + bb_pos), bb) : null; } + public GameMap map(GameMap obj) { int o = __offset(4); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; } /** * The maximum number of rounds in this match. */ diff --git a/engine/src/main/battlecode/schema/ProfilerEvent.java b/engine/src/main/battlecode/schema/ProfilerEvent.java index 535f0e25..58c47f12 100644 --- a/engine/src/main/battlecode/schema/ProfilerEvent.java +++ b/engine/src/main/battlecode/schema/ProfilerEvent.java @@ -9,7 +9,6 @@ @SuppressWarnings("unused") /** - * Profiler tables * These tables are set-up so that they match closely with speedscope's file format documented at * https://github.com/jlfwong/speedscope/wiki/Importing-from-custom-sources. * The client uses speedscope to show the recorded data in an interactive interface. @@ -18,8 +17,9 @@ */ public final class ProfilerEvent extends Table { public static ProfilerEvent getRootAsProfilerEvent(ByteBuffer _bb) { return getRootAsProfilerEvent(_bb, new ProfilerEvent()); } - public static ProfilerEvent getRootAsProfilerEvent(ByteBuffer _bb, ProfilerEvent obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public ProfilerEvent __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static ProfilerEvent getRootAsProfilerEvent(ByteBuffer _bb, ProfilerEvent obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public ProfilerEvent __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * Whether this is an open event (true) or a close event (false). diff --git a/engine/src/main/battlecode/schema/ProfilerFile.java b/engine/src/main/battlecode/schema/ProfilerFile.java index 590be8d9..c101925d 100644 --- a/engine/src/main/battlecode/schema/ProfilerFile.java +++ b/engine/src/main/battlecode/schema/ProfilerFile.java @@ -14,8 +14,9 @@ */ public final class ProfilerFile extends Table { public static ProfilerFile getRootAsProfilerFile(ByteBuffer _bb) { return getRootAsProfilerFile(_bb, new ProfilerFile()); } - public static ProfilerFile getRootAsProfilerFile(ByteBuffer _bb, ProfilerFile obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public ProfilerFile __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static ProfilerFile getRootAsProfilerFile(ByteBuffer _bb, ProfilerFile obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public ProfilerFile __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * The method names that are referred to in the events. @@ -26,7 +27,7 @@ public final class ProfilerFile extends Table { * The recorded profiles, one per robot. */ public ProfilerProfile profiles(int j) { return profiles(new ProfilerProfile(), j); } - public ProfilerProfile profiles(ProfilerProfile obj, int j) { int o = __offset(6); return o != 0 ? obj.__init(__indirect(__vector(o) + j * 4), bb) : null; } + public ProfilerProfile profiles(ProfilerProfile obj, int j) { int o = __offset(6); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; } public int profilesLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; } public static int createProfilerFile(FlatBufferBuilder builder, diff --git a/engine/src/main/battlecode/schema/ProfilerProfile.java b/engine/src/main/battlecode/schema/ProfilerProfile.java index 758e83a1..15312ead 100644 --- a/engine/src/main/battlecode/schema/ProfilerProfile.java +++ b/engine/src/main/battlecode/schema/ProfilerProfile.java @@ -13,19 +13,21 @@ */ public final class ProfilerProfile extends Table { public static ProfilerProfile getRootAsProfilerProfile(ByteBuffer _bb) { return getRootAsProfilerProfile(_bb, new ProfilerProfile()); } - public static ProfilerProfile getRootAsProfilerProfile(ByteBuffer _bb, ProfilerProfile obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public ProfilerProfile __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static ProfilerProfile getRootAsProfilerProfile(ByteBuffer _bb, ProfilerProfile obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public ProfilerProfile __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * The display-friendly name of the profile. */ public String name() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; } public ByteBuffer nameAsByteBuffer() { return __vector_as_bytebuffer(4, 1); } + public ByteBuffer nameInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); } /** * The events that occurred in the profile. */ public ProfilerEvent events(int j) { return events(new ProfilerEvent(), j); } - public ProfilerEvent events(ProfilerEvent obj, int j) { int o = __offset(6); return o != 0 ? obj.__init(__indirect(__vector(o) + j * 4), bb) : null; } + public ProfilerEvent events(ProfilerEvent obj, int j) { int o = __offset(6); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; } public int eventsLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; } public static int createProfilerProfile(FlatBufferBuilder builder, diff --git a/engine/src/main/battlecode/schema/RGBTable.java b/engine/src/main/battlecode/schema/RGBTable.java index c2beefa9..6164af79 100644 --- a/engine/src/main/battlecode/schema/RGBTable.java +++ b/engine/src/main/battlecode/schema/RGBTable.java @@ -13,18 +13,22 @@ */ public final class RGBTable extends Table { public static RGBTable getRootAsRGBTable(ByteBuffer _bb) { return getRootAsRGBTable(_bb, new RGBTable()); } - public static RGBTable getRootAsRGBTable(ByteBuffer _bb, RGBTable obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public RGBTable __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static RGBTable getRootAsRGBTable(ByteBuffer _bb, RGBTable obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public RGBTable __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } public int red(int j) { int o = __offset(4); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int redLength() { int o = __offset(4); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer redAsByteBuffer() { return __vector_as_bytebuffer(4, 4); } + public ByteBuffer redInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 4); } public int green(int j) { int o = __offset(6); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int greenLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer greenAsByteBuffer() { return __vector_as_bytebuffer(6, 4); } + public ByteBuffer greenInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 4); } public int blue(int j) { int o = __offset(8); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int blueLength() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer blueAsByteBuffer() { return __vector_as_bytebuffer(8, 4); } + public ByteBuffer blueInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 4); } public static int createRGBTable(FlatBufferBuilder builder, int redOffset, diff --git a/engine/src/main/battlecode/schema/Round.java b/engine/src/main/battlecode/schema/Round.java index f15674cf..c36316ae 100644 --- a/engine/src/main/battlecode/schema/Round.java +++ b/engine/src/main/battlecode/schema/Round.java @@ -16,8 +16,9 @@ */ public final class Round extends Table { public static Round getRootAsRound(ByteBuffer _bb) { return getRootAsRound(_bb, new Round()); } - public static Round getRootAsRound(ByteBuffer _bb, Round obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public Round __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static Round getRootAsRound(ByteBuffer _bb, Round obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public Round __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * The IDs of teams in the Game. @@ -25,40 +26,45 @@ public final class Round extends Table { public int teamIDs(int j) { int o = __offset(4); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int teamIDsLength() { int o = __offset(4); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer teamIDsAsByteBuffer() { return __vector_as_bytebuffer(4, 4); } + public ByteBuffer teamIDsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 4); } /** * The number of votes the teams get, 0 or 1. */ public int teamVotes(int j) { int o = __offset(6); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int teamVotesLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer teamVotesAsByteBuffer() { return __vector_as_bytebuffer(6, 4); } + public ByteBuffer teamVotesInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 4); } /** * The ID of the Enlightenment Center got the bid. */ public int teamBidderIDs(int j) { int o = __offset(8); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int teamBidderIDsLength() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer teamBidderIDsAsByteBuffer() { return __vector_as_bytebuffer(8, 4); } + public ByteBuffer teamBidderIDsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 4); } /** * The IDs of bodies that moved. */ public int movedIDs(int j) { int o = __offset(10); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int movedIDsLength() { int o = __offset(10); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer movedIDsAsByteBuffer() { return __vector_as_bytebuffer(10, 4); } + public ByteBuffer movedIDsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 10, 4); } /** * The new locations of bodies that have moved. */ public VecTable movedLocs() { return movedLocs(new VecTable()); } - public VecTable movedLocs(VecTable obj) { int o = __offset(12); return o != 0 ? obj.__init(__indirect(o + bb_pos), bb) : null; } + public VecTable movedLocs(VecTable obj) { int o = __offset(12); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; } /** * New bodies. */ public SpawnedBodyTable spawnedBodies() { return spawnedBodies(new SpawnedBodyTable()); } - public SpawnedBodyTable spawnedBodies(SpawnedBodyTable obj) { int o = __offset(14); return o != 0 ? obj.__init(__indirect(o + bb_pos), bb) : null; } + public SpawnedBodyTable spawnedBodies(SpawnedBodyTable obj) { int o = __offset(14); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; } /** * The IDs of bodies that died. */ public int diedIDs(int j) { int o = __offset(16); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int diedIDsLength() { int o = __offset(16); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer diedIDsAsByteBuffer() { return __vector_as_bytebuffer(16, 4); } + public ByteBuffer diedIDsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 16, 4); } /** * The IDs of robots that performed actions. * IDs may repeat. @@ -66,55 +72,60 @@ public final class Round extends Table { public int actionIDs(int j) { int o = __offset(18); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int actionIDsLength() { int o = __offset(18); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer actionIDsAsByteBuffer() { return __vector_as_bytebuffer(18, 4); } + public ByteBuffer actionIDsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 18, 4); } /** * The actions performed. These actions allow us to track how much soup or dirt a body carries. */ public byte actions(int j) { int o = __offset(20); return o != 0 ? bb.get(__vector(o) + j * 1) : 0; } public int actionsLength() { int o = __offset(20); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer actionsAsByteBuffer() { return __vector_as_bytebuffer(20, 1); } + public ByteBuffer actionsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 20, 1); } /** * The 'targets' of the performed actions. Actions without targets may have any value */ public int actionTargets(int j) { int o = __offset(22); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int actionTargetsLength() { int o = __offset(22); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer actionTargetsAsByteBuffer() { return __vector_as_bytebuffer(22, 4); } + public ByteBuffer actionTargetsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 22, 4); } /** * The IDs of bodies that set indicator dots */ public int indicatorDotIDs(int j) { int o = __offset(24); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int indicatorDotIDsLength() { int o = __offset(24); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer indicatorDotIDsAsByteBuffer() { return __vector_as_bytebuffer(24, 4); } + public ByteBuffer indicatorDotIDsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 24, 4); } /** * The location of the indicator dots */ public VecTable indicatorDotLocs() { return indicatorDotLocs(new VecTable()); } - public VecTable indicatorDotLocs(VecTable obj) { int o = __offset(26); return o != 0 ? obj.__init(__indirect(o + bb_pos), bb) : null; } + public VecTable indicatorDotLocs(VecTable obj) { int o = __offset(26); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; } /** * The RGB values of the indicator dots */ public RGBTable indicatorDotRGBs() { return indicatorDotRGBs(new RGBTable()); } - public RGBTable indicatorDotRGBs(RGBTable obj) { int o = __offset(28); return o != 0 ? obj.__init(__indirect(o + bb_pos), bb) : null; } + public RGBTable indicatorDotRGBs(RGBTable obj) { int o = __offset(28); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; } /** * The IDs of bodies that set indicator lines */ public int indicatorLineIDs(int j) { int o = __offset(30); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int indicatorLineIDsLength() { int o = __offset(30); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer indicatorLineIDsAsByteBuffer() { return __vector_as_bytebuffer(30, 4); } + public ByteBuffer indicatorLineIDsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 30, 4); } /** * The start location of the indicator lines */ public VecTable indicatorLineStartLocs() { return indicatorLineStartLocs(new VecTable()); } - public VecTable indicatorLineStartLocs(VecTable obj) { int o = __offset(32); return o != 0 ? obj.__init(__indirect(o + bb_pos), bb) : null; } + public VecTable indicatorLineStartLocs(VecTable obj) { int o = __offset(32); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; } /** * The end location of the indicator lines */ public VecTable indicatorLineEndLocs() { return indicatorLineEndLocs(new VecTable()); } - public VecTable indicatorLineEndLocs(VecTable obj) { int o = __offset(34); return o != 0 ? obj.__init(__indirect(o + bb_pos), bb) : null; } + public VecTable indicatorLineEndLocs(VecTable obj) { int o = __offset(34); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; } /** * The RGB values of the indicator lines */ public RGBTable indicatorLineRGBs() { return indicatorLineRGBs(new RGBTable()); } - public RGBTable indicatorLineRGBs(RGBTable obj) { int o = __offset(36); return o != 0 ? obj.__init(__indirect(o + bb_pos), bb) : null; } + public RGBTable indicatorLineRGBs(RGBTable obj) { int o = __offset(36); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; } /** * All logs sent this round. * Messages from a particular robot in this round start on a new line, and @@ -137,6 +148,7 @@ public final class Round extends Table { */ public String logs() { int o = __offset(38); return o != 0 ? __string(o + bb_pos) : null; } public ByteBuffer logsAsByteBuffer() { return __vector_as_bytebuffer(38, 1); } + public ByteBuffer logsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 38, 1); } /** * The first sent Round in a match should have index 1. (The starting state, * created by the MatchHeader, can be thought to have index 0.) @@ -149,12 +161,21 @@ public final class Round extends Table { public int bytecodeIDs(int j) { int o = __offset(42); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int bytecodeIDsLength() { int o = __offset(42); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer bytecodeIDsAsByteBuffer() { return __vector_as_bytebuffer(42, 4); } + public ByteBuffer bytecodeIDsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 42, 4); } /** * The bytecodes used by the player bodies. */ public int bytecodesUsed(int j) { int o = __offset(44); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int bytecodesUsedLength() { int o = __offset(44); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer bytecodesUsedAsByteBuffer() { return __vector_as_bytebuffer(44, 4); } + public ByteBuffer bytecodesUsedInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 44, 4); } + /** + * Amount of influence contributing to the teams' buffs. Added at end for backwards compatability. + */ + public int teamNumBuffs(int j) { int o = __offset(46); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } + public int teamNumBuffsLength() { int o = __offset(46); return o != 0 ? __vector_len(o) : 0; } + public ByteBuffer teamNumBuffsAsByteBuffer() { return __vector_as_bytebuffer(46, 4); } + public ByteBuffer teamNumBuffsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 46, 4); } public static int createRound(FlatBufferBuilder builder, int teamIDsOffset, @@ -177,8 +198,10 @@ public static int createRound(FlatBufferBuilder builder, int logsOffset, int roundID, int bytecodeIDsOffset, - int bytecodesUsedOffset) { - builder.startObject(21); + int bytecodesUsedOffset, + int teamNumBuffsOffset) { + builder.startObject(22); + Round.addTeamNumBuffs(builder, teamNumBuffsOffset); Round.addBytecodesUsed(builder, bytecodesUsedOffset); Round.addBytecodeIDs(builder, bytecodeIDsOffset); Round.addRoundID(builder, roundID); @@ -203,7 +226,7 @@ public static int createRound(FlatBufferBuilder builder, return Round.endRound(builder); } - public static void startRound(FlatBufferBuilder builder) { builder.startObject(21); } + public static void startRound(FlatBufferBuilder builder) { builder.startObject(22); } public static void addTeamIDs(FlatBufferBuilder builder, int teamIDsOffset) { builder.addOffset(0, teamIDsOffset, 0); } public static int createTeamIDsVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); } public static void startTeamIDsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); } @@ -249,6 +272,9 @@ public static int createRound(FlatBufferBuilder builder, public static void addBytecodesUsed(FlatBufferBuilder builder, int bytecodesUsedOffset) { builder.addOffset(20, bytecodesUsedOffset, 0); } public static int createBytecodesUsedVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); } public static void startBytecodesUsedVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); } + public static void addTeamNumBuffs(FlatBufferBuilder builder, int teamNumBuffsOffset) { builder.addOffset(21, teamNumBuffsOffset, 0); } + public static int createTeamNumBuffsVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); } + public static void startTeamNumBuffsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); } public static int endRound(FlatBufferBuilder builder) { int o = builder.endObject(); return o; diff --git a/engine/src/main/battlecode/schema/SpawnedBodyTable.java b/engine/src/main/battlecode/schema/SpawnedBodyTable.java index 622ecac4..3616e911 100644 --- a/engine/src/main/battlecode/schema/SpawnedBodyTable.java +++ b/engine/src/main/battlecode/schema/SpawnedBodyTable.java @@ -13,8 +13,9 @@ */ public final class SpawnedBodyTable extends Table { public static SpawnedBodyTable getRootAsSpawnedBodyTable(ByteBuffer _bb) { return getRootAsSpawnedBodyTable(_bb, new SpawnedBodyTable()); } - public static SpawnedBodyTable getRootAsSpawnedBodyTable(ByteBuffer _bb, SpawnedBodyTable obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public SpawnedBodyTable __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static SpawnedBodyTable getRootAsSpawnedBodyTable(ByteBuffer _bb, SpawnedBodyTable obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public SpawnedBodyTable __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * The numeric ID of the new bodies. @@ -26,23 +27,26 @@ public final class SpawnedBodyTable extends Table { public int robotIDs(int j) { int o = __offset(4); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int robotIDsLength() { int o = __offset(4); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer robotIDsAsByteBuffer() { return __vector_as_bytebuffer(4, 4); } + public ByteBuffer robotIDsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 4); } /** * The teams of the new bodies. */ public byte teamIDs(int j) { int o = __offset(6); return o != 0 ? bb.get(__vector(o) + j * 1) : 0; } public int teamIDsLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer teamIDsAsByteBuffer() { return __vector_as_bytebuffer(6, 1); } + public ByteBuffer teamIDsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 1); } /** * The types of the new bodies. */ public byte types(int j) { int o = __offset(8); return o != 0 ? bb.get(__vector(o) + j * 1) : 0; } public int typesLength() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer typesAsByteBuffer() { return __vector_as_bytebuffer(8, 1); } + public ByteBuffer typesInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 1); } /** * The locations of the bodies. */ public VecTable locs() { return locs(new VecTable()); } - public VecTable locs(VecTable obj) { int o = __offset(10); return o != 0 ? obj.__init(__indirect(o + bb_pos), bb) : null; } + public VecTable locs(VecTable obj) { int o = __offset(10); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; } /** * the amount of influence paid to create these bodies * for initial Enlightenment Centers, this is the amount of influence @@ -51,6 +55,7 @@ public final class SpawnedBodyTable extends Table { public int influences(int j) { int o = __offset(12); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int influencesLength() { int o = __offset(12); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer influencesAsByteBuffer() { return __vector_as_bytebuffer(12, 4); } + public ByteBuffer influencesInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 12, 4); } public static int createSpawnedBodyTable(FlatBufferBuilder builder, int robotIDsOffset, diff --git a/engine/src/main/battlecode/schema/TeamData.java b/engine/src/main/battlecode/schema/TeamData.java index f9ae90c9..57e3e4db 100644 --- a/engine/src/main/battlecode/schema/TeamData.java +++ b/engine/src/main/battlecode/schema/TeamData.java @@ -13,19 +13,22 @@ */ public final class TeamData extends Table { public static TeamData getRootAsTeamData(ByteBuffer _bb) { return getRootAsTeamData(_bb, new TeamData()); } - public static TeamData getRootAsTeamData(ByteBuffer _bb, TeamData obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public TeamData __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static TeamData getRootAsTeamData(ByteBuffer _bb, TeamData obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public TeamData __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } /** * The name of the team. */ public String name() { int o = __offset(4); return o != 0 ? __string(o + bb_pos) : null; } public ByteBuffer nameAsByteBuffer() { return __vector_as_bytebuffer(4, 1); } + public ByteBuffer nameInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 1); } /** * The java package the team uses. */ public String packageName() { int o = __offset(6); return o != 0 ? __string(o + bb_pos) : null; } public ByteBuffer packageNameAsByteBuffer() { return __vector_as_bytebuffer(6, 1); } + public ByteBuffer packageNameInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 1); } /** * The ID of the team this data pertains to. */ diff --git a/engine/src/main/battlecode/schema/VecTable.java b/engine/src/main/battlecode/schema/VecTable.java index 6201eb5b..82da91e7 100644 --- a/engine/src/main/battlecode/schema/VecTable.java +++ b/engine/src/main/battlecode/schema/VecTable.java @@ -13,15 +13,18 @@ */ public final class VecTable extends Table { public static VecTable getRootAsVecTable(ByteBuffer _bb) { return getRootAsVecTable(_bb, new VecTable()); } - public static VecTable getRootAsVecTable(ByteBuffer _bb, VecTable obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } - public VecTable __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; } + public static VecTable getRootAsVecTable(ByteBuffer _bb, VecTable obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; vtable_start = bb_pos - bb.getInt(bb_pos); vtable_size = bb.getShort(vtable_start); } + public VecTable __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } public int xs(int j) { int o = __offset(4); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int xsLength() { int o = __offset(4); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer xsAsByteBuffer() { return __vector_as_bytebuffer(4, 4); } + public ByteBuffer xsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 4, 4); } public int ys(int j) { int o = __offset(6); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } public int ysLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer ysAsByteBuffer() { return __vector_as_bytebuffer(6, 4); } + public ByteBuffer ysInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 4); } public static int createVecTable(FlatBufferBuilder builder, int xsOffset, diff --git a/engine/src/main/battlecode/server/Config.java b/engine/src/main/battlecode/server/Config.java index adcd05fe..751ce488 100644 --- a/engine/src/main/battlecode/server/Config.java +++ b/engine/src/main/battlecode/server/Config.java @@ -58,6 +58,8 @@ public class Config { defaults.setProperty("bc.engine.silence-c", "false"); defaults.setProperty("bc.engine.silence-d", "false"); defaults.setProperty("bc.engine.debug-methods", "false"); + defaults.setProperty("bc.engine.enable-profiler", "false"); + defaults.setProperty("bc.engine.show-indicators", "true"); defaults.setProperty("bc.game.team-a", "team000"); defaults.setProperty("bc.game.team-b", "team000"); diff --git a/engine/src/main/battlecode/server/GameMaker.java b/engine/src/main/battlecode/server/GameMaker.java index a274684f..f548f134 100644 --- a/engine/src/main/battlecode/server/GameMaker.java +++ b/engine/src/main/battlecode/server/GameMaker.java @@ -4,6 +4,9 @@ import battlecode.common.MapLocation; import battlecode.common.RobotType; import battlecode.common.Team; +import battlecode.instrumenter.profiler.Profiler; +import battlecode.instrumenter.profiler.ProfilerCollection; +import battlecode.instrumenter.profiler.ProfilerEventType; import battlecode.schema.*; import battlecode.util.FlatHelpers; import battlecode.util.TeamMapping; @@ -13,6 +16,7 @@ import gnu.trove.list.array.TFloatArrayList; import gnu.trove.list.array.TIntArrayList; import gnu.trove.list.array.TCharArrayList; +import java.util.List; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -98,11 +102,17 @@ private enum State { */ private final MatchMaker matchMaker; + /** + * Whether to serialize indicator dots and lines into the flatbuffer. + */ + private final boolean showIndicators; + /** * @param gameInfo the mapping of teams to bytes * @param packetSink the NetServer to send packets to + * @param showIndicators whether to write indicator dots and lines to replay */ - public GameMaker(final GameInfo gameInfo, final NetServer packetSink){ + public GameMaker(final GameInfo gameInfo, final NetServer packetSink, final boolean showIndicators) { this.state = State.GAME_HEADER; this.gameInfo = gameInfo; @@ -119,6 +129,8 @@ public GameMaker(final GameInfo gameInfo, final NetServer packetSink){ this.matchFooters = new TIntArrayList(); this.matchMaker = new MatchMaker(); + + this.showIndicators = showIndicators; } /** @@ -329,6 +341,7 @@ public class MatchMaker { private TIntArrayList teamIDs; private TIntArrayList teamVotes; private TIntArrayList teamBidderIDs; + private TIntArrayList teamNumBuffs; // Indicator dots with locations and RGB values private TIntArrayList indicatorDotIDs; @@ -372,6 +385,7 @@ public MatchMaker() { this.teamIDs = new TIntArrayList(); this.teamVotes = new TIntArrayList(); this.teamBidderIDs = new TIntArrayList(); + this.teamNumBuffs = new TIntArrayList(); this.indicatorDotIDs = new TIntArrayList(); this.indicatorDotLocsX = new TIntArrayList(); this.indicatorDotLocsY = new TIntArrayList(); @@ -406,11 +420,51 @@ public void makeMatchHeader(LiveMap gameMap) { clearData(); } - public void makeMatchFooter(Team winTeam, int totalRounds) { + public void makeMatchFooter(Team winTeam, int totalRounds, List profilerCollections) { changeState(State.IN_MATCH, State.IN_GAME); - createEvent((builder) -> EventWrapper.createEventWrapper(builder, Event.MatchFooter, - MatchFooter.createMatchFooter(builder, TeamMapping.id(winTeam), totalRounds))); + createEvent((builder) -> { + TIntArrayList profilerFiles = new TIntArrayList(); + + for (ProfilerCollection profilerCollection : profilerCollections) { + TIntArrayList frames = new TIntArrayList(); + TIntArrayList profiles = new TIntArrayList(); + + for (String frame : profilerCollection.getFrames()) { + frames.add(builder.createString(frame)); + } + + for (Profiler profiler : profilerCollection.getProfilers()) { + TIntArrayList events = new TIntArrayList(); + + for (battlecode.instrumenter.profiler.ProfilerEvent event : profiler.getEvents()) { + ProfilerEvent.startProfilerEvent(builder); + ProfilerEvent.addIsOpen(builder, event.getType() == ProfilerEventType.OPEN); + ProfilerEvent.addAt(builder, event.getAt()); + ProfilerEvent.addFrame(builder, event.getFrameId()); + events.add(ProfilerEvent.endProfilerEvent(builder)); + } + + int nameOffset = builder.createString(profiler.getName()); + int eventsOffset = ProfilerProfile.createEventsVector(builder, events.toArray()); + + ProfilerProfile.startProfilerProfile(builder); + ProfilerProfile.addName(builder, nameOffset); + ProfilerProfile.addEvents(builder, eventsOffset); + profiles.add(ProfilerProfile.endProfilerProfile(builder)); + } + + int framesOffset = ProfilerFile.createFramesVector(builder, frames.toArray()); + int profilesOffset = ProfilerFile.createProfilesVector(builder, profiles.toArray()); + + profilerFiles.add(ProfilerFile.createProfilerFile(builder, framesOffset, profilesOffset)); + } + + int profilerFilesOffset = MatchFooter.createProfilerFilesVector(builder, profilerFiles.toArray()); + + return EventWrapper.createEventWrapper(builder, Event.MatchFooter, + MatchFooter.createMatchFooter(builder, TeamMapping.id(winTeam), totalRounds, profilerFilesOffset)); + }); matchFooters.add(events.size() - 1); } @@ -445,6 +499,7 @@ public void makeRound(int roundNum) { int teamIDsP = Round.createTeamIDsVector(builder, teamIDs.toArray()); int teamVotesP = Round.createTeamVotesVector(builder, teamVotes.toArray()); int teamBidderIDsP = Round.createTeamBidderIDsVector(builder, teamBidderIDs.toArray()); + int teamNumBuffsP = Round.createTeamNumBuffsVector(builder, teamNumBuffs.toArray()); // The bodies that moved int movedIDsP = Round.createMovedIDsVector(builder, movedIDs.toArray()); @@ -479,6 +534,7 @@ public void makeRound(int roundNum) { Round.addTeamIDs(builder, teamIDsP); Round.addTeamVotes(builder, teamVotesP); Round.addTeamBidderIDs(builder, teamBidderIDsP); + Round.addTeamNumBuffs(builder, teamNumBuffsP); Round.addMovedIDs(builder, movedIDsP); Round.addMovedLocs(builder, movedLocsP); Round.addSpawnedBodies(builder, spawnedBodiesP); @@ -527,13 +583,17 @@ public void addAction(int userID, byte action, int targetID) { actionTargets.add(targetID); } - public void addTeamVote(Team team, int vote, int bidderID) { + public void addTeamInfo(Team team, int vote, int bidderID, int numBuffs) { teamIDs.add(TeamMapping.id(team)); teamVotes.add(vote); teamBidderIDs.add(bidderID); + teamNumBuffs.add(numBuffs); } public void addIndicatorDot(int id, MapLocation loc, int red, int green, int blue) { + if (!showIndicators) { + return; + } indicatorDotIDs.add(id); indicatorDotLocsX.add(loc.x); indicatorDotLocsY.add(loc.y); @@ -543,6 +603,9 @@ public void addIndicatorDot(int id, MapLocation loc, int red, int green, int blu } public void addIndicatorLine(int id, MapLocation startLoc, MapLocation endLoc, int red, int green, int blue) { + if (!showIndicators) { + return; + } indicatorLineIDs.add(id); indicatorLineStartLocsX.add(startLoc.x); indicatorLineStartLocsY.add(startLoc.y); @@ -584,6 +647,7 @@ private void clearData() { teamIDs.clear(); teamVotes.clear(); teamBidderIDs.clear(); + teamNumBuffs.clear(); indicatorDotIDs.clear(); indicatorDotLocsX.clear(); indicatorDotLocsY.clear(); diff --git a/engine/src/main/battlecode/server/Server.java b/engine/src/main/battlecode/server/Server.java index 821d956a..8b8dd891 100644 --- a/engine/src/main/battlecode/server/Server.java +++ b/engine/src/main/battlecode/server/Server.java @@ -146,13 +146,14 @@ public void run() { return; } - GameMaker gameMaker = new GameMaker(currentGame, netServer); + GameMaker gameMaker = new GameMaker(currentGame, netServer, options.getBoolean("bc.engine.show-indicators")); gameMaker.makeGameHeader(); debug("Running: "+currentGame); // Set up our control provider - final RobotControlProvider prov = createControlProvider(currentGame, gameMaker); + final boolean profilingEnabled = options.getBoolean("bc.engine.enable-profiler"); + final RobotControlProvider prov = createControlProvider(currentGame, gameMaker, profilingEnabled); // Count wins int aWins = 0, bWins = 0; @@ -275,10 +276,14 @@ private Team runMatch(GameInfo currentGame, /** * Create a RobotControlProvider for a game. * - * @param game the game to provide control for + * @param game the game to provide control for + * @param gameMaker the game maker containing the output streams for robot logs + * @param profilingEnabled whether profiling is enabled or not * @return a fresh control provider for the game */ - private RobotControlProvider createControlProvider(GameInfo game, GameMaker gameMaker) { + private RobotControlProvider createControlProvider(GameInfo game, + GameMaker gameMaker, + boolean profilingEnabled) { // Strictly speaking, this should probably be somewhere in battlecode.world // Whatever @@ -286,11 +291,27 @@ private RobotControlProvider createControlProvider(GameInfo game, GameMaker game teamProvider.registerControlProvider( Team.A, - new PlayerControlProvider(game.getTeamAPackage(), game.getTeamAURL(), gameMaker.getMatchMaker().getOut()) + new PlayerControlProvider( + Team.A, + game.getTeamAPackage(), + game.getTeamAURL(), + gameMaker.getMatchMaker().getOut(), + profilingEnabled + ) ); teamProvider.registerControlProvider( Team.B, - new PlayerControlProvider(game.getTeamBPackage(), game.getTeamBURL(), gameMaker.getMatchMaker().getOut()) + new PlayerControlProvider( + Team.B, + game.getTeamBPackage(), + game.getTeamBURL(), + gameMaker.getMatchMaker().getOut(), + profilingEnabled + ) + ); + teamProvider.registerControlProvider( + Team.NEUTRAL, + new NullControlProvider() ); return teamProvider; } diff --git a/engine/src/main/battlecode/world/BuildMaps.java b/engine/src/main/battlecode/world/BuildMaps.java index 832f58f5..148f9f2d 100644 --- a/engine/src/main/battlecode/world/BuildMaps.java +++ b/engine/src/main/battlecode/world/BuildMaps.java @@ -18,6 +18,8 @@ public class BuildMaps { */ public static void main(String[] args) { MapTestSmall.main(args); + Circle.main(args); + Quadrants.main(args); } } diff --git a/engine/src/main/battlecode/world/GameWorld.java b/engine/src/main/battlecode/world/GameWorld.java index ec66cbb6..b4ff29ae 100644 --- a/engine/src/main/battlecode/world/GameWorld.java +++ b/engine/src/main/battlecode/world/GameWorld.java @@ -1,6 +1,7 @@ package battlecode.world; import battlecode.common.*; +import battlecode.instrumenter.profiler.ProfilerCollection; import battlecode.schema.Action; import battlecode.server.ErrorReporter; import battlecode.server.GameMaker; @@ -33,6 +34,8 @@ public strictfp class GameWorld { private final TeamInfo teamInfo; private final ObjectInfo objectInfo; + private Map profilerCollections; + private final RobotControlProvider controlProvider; private Random rand; private final GameMaker.MatchMaker matchMaker; @@ -51,6 +54,8 @@ public GameWorld(LiveMap gm, RobotControlProvider cp, GameMaker.MatchMaker match this.objectInfo = new ObjectInfo(gm); this.teamInfo = new TeamInfo(this); + this.profilerCollections = new HashMap<>(); + this.controlProvider = cp; this.rand = new Random(this.gameMap.getSeed()); this.matchMaker = matchMaker; @@ -79,8 +84,14 @@ public GameWorld(LiveMap gm, RobotControlProvider cp, GameMaker.MatchMaker match */ public synchronized GameState runRound() { if (!this.isRunning()) { + List profilers = new ArrayList<>(2); + if (!profilerCollections.isEmpty()) { + profilers.add(profilerCollections.get(Team.A)); + profilers.add(profilerCollections.get(Team.B)); + } + // Write match footer if game is done - matchMaker.makeMatchFooter(gameStats.getWinner(), currentRound); + matchMaker.makeMatchFooter(gameStats.getWinner(), currentRound, profilers); return GameState.DONE; } @@ -353,7 +364,7 @@ public void setWinnerArbitrary() { } public boolean timeLimitReached() { - return currentRound >= this.gameMap.getRounds() - 1; + return currentRound >= this.gameMap.getRounds(); } public void processEndOfRound() { @@ -362,14 +373,16 @@ public void processEndOfRound() { // Process end of each robot's round objectInfo.eachRobot((robot) -> { - int bid = robot.getBid(); - int teamIdx = robot.getTeam().ordinal(); - if (bid > highestBids[teamIdx] || highestBidders[teamIdx] == null || - (bid == highestBids[teamIdx] && robot.compareTo(highestBidders[teamIdx]) < 0)) { - highestBids[teamIdx] = bid; - highestBidders[teamIdx] = robot; + if (robot.getTeam().isPlayer() && robot.getType().canBid()) { + int bid = robot.getBid(); + int teamIdx = robot.getTeam().ordinal(); + if (bid > highestBids[teamIdx] || highestBidders[teamIdx] == null || + (bid == highestBids[teamIdx] && robot.compareTo(highestBidders[teamIdx]) < 0)) { + highestBids[teamIdx] = bid; + highestBidders[teamIdx] = robot; + } + robot.resetBid(); } - robot.resetBid(); robot.processEndOfRound(); return true; }); @@ -397,13 +410,10 @@ public void processEndOfRound() { // Didn't win. If didn't place bid, halfBid == 0 int halfBid = (highestBids[i] + 1) / 2; highestBidders[i].addInfluenceAndConviction(-halfBid); + teamBidderIDs[i] = highestBidders[i].getID(); } } - // Send teamVotes and teamBidderIDs to matchmaker - for (int i = 0; i < 2; i++) - this.matchMaker.addTeamVote(Team.values()[i], teamVotes[i], teamBidderIDs[i]); - // Add buffs from expose int nextRound = currentRound + 1; for (int i = 0; i < 2; i++) { @@ -411,6 +421,10 @@ public void processEndOfRound() { this.buffsToAdd[i] = 0; // reset } + // Send team info (votes, bidder IDs, and num buffs) to matchmaker + for (int i = 0; i < 2; i++) + this.matchMaker.addTeamInfo(Team.values()[i], teamVotes[i], teamBidderIDs[i], this.teamInfo.getNumBuffs(Team.values()[i], nextRound)); + // Check for end of match setWinnerIfAnnihilated(); if (timeLimitReached() && gameStats.getWinner() == null) @@ -457,6 +471,18 @@ public void destroyRobot(int id) { matchMaker.addDied(id); } + + // ********************************* + // ******* PROFILER ************** + // ********************************* + + public void setProfilerCollection(Team team, ProfilerCollection profilerCollection) { + if (profilerCollections == null) { + profilerCollections = new HashMap<>(); + } + + profilerCollections.put(team, profilerCollection); + } } diff --git a/engine/src/main/battlecode/world/InternalRobot.java b/engine/src/main/battlecode/world/InternalRobot.java index 134f24ae..772e94ae 100644 --- a/engine/src/main/battlecode/world/InternalRobot.java +++ b/engine/src/main/battlecode/world/InternalRobot.java @@ -1,6 +1,5 @@ package battlecode.world; -import java.util.Arrays; import java.util.ArrayList; import battlecode.common.*; import battlecode.schema.Action; @@ -66,7 +65,7 @@ public InternalRobot(GameWorld gw, InternalRobot parent, int id, RobotType type, this.location = loc; this.influence = influence; this.conviction = (int) Math.ceil(this.type.convictionRatio * this.influence); - this.convictionCap = type == RobotType.ENLIGHTENMENT_CENTER ? Integer.MAX_VALUE : this.conviction; + this.convictionCap = type == RobotType.ENLIGHTENMENT_CENTER ? GameConstants.ROBOT_INFLUENCE_LIMIT : this.conviction; this.flag = 0; this.bid = 0; @@ -287,12 +286,18 @@ public void addCooldownTurns() { * @param influenceAmount the amount to change influence by (can be negative) */ public void addInfluenceAndConviction(int influenceAmount) { + int oldInfluence = this.influence; this.influence += influenceAmount; + if (this.influence > GameConstants.ROBOT_INFLUENCE_LIMIT) { + this.influence = GameConstants.ROBOT_INFLUENCE_LIMIT; + } this.conviction = this.influence; - this.gameWorld.getMatchMaker().addAction(getID(), Action.CHANGE_INFLUENCE, influenceAmount); - this.gameWorld.getMatchMaker().addAction(getID(), Action.CHANGE_CONVICTION, influenceAmount); + if (this.influence != oldInfluence) { + this.gameWorld.getMatchMaker().addAction(getID(), Action.CHANGE_INFLUENCE, this.influence - oldInfluence); + this.gameWorld.getMatchMaker().addAction(getID(), Action.CHANGE_CONVICTION, this.influence - oldInfluence); + } } - + /** * Sets the action cooldown given the number of turns. * @@ -359,26 +364,37 @@ public void empower(int radiusSquared) { int numBots = robots.length - 1; // excluding self if (numBots == 0) return; - - int convictionToGive = (int) (this.conviction * this.gameWorld.getTeamInfo().getBuff(this.team)); - convictionToGive -= GameConstants.EMPOWER_TAX; + + double convictionToGive = this.conviction - GameConstants.EMPOWER_TAX; if (convictionToGive <= 0) return; - - int convictionPerBot = convictionToGive / numBots; - int numBotsWithExtraConviction = convictionToGive % numBots; - Arrays.sort(robots); + final double convictionPerBot = convictionToGive / numBots; + final double buff = this.gameWorld.getTeamInfo().getBuff(this.team); + for (InternalRobot bot : robots) { // check if this robot if (bot.equals(this)) continue; - int conv = convictionPerBot; - if (numBotsWithExtraConviction > 0) { - conv++; - numBotsWithExtraConviction--; + + double conv = convictionPerBot; + if (bot.type == RobotType.ENLIGHTENMENT_CENTER && bot.team == this.team) { + // conviction doesn't get buffed, do nothing + } else if (bot.type == RobotType.ENLIGHTENMENT_CENTER) { + // complicated stuff + double convNeededToConvert = bot.conviction / buff; + if (conv <= convNeededToConvert) { + // all of conviction is buffed + conv *= buff; + } else { + // conviction buffed until conversion + conv = bot.conviction + (conv - convNeededToConvert); + } + } else { + // buff applied, cast down + conv *= buff; } - bot.empowered(this, conv, this.team); + bot.empowered(this, (int) conv, this.team); } // create new bots @@ -386,7 +402,14 @@ public void empower(int radiusSquared) { RobotInfo info = toCreate.get(i); int id = this.gameWorld.spawnRobot(toCreateParents.get(i), info.getType(), info.getLocation(), this.team, info.getInfluence()); InternalRobot newBot = this.gameWorld.getObjectInfo().getRobotByID(id); - newBot.addConviction(info.getConviction() - newBot.getConviction()); + if (newBot.type != RobotType.ENLIGHTENMENT_CENTER) { + // Shouldn't be called on an enlightenment center, because if spawned center's influence exceeds limit this would send a redundant change conviction action. + newBot.addConviction(info.getConviction() - newBot.getConviction()); + } + else { + // Resets influence and conviction to cap for enlightenment centers. Already done by reset bid, but nicer to do it here. + newBot.addInfluenceAndConviction(0); + } this.gameWorld.getMatchMaker().addAction(info.getID(), Action.CHANGE_TEAM, id); } } @@ -397,10 +420,10 @@ public void empower(int radiusSquared) { * Called when a Politician Empowers. * If conviction becomes negative, the robot switches teams or is destroyed. * - * @param amount the amount this robot is empowered by, must be positive + * @param amount the amount this robot's conviction changes by (including all buffs) * @param newTeam the team of the robot that empowered */ - public void empowered(InternalRobot caller, int amount, Team newTeam) { + private void empowered(InternalRobot caller, int amount, Team newTeam) { if (this.team != newTeam) amount = -amount; @@ -409,11 +432,11 @@ public void empowered(InternalRobot caller, int amount, Team newTeam) { else addConviction(amount); - if (conviction < 0) { + if (this.conviction < 0) { if (this.type.canBeConverted()) { - int influence = this.type == RobotType.ENLIGHTENMENT_CENTER ? -this.influence : this.influence; - int conviction = -this.conviction; - caller.addToCreate(this.parent, this.ID, this.type, influence, conviction, this.location); + int newInfluence = Math.abs(this.influence); + int newConviction = -this.conviction; + caller.addToCreate(this.parent, this.ID, this.type, newInfluence, newConviction, this.location); } this.gameWorld.destroyRobot(getID()); } @@ -457,7 +480,7 @@ public void processEndOfRound() { throw new IllegalStateException("The robot's parent is not an Enlightenment Center"); } int passiveInfluence = this.type.getPassiveInfluence(this.influence, this.roundsAlive, this.gameWorld.getCurrentRound()); - if (passiveInfluence > 0) { + if (passiveInfluence > 0 && this.team.isPlayer() && this.gameWorld.getObjectInfo().existsRobot(target.ID)) { target.addInfluenceAndConviction(passiveInfluence); if (this.type == RobotType.SLANDERER) { this.gameWorld.getMatchMaker().addAction(this.ID, Action.EMBEZZLE, target.ID); diff --git a/engine/src/main/battlecode/world/MapBuilder.java b/engine/src/main/battlecode/world/MapBuilder.java index 1b8243e0..31b1547b 100644 --- a/engine/src/main/battlecode/world/MapBuilder.java +++ b/engine/src/main/battlecode/world/MapBuilder.java @@ -183,6 +183,8 @@ public void assertIsValid() { for (RobotInfo r : bodies) { assert robots[locationToIndex(r.location.x, r.location.y)] == null; robots[locationToIndex(r.location.x, r.location.y)] = r; + if (r.influence < 50 || r.influence > 500) // this really should be a GameConstant, but oh well + throw new RuntimeException("Influence not in [50, 500]"); } @@ -202,6 +204,16 @@ public void assertIsValid() { throw new RuntimeException("Map must have starting robots of each team"); } + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + MapLocation current = new MapLocation(x, y); + // This should also be a GameConstant, but I'm speed-coding this and we can fix this later + if (passabilityArray[locationToIndex(current.x, current.y)] < 0.1 || passabilityArray[locationToIndex(current.x, current.y)] > 1.0) { + throw new RuntimeException("Map passability not between 0.1 and 1.0"); + } + } + } + // assert passability and Enlightenment Center symmetry ArrayList allMapSymmetries = getSymmetry(robots); System.out.println("This map has the following symmetries: " + allMapSymmetries); diff --git a/engine/src/main/battlecode/world/RobotControllerImpl.java b/engine/src/main/battlecode/world/RobotControllerImpl.java index 62139099..9528dd7c 100644 --- a/engine/src/main/battlecode/world/RobotControllerImpl.java +++ b/engine/src/main/battlecode/world/RobotControllerImpl.java @@ -414,9 +414,13 @@ public void buildRobot(RobotType type, Direction dir, int influence) throws Game this.robot.addCooldownTurns(); this.robot.addInfluenceAndConviction(-influence); - gameWorld.getMatchMaker().addAction(getID(), Action.CHANGE_INFLUENCE, -influence); int robotID = gameWorld.spawnRobot(this.robot, type, adjacentLocation(dir), getTeam(), influence); + + // set cooldown turns here, because not all new robots have cooldown (eg. switching teams) + InternalRobot newBot = getRobotByID(robotID); + newBot.setCooldownTurns(type.initialCooldown); + gameWorld.getMatchMaker().addAction(getID(), Action.SPAWN_UNIT, robotID); } @@ -448,7 +452,7 @@ public void empower(int radiusSquared) throws GameActionException { this.robot.addCooldownTurns(); // not needed but here for the sake of consistency this.robot.empower(radiusSquared); - gameWorld.getMatchMaker().addAction(getID(), Action.EMPOWER, -1); + gameWorld.getMatchMaker().addAction(getID(), Action.EMPOWER, radiusSquared); // self-destruct gameWorld.destroyRobot(this.robot.getID()); @@ -465,7 +469,7 @@ private void assertCanExpose(MapLocation loc) throws GameActionException { throw new GameActionException(CANT_DO_THAT, "Robot is of type " + getType() + " which cannot expose."); if (!onTheMap(loc)) - throw new GameActionException(CANT_DO_THAT, + throw new GameActionException(OUT_OF_RANGE, "Location is not on the map."); if (!this.robot.canActLocation(loc)) throw new GameActionException(CANT_DO_THAT, @@ -482,6 +486,26 @@ private void assertCanExpose(MapLocation loc) throws GameActionException { "Robot at target location is not on the enemy team."); } + private void assertCanExpose(int id) throws GameActionException { + assertIsReady(); + if (!getType().canExpose()) + throw new GameActionException(CANT_DO_THAT, + "Robot is of type " + getType() + " which cannot expose."); + if (!canSenseRobot(id)) + throw new GameActionException(OUT_OF_RANGE, + "The targeted robot cannot be sensed."); + InternalRobot bot = getRobotByID(id); + if (!this.robot.canActLocation(bot.getLocation())) + throw new GameActionException(OUT_OF_RANGE, + "Robot can't be exposed because it is out of range."); + if (!(bot.getType().canBeExposed())) + throw new GameActionException(CANT_DO_THAT, + "Robot is not of a type that can be exposed."); + if (bot.getTeam() == getTeam()) + throw new GameActionException(CANT_DO_THAT, + "Robot is not on the enemy team."); + } + @Override public boolean canExpose(MapLocation loc) { try { @@ -489,6 +513,14 @@ public boolean canExpose(MapLocation loc) { return true; } catch (GameActionException e) { return false; } } + + @Override + public boolean canExpose(int id) { + try { + assertCanExpose(id); + return true; + } catch (GameActionException e) { return false; } + } @Override public void expose(MapLocation loc) throws GameActionException { @@ -501,6 +533,16 @@ public void expose(MapLocation loc) throws GameActionException { gameWorld.getMatchMaker().addAction(getID(), Action.EXPOSE, exposedID); } + @Override + public void expose(int id) throws GameActionException { + assertCanExpose(id); + + this.robot.addCooldownTurns(); + InternalRobot bot = getRobotByID(id); + this.robot.expose(bot); + gameWorld.getMatchMaker().addAction(getID(), Action.EXPOSE, id); + } + // *********************************** // *** ENLIGHTENMENT CENTER METHODS ** // *********************************** @@ -564,7 +606,9 @@ private void assertCanGetFlag(int id) throws GameActionException { if (bot == null) throw new GameActionException(CANT_DO_THAT, "Robot of given ID does not exist."); - if (bot.getType() != RobotType.ENLIGHTENMENT_CENTER && !canSenseLocation(bot.getLocation())) + if (getType() != RobotType.ENLIGHTENMENT_CENTER && + bot.getType() != RobotType.ENLIGHTENMENT_CENTER && + !canSenseLocation(bot.getLocation())) throw new GameActionException(CANT_SENSE_THAT, "Robot at location is out of sensor range and not an Enlightenment Center."); } diff --git a/engine/src/main/battlecode/world/TeamInfo.java b/engine/src/main/battlecode/world/TeamInfo.java index 270cc635..045536a9 100644 --- a/engine/src/main/battlecode/world/TeamInfo.java +++ b/engine/src/main/battlecode/world/TeamInfo.java @@ -35,17 +35,17 @@ public int getVotes(Team t) { // returns current buff public double getBuff(Team t) { - return Math.pow(GameConstants.EXPOSE_BUFF_FACTOR, this.numBuffs[t.ordinal()]); + return 1 + GameConstants.EXPOSE_BUFF_FACTOR * this.numBuffs[t.ordinal()]; } // returns the buff at specified round public double getBuff(Team t, int roundNumber) { int buffs = getNumBuffs(t, roundNumber); - return Math.pow(GameConstants.EXPOSE_BUFF_FACTOR, buffs); + return 1 + GameConstants.EXPOSE_BUFF_FACTOR * buffs; } // returns the number of buffs at specified round - private int getNumBuffs(Team t, int roundNumber) { + public int getNumBuffs(Team t, int roundNumber) { int teamIdx = t.ordinal(); int buffs = numBuffs[teamIdx]; TreeMap map = this.buffExpirations.get(teamIdx); diff --git a/engine/src/main/battlecode/world/control/PlayerControlProvider.java b/engine/src/main/battlecode/world/control/PlayerControlProvider.java index 2f3a25df..128bf86b 100644 --- a/engine/src/main/battlecode/world/control/PlayerControlProvider.java +++ b/engine/src/main/battlecode/world/control/PlayerControlProvider.java @@ -1,8 +1,11 @@ package battlecode.world.control; +import battlecode.common.Team; import battlecode.instrumenter.InstrumentationException; import battlecode.instrumenter.TeamClassLoaderFactory; import battlecode.instrumenter.SandboxedRobotPlayer; +import battlecode.instrumenter.profiler.Profiler; +import battlecode.instrumenter.profiler.ProfilerCollection; import battlecode.server.ErrorReporter; import battlecode.world.GameWorld; import battlecode.world.InternalRobot; @@ -51,31 +54,66 @@ public class PlayerControlProvider implements RobotControlProvider { */ private final OutputStream robotOut; + /** + * The team this control provider controls. + */ + private final Team team; + + /** + * The ProfilerCollection instance holding the profilers for the team. + * Null if profiling is disabled. + */ + private ProfilerCollection profilerCollection; + + /** + * The match id of the current match. Incremented by one every time a new match starts. + */ + private int matchId = -1; + /** * Create a new PlayerControlProvider. - * @param teamPackage the name / package of the team we're loading - * @param teamURL the url of the classes for the team; - * @param robotOut the output that robots should write to + * + * @param team the team we're loading + * @param teamPackage the name / package of the team we're loading + * @param teamURL the url of the classes for the team; + * @param robotOut the output that robots should write to + * @param profilingEnabled whether profiling is enabled or not */ - public PlayerControlProvider(String teamPackage, String teamURL, OutputStream robotOut) { + public PlayerControlProvider(Team team, + String teamPackage, + String teamURL, + OutputStream robotOut, + boolean profilingEnabled) { this.teamPackage = teamPackage; this.sandboxes = new HashMap<>(); // GameWorld maintains order for us this.factory = new TeamClassLoaderFactory(teamURL); this.robotOut = robotOut; + this.team = team; + + if (profilingEnabled) { + profilerCollection = new ProfilerCollection(); + } } @Override public void matchStarted(GameWorld gameWorld) { this.gameWorld = gameWorld; + matchId++; } @Override public void matchEnded() { + if (profilerCollection != null) { + gameWorld.setProfilerCollection(team, profilerCollection); + profilerCollection = new ProfilerCollection(); + } + for (final SandboxedRobotPlayer player : this.sandboxes.values()) { if (player != null && !player.getTerminated()) { player.terminate(); } } + this.sandboxes.clear(); this.gameWorld = null; } @@ -83,12 +121,18 @@ public void matchEnded() { @Override public void robotSpawned(InternalRobot robot) { try { + Profiler profiler = null; + if (profilerCollection != null && robot.getTeam() == team) { + profiler = profilerCollection.createProfiler(robot.getID(), robot.getType()); + } + final SandboxedRobotPlayer player = new SandboxedRobotPlayer( teamPackage, robot.getController(), robot.getID(), - factory.createLoader(), - robotOut + factory.createLoader(profiler != null), + robotOut, + profiler ); this.sandboxes.put(robot.getID(), player); } catch (InstrumentationException e) { diff --git a/engine/src/main/battlecode/world/maps/Circle.java b/engine/src/main/battlecode/world/maps/Circle.java new file mode 100644 index 00000000..272f51eb --- /dev/null +++ b/engine/src/main/battlecode/world/maps/Circle.java @@ -0,0 +1,59 @@ +package battlecode.world.maps; + +import battlecode.common.MapLocation; +import battlecode.common.RobotType; +import battlecode.common.Team; +import battlecode.world.GameMapIO; +import battlecode.world.LiveMap; +import battlecode.world.MapBuilder; +import battlecode.world.TestMapBuilder; + +import battlecode.common.GameConstants; + +import java.io.File; +import java.io.IOException; +import java.util.Random; + +/** + * Generate a map. + */ +public class Circle { + + // change this!!! + public static final String mapName = "circle"; + + // don't change this!! + public static final String outputDirectory = "engine/src/main/battlecode/world/resources/"; + + /** + * @param args unused + */ + public static void main(String[] args) { + try { + makeCircle(); + } catch (IOException e) { + System.out.println(e); + } + System.out.println("Generated a map!"); + } + + public static void makeCircle() throws IOException { + final int half = 31; + final MapLocation center = new MapLocation(half, half); + MapBuilder mapBuilder = new MapBuilder(mapName, 2*half+1, 2*half+1, 25016, 12865, 116896); + mapBuilder.addSymmetricEnlightenmentCenter(20, 20); + mapBuilder.addSymmetricEnlightenmentCenter(20, 2*half-20); + mapBuilder.addSymmetricNeutralEnlightenmentCenter(0, 0, 300); + mapBuilder.addSymmetricNeutralEnlightenmentCenter(0, 2*half, 300); + + for(int i = 0; i <= half; i++) { + for (int j = 0; j <= 2*half; j++) { + int d = new MapLocation(i, j).distanceSquaredTo(center); + mapBuilder.setSymmetricPassability(i, j, + 1.0 - 0.5 * Math.exp(-0.0002 * (d - 500) * (d - 500))); + } + } + + mapBuilder.saveMap(outputDirectory); + } +} diff --git a/engine/src/main/battlecode/world/maps/Quadrants.java b/engine/src/main/battlecode/world/maps/Quadrants.java new file mode 100644 index 00000000..a041ee0c --- /dev/null +++ b/engine/src/main/battlecode/world/maps/Quadrants.java @@ -0,0 +1,56 @@ +package battlecode.world.maps; + +import battlecode.common.MapLocation; +import battlecode.common.RobotType; +import battlecode.common.Team; +import battlecode.world.GameMapIO; +import battlecode.world.LiveMap; +import battlecode.world.MapBuilder; +import battlecode.world.TestMapBuilder; + +import battlecode.common.GameConstants; + +import java.io.File; +import java.io.IOException; +import java.util.Random; + +/** + * Generate a map. + */ +public class Quadrants { + + // change this!!! + public static final String mapName = "quadrants"; + + // don't change this!! + public static final String outputDirectory = "engine/src/main/battlecode/world/resources/"; + + /** + * @param args unused + */ + public static void main(String[] args) { + try { + makeQuadrants(); + } catch (IOException e) { + System.out.println(e); + } + System.out.println("Generated a map!"); + } + + public static void makeQuadrants() throws IOException { + MapBuilder mapBuilder = new MapBuilder(mapName, 40, 40, 13265, 17387, 215957); + mapBuilder.setSymmetry(MapBuilder.MapSymmetry.rotational); + mapBuilder.addSymmetricEnlightenmentCenter(5, 5); + mapBuilder.addSymmetricEnlightenmentCenter(10, 30); + mapBuilder.addSymmetricNeutralEnlightenmentCenter(18, 19, 500); + + for (int i = 5; i <= 15; i++) { + mapBuilder.setSymmetricPassability(i, 15, 0.1); + mapBuilder.setSymmetricPassability(i, 25, 0.1); + mapBuilder.setSymmetricPassability(15, i, 0.1); + mapBuilder.setSymmetricPassability(25, i, 0.1); + } + + mapBuilder.saveMap(outputDirectory); + } +} diff --git a/engine/src/main/battlecode/world/resources/AmidstWe.map21 b/engine/src/main/battlecode/world/resources/AmidstWe.map21 new file mode 100644 index 00000000..39397b34 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/AmidstWe.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Andromeda.map21 b/engine/src/main/battlecode/world/resources/Andromeda.map21 new file mode 100644 index 00000000..f0d8d313 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Andromeda.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Arena.map21 b/engine/src/main/battlecode/world/resources/Arena.map21 new file mode 100644 index 00000000..1d5d2309 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Arena.map21 differ diff --git a/engine/src/main/battlecode/world/resources/BadSnowflake.map21 b/engine/src/main/battlecode/world/resources/BadSnowflake.map21 new file mode 100644 index 00000000..60e9b68c Binary files /dev/null and b/engine/src/main/battlecode/world/resources/BadSnowflake.map21 differ diff --git a/engine/src/main/battlecode/world/resources/BattleCode.map21 b/engine/src/main/battlecode/world/resources/BattleCode.map21 new file mode 100644 index 00000000..3fb82d09 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/BattleCode.map21 differ diff --git a/engine/src/main/battlecode/world/resources/BattleCodeToo.map21 b/engine/src/main/battlecode/world/resources/BattleCodeToo.map21 new file mode 100644 index 00000000..919a5324 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/BattleCodeToo.map21 differ diff --git a/engine/src/main/battlecode/world/resources/BlobWithLegs.map21 b/engine/src/main/battlecode/world/resources/BlobWithLegs.map21 new file mode 100644 index 00000000..961cc076 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/BlobWithLegs.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Blotches.map21 b/engine/src/main/battlecode/world/resources/Blotches.map21 new file mode 100644 index 00000000..5e1f052f Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Blotches.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Bog.map21 b/engine/src/main/battlecode/world/resources/Bog.map21 new file mode 100644 index 00000000..58961058 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Bog.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Branches.map21 b/engine/src/main/battlecode/world/resources/Branches.map21 new file mode 100644 index 00000000..d96140e7 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Branches.map21 differ diff --git a/engine/src/main/battlecode/world/resources/ButtonsAndBows.map21 b/engine/src/main/battlecode/world/resources/ButtonsAndBows.map21 new file mode 100644 index 00000000..46d00d56 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/ButtonsAndBows.map21 differ diff --git a/engine/src/main/battlecode/world/resources/CToE.map21 b/engine/src/main/battlecode/world/resources/CToE.map21 new file mode 100644 index 00000000..93ad6959 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/CToE.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Chevron.map21 b/engine/src/main/battlecode/world/resources/Chevron.map21 new file mode 100644 index 00000000..4bddf3bb Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Chevron.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Circles.map21 b/engine/src/main/battlecode/world/resources/Circles.map21 new file mode 100644 index 00000000..75c416f2 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Circles.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Corridor.map21 b/engine/src/main/battlecode/world/resources/Corridor.map21 new file mode 100644 index 00000000..a0826128 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Corridor.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Cow.map21 b/engine/src/main/battlecode/world/resources/Cow.map21 new file mode 100644 index 00000000..5c293d82 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Cow.map21 differ diff --git a/engine/src/main/battlecode/world/resources/CowTwister.map21 b/engine/src/main/battlecode/world/resources/CowTwister.map21 new file mode 100644 index 00000000..71bd2d5a Binary files /dev/null and b/engine/src/main/battlecode/world/resources/CowTwister.map21 differ diff --git a/engine/src/main/battlecode/world/resources/CringyAsF.map21 b/engine/src/main/battlecode/world/resources/CringyAsF.map21 new file mode 100644 index 00000000..d29fa78b Binary files /dev/null and b/engine/src/main/battlecode/world/resources/CringyAsF.map21 differ diff --git a/engine/src/main/battlecode/world/resources/CrossStitch.map21 b/engine/src/main/battlecode/world/resources/CrossStitch.map21 new file mode 100644 index 00000000..70ee7e04 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/CrossStitch.map21 differ diff --git a/engine/src/main/battlecode/world/resources/CrownJewels.map21 b/engine/src/main/battlecode/world/resources/CrownJewels.map21 new file mode 100644 index 00000000..b521c9f8 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/CrownJewels.map21 differ diff --git a/engine/src/main/battlecode/world/resources/EggCarton.map21 b/engine/src/main/battlecode/world/resources/EggCarton.map21 new file mode 100644 index 00000000..ff30301b Binary files /dev/null and b/engine/src/main/battlecode/world/resources/EggCarton.map21 differ diff --git a/engine/src/main/battlecode/world/resources/ExesAndOhs.map21 b/engine/src/main/battlecode/world/resources/ExesAndOhs.map21 new file mode 100644 index 00000000..5c356f60 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/ExesAndOhs.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Extensions.map21 b/engine/src/main/battlecode/world/resources/Extensions.map21 new file mode 100644 index 00000000..d303b7cf Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Extensions.map21 differ diff --git a/engine/src/main/battlecode/world/resources/FindYourWay.map21 b/engine/src/main/battlecode/world/resources/FindYourWay.map21 new file mode 100644 index 00000000..5f57d0b6 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/FindYourWay.map21 differ diff --git a/engine/src/main/battlecode/world/resources/FiveOfHearts.map21 b/engine/src/main/battlecode/world/resources/FiveOfHearts.map21 new file mode 100644 index 00000000..f8dd9b8c Binary files /dev/null and b/engine/src/main/battlecode/world/resources/FiveOfHearts.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Flawars.map21 b/engine/src/main/battlecode/world/resources/Flawars.map21 new file mode 100644 index 00000000..b3363dc4 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Flawars.map21 differ diff --git a/engine/src/main/battlecode/world/resources/FrogOrBath.map21 b/engine/src/main/battlecode/world/resources/FrogOrBath.map21 new file mode 100644 index 00000000..83d70e55 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/FrogOrBath.map21 differ diff --git a/engine/src/main/battlecode/world/resources/GetShrekt.map21 b/engine/src/main/battlecode/world/resources/GetShrekt.map21 new file mode 100644 index 00000000..f6677ff2 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/GetShrekt.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Goldfish.map21 b/engine/src/main/battlecode/world/resources/Goldfish.map21 new file mode 100644 index 00000000..116a291d Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Goldfish.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Gridlock.map21 b/engine/src/main/battlecode/world/resources/Gridlock.map21 new file mode 100644 index 00000000..bb518928 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Gridlock.map21 differ diff --git a/engine/src/main/battlecode/world/resources/HappyBoba.map21 b/engine/src/main/battlecode/world/resources/HappyBoba.map21 new file mode 100644 index 00000000..9ebb2dd4 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/HappyBoba.map21 differ diff --git a/engine/src/main/battlecode/world/resources/HexesAndOhms.map21 b/engine/src/main/battlecode/world/resources/HexesAndOhms.map21 new file mode 100644 index 00000000..6616991f Binary files /dev/null and b/engine/src/main/battlecode/world/resources/HexesAndOhms.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Hourglass.map21 b/engine/src/main/battlecode/world/resources/Hourglass.map21 new file mode 100644 index 00000000..ec2d0cf8 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Hourglass.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Illusion.map21 b/engine/src/main/battlecode/world/resources/Illusion.map21 new file mode 100644 index 00000000..51344ee0 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Illusion.map21 differ diff --git a/engine/src/main/battlecode/world/resources/InaccurateBritishFlag.map21 b/engine/src/main/battlecode/world/resources/InaccurateBritishFlag.map21 new file mode 100644 index 00000000..5471ebac Binary files /dev/null and b/engine/src/main/battlecode/world/resources/InaccurateBritishFlag.map21 differ diff --git a/engine/src/main/battlecode/world/resources/JerryIsEvil.map21 b/engine/src/main/battlecode/world/resources/JerryIsEvil.map21 new file mode 100644 index 00000000..6c51af46 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/JerryIsEvil.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Legends.map21 b/engine/src/main/battlecode/world/resources/Legends.map21 new file mode 100644 index 00000000..d83ae96b Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Legends.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Licc.map21 b/engine/src/main/battlecode/world/resources/Licc.map21 new file mode 100644 index 00000000..9c4e687b Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Licc.map21 differ diff --git a/engine/src/main/battlecode/world/resources/MainCampus.map21 b/engine/src/main/battlecode/world/resources/MainCampus.map21 new file mode 100644 index 00000000..83cc6917 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/MainCampus.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Mario.map21 b/engine/src/main/battlecode/world/resources/Mario.map21 new file mode 100644 index 00000000..ed70fbfb Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Mario.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Maze.map21 b/engine/src/main/battlecode/world/resources/Maze.map21 new file mode 100644 index 00000000..98abcdfa Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Maze.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Misdirection.map21 b/engine/src/main/battlecode/world/resources/Misdirection.map21 new file mode 100644 index 00000000..a42e9982 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Misdirection.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Networking.map21 b/engine/src/main/battlecode/world/resources/Networking.map21 new file mode 100644 index 00000000..56b6b022 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Networking.map21 differ diff --git a/engine/src/main/battlecode/world/resources/NextHouse.map21 b/engine/src/main/battlecode/world/resources/NextHouse.map21 new file mode 100644 index 00000000..8ba4153b Binary files /dev/null and b/engine/src/main/battlecode/world/resources/NextHouse.map21 differ diff --git a/engine/src/main/battlecode/world/resources/NoInternet.map21 b/engine/src/main/battlecode/world/resources/NoInternet.map21 new file mode 100644 index 00000000..8ac6d8dc Binary files /dev/null and b/engine/src/main/battlecode/world/resources/NoInternet.map21 differ diff --git a/engine/src/main/battlecode/world/resources/NotAPuzzle.map21 b/engine/src/main/battlecode/world/resources/NotAPuzzle.map21 new file mode 100644 index 00000000..a169143d Binary files /dev/null and b/engine/src/main/battlecode/world/resources/NotAPuzzle.map21 differ diff --git a/engine/src/main/battlecode/world/resources/OneCallAway.map21 b/engine/src/main/battlecode/world/resources/OneCallAway.map21 new file mode 100644 index 00000000..3af0bf05 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/OneCallAway.map21 differ diff --git a/engine/src/main/battlecode/world/resources/PaperWindmill.map21 b/engine/src/main/battlecode/world/resources/PaperWindmill.map21 new file mode 100644 index 00000000..2893370c Binary files /dev/null and b/engine/src/main/battlecode/world/resources/PaperWindmill.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Punctuation.map21 b/engine/src/main/battlecode/world/resources/Punctuation.map21 new file mode 100644 index 00000000..8c88dcc0 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Punctuation.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Radial.map21 b/engine/src/main/battlecode/world/resources/Radial.map21 new file mode 100644 index 00000000..277f066d Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Radial.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Rainbow.map21 b/engine/src/main/battlecode/world/resources/Rainbow.map21 new file mode 100644 index 00000000..adcd26fd Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Rainbow.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Randomized.map21 b/engine/src/main/battlecode/world/resources/Randomized.map21 new file mode 100644 index 00000000..85fff6ea Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Randomized.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Saturn.map21 b/engine/src/main/battlecode/world/resources/Saturn.map21 new file mode 100644 index 00000000..b7e2a4b2 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Saturn.map21 differ diff --git a/engine/src/main/battlecode/world/resources/SeaFloor.map21 b/engine/src/main/battlecode/world/resources/SeaFloor.map21 new file mode 100644 index 00000000..520c6f40 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/SeaFloor.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Sediment.map21 b/engine/src/main/battlecode/world/resources/Sediment.map21 new file mode 100644 index 00000000..8dbef3a6 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Sediment.map21 differ diff --git a/engine/src/main/battlecode/world/resources/SlowMusic.map21 b/engine/src/main/battlecode/world/resources/SlowMusic.map21 new file mode 100644 index 00000000..59c1e693 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/SlowMusic.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Smile.map21 b/engine/src/main/battlecode/world/resources/Smile.map21 new file mode 100644 index 00000000..9edc9464 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Smile.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Snowflake.map21 b/engine/src/main/battlecode/world/resources/Snowflake.map21 new file mode 100644 index 00000000..c17a14a9 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Snowflake.map21 differ diff --git a/engine/src/main/battlecode/world/resources/SpaceInvaders.map21 b/engine/src/main/battlecode/world/resources/SpaceInvaders.map21 new file mode 100644 index 00000000..55fe333e Binary files /dev/null and b/engine/src/main/battlecode/world/resources/SpaceInvaders.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Star.map21 b/engine/src/main/battlecode/world/resources/Star.map21 new file mode 100644 index 00000000..6a5bdb15 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Star.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Stonks.map21 b/engine/src/main/battlecode/world/resources/Stonks.map21 new file mode 100644 index 00000000..57a895b7 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Stonks.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Superposition.map21 b/engine/src/main/battlecode/world/resources/Superposition.map21 new file mode 100644 index 00000000..27db7d44 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Superposition.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Surprised.map21 b/engine/src/main/battlecode/world/resources/Surprised.map21 new file mode 100644 index 00000000..53afb5d5 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Surprised.map21 differ diff --git a/engine/src/main/battlecode/world/resources/TheClientMapEditorIsSuperiorToGoogleSheetsEom.map21 b/engine/src/main/battlecode/world/resources/TheClientMapEditorIsSuperiorToGoogleSheetsEom.map21 new file mode 100644 index 00000000..0edd00a2 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/TheClientMapEditorIsSuperiorToGoogleSheetsEom.map21 differ diff --git a/engine/src/main/battlecode/world/resources/TheSnackThatSmilesBack.map21 b/engine/src/main/battlecode/world/resources/TheSnackThatSmilesBack.map21 new file mode 100644 index 00000000..871eeae4 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/TheSnackThatSmilesBack.map21 differ diff --git a/engine/src/main/battlecode/world/resources/TicTacTie.map21 b/engine/src/main/battlecode/world/resources/TicTacTie.map21 new file mode 100644 index 00000000..73c51f30 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/TicTacTie.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Tiger.map21 b/engine/src/main/battlecode/world/resources/Tiger.map21 new file mode 100644 index 00000000..940fe8dd Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Tiger.map21 differ diff --git a/engine/src/main/battlecode/world/resources/UnbrandedWordGame.map21 b/engine/src/main/battlecode/world/resources/UnbrandedWordGame.map21 new file mode 100644 index 00000000..2cee0cc6 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/UnbrandedWordGame.map21 differ diff --git a/engine/src/main/battlecode/world/resources/VideoGames.map21 b/engine/src/main/battlecode/world/resources/VideoGames.map21 new file mode 100644 index 00000000..dc687338 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/VideoGames.map21 differ diff --git a/engine/src/main/battlecode/world/resources/WhatISeeInMyDreams.map21 b/engine/src/main/battlecode/world/resources/WhatISeeInMyDreams.map21 new file mode 100644 index 00000000..d4ebb3a3 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/WhatISeeInMyDreams.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Yoda.map21 b/engine/src/main/battlecode/world/resources/Yoda.map21 new file mode 100644 index 00000000..a095fd59 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Yoda.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Z.map21 b/engine/src/main/battlecode/world/resources/Z.map21 new file mode 100644 index 00000000..a50ad91c Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Z.map21 differ diff --git a/engine/src/main/battlecode/world/resources/Zodiac.map21 b/engine/src/main/battlecode/world/resources/Zodiac.map21 new file mode 100644 index 00000000..791db140 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Zodiac.map21 differ diff --git a/engine/src/main/battlecode/world/resources/circle.map21 b/engine/src/main/battlecode/world/resources/circle.map21 new file mode 100644 index 00000000..0840c4e9 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/circle.map21 differ diff --git a/engine/src/main/battlecode/world/resources/quadrants.map21 b/engine/src/main/battlecode/world/resources/quadrants.map21 new file mode 100644 index 00000000..7f0c5c99 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/quadrants.map21 differ diff --git a/engine/src/test/battlecode/instrumenter/LoaderTest.java b/engine/src/test/battlecode/instrumenter/LoaderTest.java index 42533673..ec651ebc 100644 --- a/engine/src/test/battlecode/instrumenter/LoaderTest.java +++ b/engine/src/test/battlecode/instrumenter/LoaderTest.java @@ -1,5 +1,6 @@ package battlecode.instrumenter; +import battlecode.instrumenter.profiler.Profiler; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -53,7 +54,7 @@ public static void writeCache() throws Exception { } public TeamClassLoaderFactory.Loader setupLoader(TeamClassLoaderFactory cache) throws Exception { - TeamClassLoaderFactory.Loader result = cache.createLoader(); + TeamClassLoaderFactory.Loader result = cache.createLoader(false); // Set up noop RobotMonitors. // Necessary for... reasons. @@ -66,8 +67,9 @@ public TeamClassLoaderFactory.Loader setupLoader(TeamClassLoaderFactory cache) t monitor1.getMethod("init", SandboxedRobotPlayer.Pauser.class, SandboxedRobotPlayer.Killer.class, - int.class) - .invoke(null, pauser, killer, 0); + int.class, + Profiler.class) + .invoke(null, pauser, killer, 0, null); monitor1.getMethod("setBytecodeLimit", int.class) .invoke(null, Integer.MAX_VALUE); diff --git a/engine/src/test/battlecode/server/GameMakerTest.java b/engine/src/test/battlecode/server/GameMakerTest.java index c8623561..6cf73e5e 100644 --- a/engine/src/test/battlecode/server/GameMakerTest.java +++ b/engine/src/test/battlecode/server/GameMakerTest.java @@ -10,6 +10,7 @@ import battlecode.util.TeamMapping; import battlecode.world.TestMapBuilder; +import java.util.ArrayList; import org.apache.commons.io.IOUtils; import org.junit.Test; import org.mockito.Mockito; @@ -38,22 +39,22 @@ public class GameMakerTest { @Test(expected=RuntimeException.class) public void testStateExceptions() { - GameMaker gm = new GameMaker(info, null); + GameMaker gm = new GameMaker(info, null, true); gm.makeGameFooter(Team.A); } @Test(expected=RuntimeException.class) public void testMatchStateExceptions() { - GameMaker gm = new GameMaker(info, null); + GameMaker gm = new GameMaker(info, null, true); gm.makeGameHeader(); - gm.getMatchMaker().makeMatchFooter(Team.A, 23); + gm.getMatchMaker().makeMatchFooter(Team.A, 23, new ArrayList<>()); } // @Test // public void fullReasonableGame() throws Exception { // NetServer mockServer = Mockito.mock(NetServer.class); - // GameMaker gm = new GameMaker(info, mockServer); + // GameMaker gm = new GameMaker(info, mockServer, true); // gm.makeGameHeader(); // GameMaker.MatchMaker mm = gm.getMatchMaker(); @@ -65,7 +66,7 @@ public void testMatchStateExceptions() { // mm.makeRound(0); // mm.addDied(0); // mm.makeRound(1); - // mm.makeMatchFooter(Team.B, 2); + // mm.makeMatchFooter(Team.B, 2, new ArrayList<>()); // GameMaker.MatchMaker mm2 = gm.getMatchMaker(); // mm2.makeMatchHeader(new TestMapBuilder("argentina", 55, 3, 58, 50, 1337, 50) @@ -73,7 +74,7 @@ public void testMatchStateExceptions() { // .addEnlightenmentCenter(1, Team.B, GameConstants.INITIAL_ENLIGHTENMENT_CENTER_INFLUENCE, new MapLocation(25, 25)) // .build()); // mm2.makeRound(0); - // mm2.makeMatchFooter(Team.A, 1); + // mm2.makeMatchFooter(Team.A, 1, new ArrayList<>()); // gm.makeGameFooter(Team.A); // byte[] gameBytes = ungzip(gm.toBytes()); diff --git a/frontend/.env.development b/frontend/.env.development index ddf3e5f8..9f3c2059 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,2 +1,3 @@ +THIS_URL=http://localhost:3000 REACT_APP_BACKEND_URL=http://localhost:8000 -REACT_APP_REPLAY_URL=http://localhost:8000 +REACT_APP_REPLAY_URL=https://2021.battlecode.org diff --git a/frontend/.env.production b/frontend/.env.production index 299c3967..28a6b478 100644 --- a/frontend/.env.production +++ b/frontend/.env.production @@ -1,2 +1,3 @@ +THIS_URL=https://2021.battlecode.org REACT_APP_BACKEND_URL=https://2021.battlecode.org -REACT_APP_REPLAY_URL=https://2021.battlecode.org \ No newline at end of file +REACT_APP_REPLAY_URL=https://2021.battlecode.org diff --git a/frontend/deploy.sh b/frontend/deploy.sh index 59d3c3fb..a7914791 100755 --- a/frontend/deploy.sh +++ b/frontend/deploy.sh @@ -1,3 +1,5 @@ +# TODO check presence of 2nd argument (version number) + if [ "$1" == "deploy" ] then echo "WARNING: Do you really want to deploy with the game? This SHOULD NEVER BE DONE before the game is released to the world, since this means that the game specs and the visualizer become public." @@ -15,8 +17,45 @@ then # esac # done echo "Proceding with deploy!" - npm install + + # TODO check git status + # (on master, up-to-date) + + rm -r public/specs + mkdir public/specs + cp -r ../specs public + + rm -r public/javadoc + cd .. + # Assumes version as second arg to deploy script + ./gradlew release_docs_zip -Prelease_version=$2 + mv battlecode-javadoc-$2.zip javadoc.zip + unzip -d javadoc javadoc.zip + rm javadoc.zip + mkdir frontend/public/javadoc + mv javadoc frontend/public + cd frontend + # Gets generated somewhere in the javadoc process; is better not to have, to ensure that a different bucket (the battleaccess bucket) always holds this. + rm public/version.txt + + rm -r public/out + cd ../client + cd playback + npm install + npm run build + cd ../visualizer + npm install + npm run prod + mkdir ../../frontend/public/out + cp -r out ../../frontend/public + cd ../../frontend + + npm install npm run build + + rm -r public/specs + rm -r public/javadoc + rm -r public/out cd build gsutil -m rm gs://battlecode21-frontend/** gsutil -m cp -r * gs://battlecode21-frontend diff --git a/frontend/package.json b/frontend/package.json index 38810b1e..0a7de26d 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,7 @@ "startx": "BROWSER=none react-scripts start", "start": "react-scripts start", "build": "rm -rf build && react-scripts build", - "buildnogame": "rm -rf build && rm -rf public/bc20 && rm -rf public/javadoc && rm -rf public/specs.html && react-scripts build", + "buildnogame": "rm -rf build && rm -rf public/out && rm -rf public/javadoc && rm -rf public/specs.html && react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, diff --git a/frontend/public/version.txt b/frontend/public/version.txt deleted file mode 100644 index b53028ea..00000000 --- a/frontend/public/version.txt +++ /dev/null @@ -1 +0,0 @@ -2021.0.0.1 \ No newline at end of file diff --git a/frontend/public/visualizer.html b/frontend/public/visualizer.html index 097adffd..b2a70b63 100644 --- a/frontend/public/visualizer.html +++ b/frontend/public/visualizer.html @@ -10,25 +10,26 @@

- + diff --git a/frontend/src/api.js b/frontend/src/api.js index 37dd960f..efef8064 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -20,6 +20,13 @@ class Api { //uploads a new submission to the google cloud bucket static newSubmission(submissionfile, callback){ + // First check if the user is part of a team + if (Cookies.get('team_id') === null) { + console.log("File cannot be submitted without a team."); + Cookies.set('upload_status_cookie', 12); + return; + } + // URLs which files are uploaded to are generated by the backend; // call the backend api to get this link $.post(`${URL}/api/${LEAGUE}/submission/`) @@ -344,7 +351,7 @@ class Api { static joinTeam(secret_key, team_name, callback) { $.get(`${URL}/api/${LEAGUE}/team/?search=${encodeURIComponent(team_name)}`, (team_data, team_success) => { let found_result = null - team_data.results.forEach(result => { + team_data.forEach(result => { if (result.name === team_name) { found_result = result } @@ -424,7 +431,7 @@ class Api { } static resumeUpload(resume_file, callback) { - $.get(`${Cookies.get('user_url')}resume_upload/`, (data, succcess) => { + $.post(`${Cookies.get('user_url')}resume_upload/`, (data, succcess) => { $.ajax({ url: data['upload_url'], method: "PUT", @@ -495,13 +502,22 @@ class Api { static getAllTeamScrimmages(callback) { $.get(`${URL}/api/${LEAGUE}/scrimmage/`, (data, succcess) => { - callback(data.results); + callback(data); }); } - static getScrimmageHistory(callback) { + /* for some reason the data format from getAllTeamScrimmages and getTeamScrimmages + are different; has to do with pagination but not sure how to make the same + */ + static getTeamScrimmages(callback, page) { + $.get(`${URL}/api/${LEAGUE}/scrimmage/?page=${page}`, (data, succcess) => { + callback(data.results, data.count); + }); + } + + static getScrimmageHistory(callback, page) { const my_id = parseInt(Cookies.get('team_id'), 10); - this.getAllTeamScrimmages((s) => { + this.getTeamScrimmages((s, count) => { const requests = []; for (let i = 0; i < s.length; i++) { const on_red = s[i].red_team === Cookies.get('team_name'); @@ -531,8 +547,11 @@ class Api { s[i].color = on_red ? 'Red' : 'Blue'; requests.push(s[i]); - } callback(requests); - }); + } + // scrimLimit for pagination + const scrimLimit = parseInt(count / PAGE_LIMIT, 10) + !!(count % PAGE_LIMIT); + callback({scrimmages: requests, scrimLimit}); + }, page); } @@ -573,11 +592,16 @@ class Api { static getNextTournament(callback) { // TODO: actually use real API for this callback({ - "est_date_str": '8 PM EDT on April 22, 2020', - "seconds_until": (Date.parse(new Date('April 22, 2020 20:00:00-4:00')) - Date.parse(new Date())) / 1000, - "tournament_name": "Battlehack 2020 Tournament" + "est_date_str": '7 PM ET on January 28, 2021', + "seconds_until": (Date.parse(new Date('January 28, 2021 19:00:00-5:00')) - Date.parse(new Date())) / 1000, + "tournament_name": "Final Tournament" }); // callback({ + // "est_date_str": '8 PM EDT on April 22, 2020', + // "seconds_until": (Date.parse(new Date('April 22, 2020 20:00:00-4:00')) - Date.parse(new Date())) / 1000, + // "tournament_name": "Battlehack 2020 Tournament" + // }); + // callback({ // "est_date_str": '7 PM EST on January 23, 2020', // "seconds_until": (Date.parse(new Date('January 23, 2020 19:00:00')) - Date.parse(new Date())) / 1000, // "tournament_name": "International Qualifying Tournament" diff --git a/frontend/src/components/paginationControl.js b/frontend/src/components/paginationControl.js index 3ebf8cb9..a388e0f6 100644 --- a/frontend/src/components/paginationControl.js +++ b/frontend/src/components/paginationControl.js @@ -4,7 +4,7 @@ class PaginationControl extends Component { render() { const { props } = this; - if (!props.pageLimit || props.pageLimit<= 1) { + if (!props.pageLimit || props.pageLimit<= 1) { return null; }; diff --git a/frontend/src/components/rankingTeamList.js b/frontend/src/components/rankingTeamList.js index 1affd282..1ff800b9 100644 --- a/frontend/src/components/rankingTeamList.js +++ b/frontend/src/components/rankingTeamList.js @@ -51,8 +51,8 @@ class RankingTeamList extends TeamList { { team.name } { team.users.join(", ") } { team.bio } - { team.student ? "✅" : "🛑"}{(team.student && team.mit) ? "🐥" : ""} - { team.auto_accept_ranked ? "Yes" : "No"} + { team.student ? "✅" : "🛑"}{(team.student && team.international) ? "🌍" : "🇺🇸"}{(team.student && team.mit) ? "🐥" : ""}{(team.student && team.high_school) ? "HS" : ""} + { team.auto_accept_unranked ? "Yes" : "No"} {props.canRequest && ( )} @@ -95,4 +95,4 @@ class RankingTeamList extends TeamList { } } -export default RankingTeamList; \ No newline at end of file +export default RankingTeamList; diff --git a/frontend/src/views/account.js b/frontend/src/views/account.js index a21152e2..f17523ba 100644 --- a/frontend/src/views/account.js +++ b/frontend/src/views/account.js @@ -114,11 +114,14 @@ class Account extends Component {

Edit Profile

+
+ Make sure to press the "Update Info" button, and wait for confirmation! +
- +
diff --git a/frontend/src/views/countdown.js b/frontend/src/views/countdown.js index 8f48e4b9..8404a659 100644 --- a/frontend/src/views/countdown.js +++ b/frontend/src/views/countdown.js @@ -98,8 +98,9 @@ class Countdown extends Component { if (this.state.tournament_name == 'START') { title = 'Game Specs are now released!'; } - // let explanatoryText =
The submission deadline for the {this.state.tournament_name} is at {this.state.est_date}.
; - let explanatoryText =
The submission deadline has not been set yet.
; + // TODO choosing which one to display should really be dynamic + let explanatoryText =
The submission deadline for the {this.state.tournament_name} is at {this.state.est_date}.
; + // let explanatoryText =
The submission deadline has not been set yet.
; let countdown = (
diff --git a/frontend/src/views/getting-started.js b/frontend/src/views/getting-started.js index 548082ce..0b0b72c5 100755 --- a/frontend/src/views/getting-started.js +++ b/frontend/src/views/getting-started.js @@ -46,86 +46,110 @@ class GettingStarted extends Component { getIDEInstallation() { if (this.state.ide === 'intellij') { return ( -
-
    -
  • Install IntelliJ IDEA Community Edition from here.
  • - -
  • In the Welcome to IntelliJ IDEA window that pops up when you start IntelliJ, select Import Project
  • - -
  • In the Select File or Dictionary to Import window, select the build.gradle file in the scaffold folder.
  • - -
  • Hit OK.
  • - -
  • We need to set the jdk properly; open the settings with File > Settings (IntelliJ IDEA > Preferences on Mac) or ctrl+alt+s. Navigate to Build, Execution, Deployment > Build Tools > Gradle and change Gradle JVM to 1.8
  • - -
  • Time for a first build! On the right side of the screen, click the small button that says gradle and has a picture of an elephant. Navigate to battlecode20-scaffold > Tasks > battlecode and double click on build. This will install the client and engine for you.
  • - -
  • If you haven't seen any errors, you should be good to go.
  • -
-
) +
+
    +
  • Install IntelliJ IDEA Community Edition from here.
  • + +
  • In the Welcome to IntelliJ IDEA window that pops up when you start IntelliJ, select Import Project
  • + +
  • In the Select File or Dictionary to Import window, select the build.gradle file in the scaffold folder.
  • + +
  • Hit OK.
  • + +
  • + We need to set the jdk properly; open the settings with File > Settings (IntelliJ IDEA > Preferences on Mac) + or ctrl+alt+s. Navigate to Build, Execution, Deployment > Build Tools > Gradle and change Gradle JVM to 1.8 +
  • + +
  • + Time for a first build! On the right side of the screen, click the small button that says gradle and has a picture of an elephant. Navigate to + battlecode21-scaffold > Tasks > battlecode and double click on update and then build. This will run tests + to verify that everything is working correctly, as well as download the client and other resources. +
  • + +
  • If you haven't seen any errors, you should be good to go.
  • +
+
) } else if (this.state.ide === 'eclipse') { return ( -
-
    -
  • Download the latest version of Eclipse from here.
  • +
    +
      +
    • Download the latest version of Eclipse from here.
    • -
    • In the Installer, select Eclipse IDE for Java Developers.
    • +
    • In the Installer, select Eclipse IDE for Java Developers.
    • -
    • Create a new Eclipse workspace. The workspace should NOT contain the battlecode20-scaffold folder.
    • +
    • Create a new Eclipse workspace. The workspace should NOT contain the battlecode21-scaffold folder.
    • -
    • Run File -> Import..., and select Gradle / Existing Gradle Project. - -

      -If you are unable to find this option, you may be using an old version of Eclipse. If updating your Eclipse version still does not work, you may need to manually install the "Buildship" plugin from the Eclipse marketplace. -

    -} showCloseButton={true}> - - - +
  • + Run File -> Import..., and select Gradle / Existing Gradle Project. +

    + If you are unable to find this option, you may be using an old version of Eclipse. + If updating your Eclipse version still does not work, you may need to manually install + the "Buildship" plugin from the Eclipse marketplace. +

+ } showCloseButton={true}> + + + -
  • Next to Project root directory field, press Browse... and navigate to battlecode20-scaffold. Finish importing the project.
  • +
  • Next to Project root directory field, press Browse... and navigate to battlecode21-scaffold. Finish importing the project.
  • -
  • If you do not see a window labeled Gradle Tasks, navigate to Window / Show View / Other.... Select Gradle / Gradle Tasks.
  • +
  • If you do not see a window labeled Gradle Tasks, navigate to Window / Show View / Other.... Select Gradle / Gradle Tasks.
  • -
  • In the Gradle Tasks window, you should now see a list of available Gradle tasks. Open the battlecode20-scaffold folder and navigate to the battlecode group, and then double-click build. This will run tests to verify that everything is working correctly
  • +
  • + In the Gradle Tasks window, you should now see a list of available Gradle tasks. Open the battlecode21-scaffold folder and navigate to the + battlecode group, and then double-click update and build. This will run tests to verify that everything is working correctly, + as well as download the client and other resources. +
  • -
  • You're good to go; you can run other Gradle tasks using the other options in the Gradle Tasks menu. Note that you shouldn't need any task not in the battlecode group. - -

    +

  • + You're good to go; you can run other Gradle tasks using the other options in the Gradle Tasks menu. + Note that you shouldn't need any task not in the battlecode group. +

    If you rename or add jar files to the lib directory, Eclipse gets confused. You'll need to re-add them using Project / Properties / Java Build Path. -

  • -} showCloseButton={true}> - - - - -
    - ) +

    + } showCloseButton={true}> + + + + +
    ) } else if (this.state.ide === 'terminal') { return ( -
    -
      -
    • Start every Gradle command with ./gradlew, if using Mac or Linux, or gradlew, if using Windows.
    • - -
    • You will need to set the JAVA_HOME environment variable to point to the installation path of your JDK. - -

      - On Mac, JAVA_HOME should probably be something like /Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home. -

    -} showCloseButton={true}> - - - - -
  • Navigate to the root directory of your battlecode20-scaffold, and run ./gradlew build (or gradlew build on Windows). This will run tests, to verify that everything is working.
  • - -
  • You're good to go. Run ./gradlew -q tasks (gradlew -q tasks on Windows) to see the other Gradle build tasks available. You shouldn't need to use any tasks outside of the battlecode group.
  • - -
    - ) +
    +
      +
    • Start every Gradle command with ./gradlew, if using Mac or Linux, or gradlew, if using Windows.
    • + +
    • + You will need to set the JAVA_HOME environment variable to point to the installation path of your JDK. + +

      + On Mac, JAVA_HOME should probably be something like /Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home. +

      +

      + On windows, it will likely be C:\Program Files\Java\jdk1.8.0_271. +

      +
    + } showCloseButton={true}> + + + + +
  • + Navigate to the root directory of your battlecode20-scaffold, and run ./gradlew update and then ./gradlew build + (or gradlew build on Windows). This will run tests to verify that everything is working correctly, as well as download the client + and other resources. +
  • + +
  • + You're good to go. Run ./gradlew -q tasks (gradlew -q tasks on Windows) to see the other Gradle tasks available. + You shouldn't need to use any tasks outside of the battlecode group. +
  • + + ) } } @@ -147,7 +171,7 @@ If you are unable to find this option, you may be using an old version of Eclips

    - Battlecode 2021 has not been released yet! Register a team and watch out for the January 2021 launch. + Battlecode 2021 is now running! See below for instructions on getting started.

    @@ -160,7 +184,7 @@ If you are unable to find this option, you may be using an old version of Eclips To participate in Battlecode, you need an account and a team. Each team can consist of 1 to 4 people.

    - Create an account on this website, and then go to the team section to either create + Create an account on this website, and then go to the team section to either create or join a team.

    @@ -171,32 +195,108 @@ If you are unable to find this option, you may be using an old version of Eclips

    - If you experience problems with the instructions below, check common issues, and if that doesn't help, ask on the Discord. + If you experience problems with the instructions below, check common issues, and if that doesn't help, ask on the Discord.

    Step 1: Install Java
    -

    - You'll need a Java Development Kit (JDK) version 8. Unfortunately, higher versions will not - work. Download it here. - You may need to create an Oracle account. +

    + You'll need a Java Development Kit (JDK) version 8. Unfortunately, higher versions will not + work. Download it here. + You may need to create an Oracle account. - +

    + Alternatively, you can install a JDK yourself using your favorite package manager. Make sure it's an Oracle JDK — we don't support anything else — and is compatible with Java 8. +

    + } showCloseButton={true}> + + +

    +

    +

    + If you're unsure how to install the JDK, you can find instructions for all operating + systems here (pay attention to PATH and CLASSPATH). +

    +
    Step 2: Download Battlecode

    - Alternatively, you can install a JDK yourself using your favorite package manager. Make sure it's an Oracle JDK — we don't support anything else — and is compatible with Java 8. -

    - } showCloseButton={true}> - - -

    -

    -

    - If you're unsure how to install the JDK, you can find instructions for all operating systems here (pay attention to PATH and CLASSPATH). + Next, you should download the Battlecode 2021 scaffold. + To get up and running quickly, you can click "Clone or download" and then "Download ZIP," and move on to the next step.

    -
    Step 2: Coming later!

    - 2021-specific installation instructions will appear here once the game is released. + We recommend, however, that you instead use Git to organize your code. If you haven't used Git before, + read this guide (or wait for our lecture covering it). + On the scaffold page, click "Use this template." + Importantly, on the next page, make your new repo private (you don't want other teams to steal your code!). + You can then clone your newly created repo and invite your team members to collaborate on it. +

    + + + {/*
    Step 3: Build the Game
    +

    + Open a terminal in the scaffold you just downloaded. Run the commands ./gradlew update and ./gradlew build. + The client won't appear until you've run these two commands. +

    */} + + +
    Step 3: Local Setup
    +

    + We recommend using an IDE like IntelliJ IDEA or Eclipse to work on Battlecode, but you can also use your favorite text editor combined with a terminal. + Battlecode 2020 uses Gradle to run tasks like run, debug and jarForUpload (but don't worry about that — you don't need to install it). +

    +

    + View instructions for: + +

    + + + +

    +

    + {this.getIDEInstallation()} +

    +

    + There should now be a folder called client in your scaffold folder; if you go in there, and double click the + Battlecode Client application, you should be able to run and watch matches. (Please don't move that application, + it will be sad.) If you're on Linux, navigate to the client folder and run ./battlecode-visualizer + to launch the client. +

    + + + +
    Developing your Bot
    +

    + Place each version of your robot in a new subfolder in the src folder. Make sure every version has a RobotPlayer.java. +

    + +
    Running Battlecode from the Client
    +

    + Open the client as described in Step 3. Navigate to the runner tab, + select which bots and maps to run, and hit Run Game! + Finally, click the play/pause button to view the replay. +

    + + +
    Running Battlecode from the terminal or IDE
    +

    + You can run games directly from the terminal with the gradle task ./gradlew run -Pmaps=[map] -PteamA=[Team A] -PteamB=[Team B]. If you + don't include the map or team flags, Battlecode will default to whatever is listed in gradle.properties. + Running the same gradle task from you IDE will also work. +

    + + +
    +
    +

    Client Tips

    +
    +
    +

    + If you're experiencing memory problems with the client, please try: +

    +
      +
    • Making fewer logs and/or disabling log processsing in the client (toggled with "L").
    • +
    • Making .bc21 files with the engine directly and uploading them to the client's match queue, rather than using the client's runner. With this method, you can just use the web version at 2021.battlecode.org/visualizer.html rather than the desktop application.
    • +
    -
    +

    Join the Community!

    @@ -206,7 +306,7 @@ If you are unable to find this option, you may be using an old version of Eclips Battlecode has a Discord server! Everyone is encouraged to join. Announcements, strategy discussions, bug fixes and ~memes~ all - happen on Discord. Follow this invite link to join: https://discord.gg/N86mxkH. + happen on Discord. Follow this invite link to join: https://discord.gg/N86mxkH.

    diff --git a/frontend/src/views/home.js b/frontend/src/views/home.js index 793ee5f2..d6216172 100755 --- a/frontend/src/views/home.js +++ b/frontend/src/views/home.js @@ -103,7 +103,7 @@ class InstrCard extends UpdateCard {

    - 👀 The competition has not been released yet. Feel free to register a team and watch for our release on January 4, 2021! + The competition is now running! Check out the Getting Started page for instructions on how to get started.

    @@ -135,7 +135,7 @@ class LinksCard extends Component { Discord (invite)
  • - GitHub + GitHub
  • Twitch diff --git a/frontend/src/views/issues.js b/frontend/src/views/issues.js index f8f04317..d783b30e 100755 --- a/frontend/src/views/issues.js +++ b/frontend/src/views/issues.js @@ -33,8 +33,8 @@ class Issues extends Component { installed from earlier. We will add instructions here shortly, but for now, ask on the Discord for the fix.
    • Before doing the following two suggestions, try adding the line org.gradle.java.home=<path to your java 8 jdk> to your gradle.properties file
    • -
    • For Windows, try following these instructions.
    • -
    • Try setting org.gradle.java.home=/path_to_jdk_1.8_directory. You need to know your JAVA_HOME (try this guide).
    • +
    • For Windows, try following these instructions.
    • +
    • Try setting org.gradle.java.home=/path_to_jdk_1.8_directory. You need to know your JAVA_HOME (try this guide).
  • Exception in thread "WebsocketSelector14" java.lang.NullPointerException. A common error in java, but sometimes happens if you close the client while a game is running. @@ -44,7 +44,7 @@ class Issues extends Component { things in IntelliJ, click the elephant in the Gradle Tool Window (the right-hand sidebar) and then execute gradle --stop in the window that pops up). If that doesn't work, ask on the Discord.
  • - If your error is not listed above, ask on the Discord. + If your error is not listed above, ask on the Discord.

    @@ -77,14 +77,15 @@ class Issues extends Component {

      -
    • Did you download the Oracle JDK 8 listed in the installation instructions?
    • +
    • Are you on the latest version of Battlecode? try ./gradlew update
    • +
    • Did you download the Oracle JDK 8 listed in the installation instructions?
    • Did you set your JAVA_HOME correctly?
    • ./gradlew clean (always good to try)
    • ./gradlew cleanEclipse (if Eclipse)
    • Refresh Gradle Dependencies in IntelliJ (see above)
    • ./gradlew --stop (stops Gradle daemons)
    • rm -r ~/.gradle (removes the Gradle cache)
    • -
    • Redownload the scaffold.
    • +
    • Redownload the scaffold.

    diff --git a/frontend/src/views/resources.js b/frontend/src/views/resources.js index 894581e5..978118f5 100755 --- a/frontend/src/views/resources.js +++ b/frontend/src/views/resources.js @@ -25,7 +25,10 @@ class Resources extends Component { @@ -43,18 +46,7 @@ class Resources extends Component { Everyone will love you!!

      -
    • battlehack2020-viewer: A viewer for replay files, in your browser! Useful for watching your scrimmages. Made by rzhan11 of team Kryptonite, and - with installation instructions on Github.
    • -
    • battlehack20-fancyviewer: Another viewer, written in Python! Like the command line viewer, - this viewer automatically shows the game when you run the match, but it also supports viewing a replay file. Made by cooljoseph of team D5. - Discord has installation instructions, - and the source code is on GitHub
    • -
    • viewer.py: A third viewer! Lightweight, in Python. Made by Houwang of team IDIOT, and you can download the Python file here.
    • -
    • battlehack20-minimal: A minimal version of the engine, promising speedups of up to 30x! - Made by cooljoseph of team D5, with installation instructions on GitHub. Important: Note that - using this engine might mean that the code you run locally doesn't behave in exactly the same way as the same code run on our - servers — in particular, this engine does not impose any bytecode limits, which you might run into on the server — and this could - cause unexpected errors and make debugging harder.
    • +
    • There is nothing here yet...
    diff --git a/frontend/src/views/scrimmaging.js b/frontend/src/views/scrimmaging.js index 8985a1e3..47369b22 100755 --- a/frontend/src/views/scrimmaging.js +++ b/frontend/src/views/scrimmaging.js @@ -3,6 +3,7 @@ import Api from '../api'; import Floater from 'react-floater'; import ScrimmageRequestor from '../components/scrimmageRequestor'; +import PaginationControl from "../components/paginationControl"; class ScrimmageRequest extends Component { @@ -64,18 +65,20 @@ class ScrimmageRequests extends Component { class ScrimmageHistory extends Component { state = { + scrimPage: 1, + scrimLimit: 0, scrimmages: [], }; - refresh = () => { + refresh = (page) => { Api.getScrimmageHistory(function(s) { - this.setState({ scrimmages: s }); - }.bind(this)); + this.setState({...s, scrimPage: page}); + }.bind(this), page); } componentDidMount() { - this.refresh(); + this.refresh(this.state.scrimPage); } playReplay(e) { @@ -84,6 +87,12 @@ class ScrimmageHistory extends Component { window.open(url, "replay_window", "scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,width=600,height=750"); } + getScrimPage = (page) => { + if (page !== this.state.scrimPage && page >= 0 && page <= this.state.scrimLimit) { + this.refresh(page); + } + } + render() { return (
    @@ -113,7 +122,9 @@ class ScrimmageHistory extends Component { { s.status } -

    Our server has run into an error running this scrimmage. Don't worry, we're working on resolving it!

    } showCloseButton={true}> +

    Our server has run into an error running this scrimmage. Don't worry, we're working on resolving it!

    +

    Error: {s.error_msg}

    + } showCloseButton={true}> ) @@ -126,7 +137,7 @@ class ScrimmageHistory extends Component { { s.score } { s.team } { s.ranked ? "Ranked" : "Unranked"} - { s.replay?Watch:N/A } + { s.replay?Watch:N/A } ) }) } @@ -134,6 +145,11 @@ class ScrimmageHistory extends Component { + this.getScrimPage(page)} + /> ) } @@ -143,7 +159,7 @@ class Scrimmaging extends Component { refresh = () => { this.requests.refresh(); - this.history.refresh(); + this.history.refresh(1); } render() { diff --git a/frontend/src/views/submissions.js b/frontend/src/views/submissions.js index 24db017d..42780d5f 100755 --- a/frontend/src/views/submissions.js +++ b/frontend/src/views/submissions.js @@ -158,10 +158,10 @@ class Submissions extends Component { } else { switch (key) { case 'tour_sprint': - add_data = ['Sprint', data] + add_data = ['Sprint 1', data] break case 'tour_seed': - add_data = ['Seeding', data] + add_data = ['Sprint 2', data] break case 'tour_qual': add_data = ['Qualifying', data] @@ -170,7 +170,7 @@ class Submissions extends Component { add_data = ['Final', data] break case 'tour_hs': - add_data = ['High School', data] + add_data = ['US High School', data] break case 'tour_intl_qual': add_data = ['International Qualifying', data] @@ -240,16 +240,26 @@ class Submissions extends Component {

    Submit Code

    - {/*

    - The final submission deadline is 7 pm EST on Wednesday 1/29 (which is s o o n). This applies to the Final, Newbie and High School tournaments. Submit your code using the button below. For peace of mind, submit 15 minutes before and make sure it compiles and shows up - under "Latest Submissions." + {/* TODO could this paragraph be dynamically filled? that'd be amazing */} +

    + The submission deadline for the Final Tournament is 7 pm ET on Thursday 1/28. Make sure to have indicated your eligibility on your Team Profile page. Also make sure to have all members upload a resume, at your personal profile page. + **See the Eligibility Rules in the Tournaments page for more info** +

    +

    + Submit your code using the button below. For peace of mind, submit 15 minutes before and make sure it compiles and shows up under "Latest Submissions." We will have a 5-minute grace period; if you're having trouble submitting, send us your code on Discord before 7:05. If the code you submit to us on Discord has only minor differences to the code submitted on time through the website (e.g., 1 or 2 lines), we will accept it. We will not accept anything submitted after 7:05 pm. -

    */} +

    - Create a zip file of your robot player. The zip file can only contain 1 player package, and needs to have a RobotPlayer.java file. Submit the zip file below. Ensure that you're not importing any packages not included in the zip file, or your code won't compile. -

    - Please stay on this page until the card below indicates success. To double-check that your code has been submitted, you can download at "Latest Submissions". - + Create a zip file of your robot player, and submit it below. The submission format should be a zip file containing a single folder (which is your package name), which should contain RobotPlayer.java and any other code you have written, for example: +

    
    +                                submission.zip --> examplefuncsplayer --> RobotPlayer.java, FooBar.java
    +                            
    +

    +

    + Please stay on this page until the card below indicates success. To double-check that your code has been submitted, you can download at "Latest Submissions". +

    +

    + If your bot does not compile, see the "Compiling Tips" section at the bottom of this page.

    {file_button} {file_button_2} @@ -288,6 +298,9 @@ class Submissions extends Component { case 11: status_str = "Successfully queued for compilation!" break + case 12: + status_str = "Files cannot be submitted without a team." + break case 13: status_str = "Submitting failed. Try re-submitting your code." break @@ -475,6 +488,31 @@ class Submissions extends Component { } } + renderCompilingTips() { + return ( +
    +
    +

    Compiling Tips

    +
    +
    +

    +

      +
    • + Submission format: Check that your zip contains exactly one directory, and your code is inside that directory. +
    • +
    • + Non-ASCII characters: Ensure your code is completely ASCII. In the past we have had compile errors due to comments containing diacritic characters (áéíóú). +
    • +
    • + Make sure you only import from your own bot, and from java. packages. In particular, do not use javax, javafx, and watch out for importing from other versions of your bot (which may work locally, but will not work on our servers as you can only submit one folder). +
    • +
    +

    +
    +
    + ) + } + render() { return (
    @@ -506,6 +544,7 @@ class Submissions extends Component { { this.renderHelperTourTable() }
    + { this.renderCompilingTips() } diff --git a/frontend/src/views/team.js b/frontend/src/views/team.js index 3ee71996..1710d82b 100755 --- a/frontend/src/views/team.js +++ b/frontend/src/views/team.js @@ -102,7 +102,7 @@ class YesTeam extends Component {
    {/* */}

    We need to know a little about your team in order to determine which prizes your team is eligible for. - Check all boxes that apply to your team. + Check all boxes that apply to all members your team.

    @@ -129,7 +129,7 @@ class YesTeam extends Component {
    - +
    @@ -295,6 +295,7 @@ class ResumeStatus extends Component { } // pass change handler in props.change and team in props.team +// NOTE: If you are ever working with teams' eligility (for example, to pull teams for the newbie tournament), please see backend/docs/ELIGIBILITY.md before you do anything! The variable names here are poorly named (because columns in the database are poorly named). class EligibiltyOptions extends Component { render() { return ( @@ -309,7 +310,7 @@ class EligibiltyOptions extends Component {
    - {/*
    +
    @@ -317,25 +318,25 @@ class EligibiltyOptions extends Component { -
    */} +
    - + -

    Look it up! (If you don't know, you probably aren't one...)

    } showCloseButton={true}> +

    Teams consisting entirely of MIT students who have never competed in Battlecode before are eligible for the Newbie Tournament.

    } showCloseButton={true}> - {/*
    +
    -

    Teams of only high school (and earlier) students are eligible for the High School Tournament.

    } showCloseButton={true}> +

    Teams of only high school (and earlier) students are eligible for the US High School Tournament. (Note that you must also be all US students to be eligible -- if you're all US students, don't forget to check that box, too!)

    } showCloseButton={true}> - */} + @@ -370,4 +371,4 @@ class Team extends Component { ); } } -export default Team; \ No newline at end of file +export default Team; diff --git a/frontend/src/views/tournaments.js b/frontend/src/views/tournaments.js index bd38ad05..9764dfcd 100644 --- a/frontend/src/views/tournaments.js +++ b/frontend/src/views/tournaments.js @@ -51,10 +51,10 @@ class Tournaments extends Component {

    • - Sprint Tournament: 1/12. One week after spec release, you're given a chance to win small prizes in this tournament. The goal is to get an idea of the meta-game, and a chance to test your bot prototypes. + Sprint Tournament 1: 1/12. One week after spec release, you're given a chance to win small prizes in this tournament. The goal is to get an idea of the meta-game, and a chance to test your bot prototypes.
    • - Seeding Tournament: 1/19. One week after the Sprint Tournament, this tournament determines your positioning in the Qualifying Tournament. + Sprint Tournament 2: 1/19. One week after the Sprint Tournament 1, you're given another chance to win small prizes, test the metagame, and make changes.
    • International Qualifying Tournament: 1/26. This tournament determines the 4 international teams that will qualify for the Final Tournament. @@ -67,7 +67,7 @@ class Tournaments extends Component { Newbie Tournament: 1/28. The top newbie teams compete for a smaller prize pool. The final match between the top 2 teams will be run at the Final Tournament.
    • - High School Tournament: 1/28. The top high school teams compete for a smaller prize pool. Like the Newbie Tournament, the final match will be run at the Final Tournament. + US High School Tournament: 1/28. The top US high school teams compete for a smaller prize pool. Like the Newbie Tournament, the final match will be run at the Final Tournament.
    • Final Tournament: 1/30. The top 16 teams, as determined by the qualifying tournaments, compete for glory, fame and a big prize pool. The tournament will take place live, and will be streamed online for 2021. There will not be a component on MIT campus this year. @@ -85,6 +85,41 @@ class Tournaments extends Component {

      Tournament Results

      +

      View tournament brackets here:

      + +

      Congratulations to our prizewinning teams!

      +
      +{`                                         1st      $5000  babyducks
      +                                         2nd      $3000  Producing Perfection
      +                                         3rd      $2500  Chicken
      +                                         4th      $1500  Malott Fat Cats
      +                                         5-6th    $1250  Kryptonite
      +                                         5-6th    $1250  monky
      +                                         7-8th    $1000  Nikola
      +                                         7-8th    $1000  wololo
      +                                         9-12th    $750  3 Musketeers
      +                                         9-12th    $750  Chop Suey
      +                                         9-12th    $750  confused
      +                                         9-12th    $750  smite
      +                                         13-16th   $500  BattlePath
      +                                         13-16th   $500  Bytecode Mafia
      +                                         13-16th   $500  GoreTeks
      +                                         13-16th   $500  waffle
      +Most adaptive strategy (sponsored by Five Rings)  $1500  Chop Suey
      +                      Sprint Tournament 1 Winner   $500  Super Cow Powers
      +                      Sprint Tournament 2 Winner   $500  Super Cow Powers
      +                                 High School 1st   $500  idrc
      +                                 High School 2nd   $200  java :ghosthug:
      +                                      Newbie 1st   $500  Dis Team
      +                                      Newbie 2nd   $200  $nowball`}
      @@ -94,10 +129,17 @@ class Tournaments extends Component {

      - In response to competitor feedback, we have changed the format of the competition. - Teams are split into four divisions. Round Robin tournaments are held within each of these divisions. - From each division, the four teams with the highest win-ratio move on the the next round. - These top 16 will face eachother in another round-robin tournament. + Scrimmage rankings will be used to determine seeds for the Sprint Tournaments. For all other tournaments, results from the previous tournament will be used to seed teams (where ties will be broken by the scrimmage ranking right before the tournament). +

      +

      + Tournaments will be in a double elimination format, with the exception of both Sprint Tournaments, which are single elimination. The Final Tournament will start with a blank slate (any losses from the Qualifying Tournament are reset). +

      +

      + Even if you miss earlier tournaments, you can participate in later tournaments (except the Final Tournament). + This includes the Qualifying Tournament — you can participate even if you miss every other tournament (your seed will be determined by your scrimmage rank). +

      +

      + Each match within a tournament will consist of at least 3 games, each on a different map, and the team that wins the most games will advance.

      @@ -112,7 +154,7 @@ class Tournaments extends Component { Thanks to our gold sponsor, Five Rings!
      • 1st Place prize: to whosoever has the highest rating at the end (hacks not allowed). Smaller prizes for subsequent placers.
      • -
      • Smaller prizes for top placers in other non-final (newbie, high school, sprint) tournaments.
      • +
      • Smaller prizes for top placers in other non-final (newbie, US high school, sprint) tournaments.
      • More prizes??? TBA, maybe 👀
        • Historically, we have given out prizes for creative strategies, major bugs found, and other game-specific topics. Have fun with your strategies, write-ups, and overall participation in Battlecode!
        • @@ -131,7 +173,40 @@ class Tournaments extends Component {

          - Anyone is welcome to participate in Battlecode! Anyone can write a bot, create a team and participate in the tournament. More eligibility details can be found here. + Anyone is welcome to participate in Battlecode! Anyone can write a bot, create a team, and participate in matches and the Sprint Tournaments. +

          +

          Your team must meet all three conditions to be eligible for the Qualifying and Final tournaments by the submission deadline: +

            +
          1. + Have uploaded a bot +
          2. +
          3. + Have indicated your eligibility on your Team Profile page +
          4. +
          5. + Have all members upload a resume, at your personal profile page. +
          6. +
          +

          +

          As a reminder, the tournament divisions are: +

            +
          • + Full-time US teams, consisting entirely of US students studying full-time, or in a transition phase. We may ask for some documentation to verify your student status if you advance to the finals. The top 12 teams in this division will earn a place out of 16 final tournament spots; eligibility is conditioned on attendance to our virtual finalists celebration on the evening of Friday 1/29. +
          • +
          • + Full-time international teams, consisting entirely of students studying full-time, or in a transition phase, where at least one team member is not a US student. We may ask for some documentation to verify your student status if you advance to the finals. The top 4 teams in this division will earn a place out of 16 final tournament spots. +
          • +
          • + US High-school teams, consisting entirely of high school students in the US. The top 2 teams will have the final match played during the final tournament. +
          • +
          • + Newbie teams, consisting entirely of MIT students who have never competed in Battlecode before. The top 2 teams will have their final match played during the final tournament. +
          • +
          +

          + +

          + More eligibility details can be found here.

          diff --git a/gradle.properties b/gradle.properties index 9de0d24a..1565098d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,7 @@ teamA=examplefuncsplayer teamB=examplefuncsplayer maps=maptestsmall +profilerEnabled=false source=src mapLocation=maps -release_version=2021.0.0.1 \ No newline at end of file +release_version=2021.3.0.5 diff --git a/infrastructure/README.md b/infrastructure/README.md index aada1095..1d447f03 100644 --- a/infrastructure/README.md +++ b/infrastructure/README.md @@ -69,6 +69,11 @@ In GCloud > Compute Engine > Instance templates: export GOOGLE_APPLICATION_CREDENTIALS=worker/app/gcloud-key.json ./pub.py battlecode18 bc20-compile ``` + (in windows powershell) + ``` + $env:GOOGLE_APPLICATION_CREDENTIALS="worker/app/gcloud-key.json" + ./pub.py battlecode18 bc20-compile + ``` - To run docker images on the gcloud servers: push the images to the container registry (see above) Set up instance templates in gcloud: diff --git a/infrastructure/compile.Dockerfile b/infrastructure/compile.Dockerfile index 8363dadd..b5c79345 100644 --- a/infrastructure/compile.Dockerfile +++ b/infrastructure/compile.Dockerfile @@ -1,4 +1,4 @@ FROM bc21-worker COPY app/compile_server.py app/ -CMD /app/compile_server.py +CMD python3 /app/compile_server.py diff --git a/infrastructure/env.Dockerfile b/infrastructure/env.Dockerfile index efd1c185..92d5c4ba 100644 --- a/infrastructure/env.Dockerfile +++ b/infrastructure/env.Dockerfile @@ -1,4 +1,4 @@ FROM python:3 ENV BC_DB_USERNAME database_admin ENV BC_DB_PASSWORD ??? -ENV DOMAIN https://2020.battlecode.org \ No newline at end of file +ENV DOMAIN https://2021.battlecode.org \ No newline at end of file diff --git a/infrastructure/game.Dockerfile b/infrastructure/game.Dockerfile index efcf9c74..f8305df0 100644 --- a/infrastructure/game.Dockerfile +++ b/infrastructure/game.Dockerfile @@ -2,4 +2,4 @@ FROM bc21-worker COPY app/game_server.py app/ # COPY maps box/maps/ -CMD /app/game_server.py +CMD python3 /app/game_server.py diff --git a/infrastructure/matcher/maps.json b/infrastructure/matcher/maps.json new file mode 100644 index 00000000..7ccb7c7e --- /dev/null +++ b/infrastructure/matcher/maps.json @@ -0,0 +1,20 @@ +{ + "Round 1 (Winners)": [ + "circle","maptestsmall","quadrants" + ], + "Round 1 (Losers)": [ + "maptestsmall","quadrants","circle" + ], + "Round 2 (Winners)": [ + "quadrants","circle","maptestsmall" + ], + "Round 2 (Losers A)": [ + "circle","maptestsmall","quadrants" + ], + "Round 3": [ + "maptestsmall","quadrants","circle" + ], + "Round 4 (if needed)": [ + "quadrants","circle","maptestsmall" + ] +} diff --git a/infrastructure/matcher/replay_dump.json b/infrastructure/matcher/replay_dump.json new file mode 100644 index 00000000..9a0740e6 --- /dev/null +++ b/infrastructure/matcher/replay_dump.json @@ -0,0 +1,117 @@ +[ + [ + [ + "testteam the redux", + "Teh Dev", + "circle", + 2, + "ed18b8d6df89457a9a4f3316590a04" + ], + [ + "Teh Dev", + "testteam the redux", + "maptestsmall", + 1, + "e1f9d9799f0c92c69739f414db3806" + ], + [ + "testteam the redux", + "Teh Dev", + "quadrants", + 2, + "5e04919b30c410882412355e229ec9" + ] + ], + [ + [ + "asdfsddtjggyftawehjjhsrsfghgsdf", + "Teh Dev", + "quadrants", + 2, + "94cfdd55bad5bdc12361e30590cab5" + ], + [ + "Teh Dev", + "asdfsddtjggyftawehjjhsrsfghgsdf", + "circle", + 1, + "9f9fa4b3c968ad5d6b0ca9a9d50206" + ], + [ + "asdfsddtjggyftawehjjhsrsfghgsdf", + "Teh Dev", + "maptestsmall", + 2, + "e74caebeae5c2a8fcdda321b3e7674" + ] + ], + [ + [ + "asdfsddtjggyftawehjjhsrsfghgsdf", + "testteam the redux", + "circle", + 1, + "69a7d58c6deb7702424a90dcd3f6f4" + ], + [ + "testteam the redux", + "asdfsddtjggyftawehjjhsrsfghgsdf", + "maptestsmall", + 2, + "8f791ed3a1886383344cc6c3935674" + ], + [ + "asdfsddtjggyftawehjjhsrsfghgsdf", + "testteam the redux", + "quadrants", + 1, + "98dece93751195cd749ec873af5b76" + ] + ], + [ + [ + "Teh Dev", + "asdfsddtjggyftawehjjhsrsfghgsdf", + "maptestsmall", + 1, + "3c1bf7ea8946dd41be747ae9d50d61" + ], + [ + "asdfsddtjggyftawehjjhsrsfghgsdf", + "Teh Dev", + "quadrants", + 2, + "66e34f96783bff4766c586b3712613" + ], + [ + "Teh Dev", + "asdfsddtjggyftawehjjhsrsfghgsdf", + "circle", + 1, + "7f3ce4410ad81f6148ced409a39249" + ] + ], + [ + [ + "Teh Dev", + "asdfsddtjggyftawehjjhsrsfghgsdf", + "quadrants", + 1, + "efecbea6cc0c417e1a705c67f6194d" + ], + [ + "asdfsddtjggyftawehjjhsrsfghgsdf", + "Teh Dev", + "circle", + 2, + "943863b22850b31aef653e8b95bade" + ], + [ + "Teh Dev", + "asdfsddtjggyftawehjjhsrsfghgsdf", + "maptestsmall", + 1, + "5325b4a444057b3bb0955b78d988cd" + ] + ] +] diff --git a/infrastructure/matcher/scrimmage.py b/infrastructure/matcher/scrimmage.py index aa905b51..581eb2d3 100755 --- a/infrastructure/matcher/scrimmage.py +++ b/infrastructure/matcher/scrimmage.py @@ -27,7 +27,7 @@ def worker(): if result == None: scrim_queue.put(scrim) -@sched.scheduled_job('cron', minute=0) +@sched.scheduled_job('cron', hour='*/3') def matchmake(): try: logging.info('Obtaining scrimmage list') diff --git a/infrastructure/matcher/team_names b/infrastructure/matcher/team_names new file mode 100644 index 00000000..f36c0dfa --- /dev/null +++ b/infrastructure/matcher/team_names @@ -0,0 +1,3 @@ +asdfsddtjggyftawehjjhsrsfghgsdf +testteam the redux +Teh Devs \ No newline at end of file diff --git a/infrastructure/matcher/team_pk b/infrastructure/matcher/team_pk new file mode 100644 index 00000000..3f0067f9 --- /dev/null +++ b/infrastructure/matcher/team_pk @@ -0,0 +1,3 @@ +1883 +1810 +1790 diff --git a/infrastructure/matcher/tournament_server.py b/infrastructure/matcher/tournament_server.py index 6fbc5860..6a1ee5f4 100644 --- a/infrastructure/matcher/tournament_server.py +++ b/infrastructure/matcher/tournament_server.py @@ -243,7 +243,7 @@ def dequeue_worker(): if __name__ == '__main__': # Command-line usage: ./tournament_server.py argv, where: # argv[1] = tournament_id - # argv[2] = file containing pk + # argv[2] = file containing package ids in the gcloud # argv[3] = file containing names # argv[4] = file containing map config # Team data should be ordered from first to last seed, one per line diff --git a/infrastructure/passed_students.sql b/infrastructure/passed_students.sql index a0121951..a123582b 100644 --- a/infrastructure/passed_students.sql +++ b/infrastructure/passed_students.sql @@ -22,14 +22,14 @@ SELECT api_user.last_name, mit_students.email, collated_results.max_wins_out_of_10, - (collated_results.max_wins_out_of_10 >= 8) AS passed + (collated_results.max_wins_out_of_10 >= 7) AS passed FROM api_user INNER JOIN api_team_users ON api_user.id = api_team_users.user_id -INNER JOIN ( +LEFT JOIN ( SELECT team_id, MAX(wins_out_of_10) AS max_wins_out_of_10 @@ -63,7 +63,7 @@ INNER JOIN ( FROM api_scrimmage CROSS JOIN ( - VALUES (919) + VALUES (1790) ) AS consts(ref_team_id) WHERE red_team_id = consts.ref_team_id OR blue_team_id = consts.ref_team_id @@ -78,10 +78,10 @@ INNER JOIN ( ) ) AS last_10 CROSS JOIN ( - VALUES (CAST ('2020-01-17 16:57:35.785685+00' AS TIMESTAMP)) + VALUES (CAST ('2021-01-25 08:54:31.889726+00' AS TIMESTAMP)) ) AS consts(ref_timestamp) WHERE - oldest_scrim >= consts.ref_timestamp + oldest_scrim >= consts.ref_timestamp OR oldest_scrim IS NULL ) AS result_dump GROUP BY team_id diff --git a/infrastructure/scrimmage.Dockerfile b/infrastructure/scrimmage.Dockerfile index 7d30806a..49fcfe2d 100644 --- a/infrastructure/scrimmage.Dockerfile +++ b/infrastructure/scrimmage.Dockerfile @@ -6,4 +6,4 @@ RUN pip3 install --upgrade \ requests COPY config.py util.py scrimmage.py app/ -CMD /app/scrimmage.py +CMD python3 /app/scrimmage.py diff --git a/infrastructure/tournament-util/.gitignore b/infrastructure/tournament-util/.gitignore index 512d5e24..8f09d666 100644 --- a/infrastructure/tournament-util/.gitignore +++ b/infrastructure/tournament-util/.gitignore @@ -1,2 +1,3 @@ /data/* !/data/0-example +!/data/1-sprint1 diff --git a/infrastructure/tournament-util/challonge_pubber.py b/infrastructure/tournament-util/challonge_pubber.py new file mode 100644 index 00000000..a5e82a69 --- /dev/null +++ b/infrastructure/tournament-util/challonge_pubber.py @@ -0,0 +1,108 @@ +# Requires achallonge, and _not_ pychal. Make sure to `pip uninstall pychal`, `pip install achallonge` etc before using. + +# IMPORTANT -- BEFORE RUNNING THIS: +# Get the Challonge API Key. Set it to an env, CHALLONGE_API_KEY. DON'T PUSH NOR SCREENSHARE/STREAM IT! +# Get the tournament url, it's the alphanumeric string at the end of the tournament website's url. (e.g. http://challonge.com/thispart). Set it to the tour_url variable. +# Now with all those set, run the script as specified directly below: + +# Usage: +# python challonge_pubber.py path/to/json.json start end +# path/to/json: a json produced by running the tour. Get this from someone who ran the tournament. +# start: the challonge match number (as shown on the bracket page) of the first Challonge match (or, if argv[3] not specified, the only challonge match) whose result is to be published +# end: optional; the challonge match number of the last Challonge match to be published, _exclusive_. +# e.g. python challonge_pubber.py path/to/replay_dump.json 1 4 +# will publish the results of the Challonge bracket's matches 1, 2, and 3. + +import sys, json, challonge, asyncio, os + +tour_url = 'example' + +async def run(): + print("Setting up...\n") + try: + api_key = os.getenv('CHALLONGE_API_KEY') + user = await challonge.get_user('mitbattlecode',api_key) + tournament = await user.get_tournament(url = tour_url) + except: + print("Make sure you have properly configured CHALLONGE_API_KEY and tour_url.") + print("See the comments at the top of this file for instructions.") + raise Exception + + # To ensure tournament is started and attachments are allowed. + # Only needs to be run once per tournament. + # But, we run it every time the script is run: this can be run unlimited times, and is pretty quick. + # Also makes the script much simpler to use. + await tournament.start() + await tournament.allow_attachments(True) + + # We map matches' suggested play order to their match objects, so that we can easily access the matches. + # This is because we (battlecode) play matches in their suggested play order, and so suggested play order is the index that we use. + # There's no way to directly access the match objects by their suggested play order, so we need some preprocessing. + match_play_order_dict = dict() + tournament_matches = await tournament.get_matches() + for m in tournament_matches: + suggested_play_order = m.suggested_play_order + match_play_order_dict[suggested_play_order] = m + + replay_file_name = sys.argv[1] + match_no_start = int(sys.argv[2]) + try: + match_no_end = int(sys.argv[3]) + except: + match_no_end = match_no_start+1 + + with open(replay_file_name, 'r') as replay_file: + replays = json.load(replay_file) + + for match_no in range(match_no_start, match_no_end): + print(f'Reporting match {match_no}') + match = replays[match_no - 1] # note -1, for proper indexing: challonge is 1-indexed while the json is 0 + api_match = match_play_order_dict[match_no] + if api_match.state == 'pending': + print('Match is not ready to have results reported.') + print('Check that necessary previous matches have been reported, so that participants are set.') + raise Exception + + api_player1 = await tournament.get_participant( api_match.player1_id ) + api_player2 = await tournament.get_participant( api_match.player2_id ) + + + player1 = match[0][0] + player2 = match[0][1] + + if (api_player1.display_name != player1) or (api_player2.display_name != player2): + print("Match's player names on json do not match those on Challonge.") + print("Check proper entry/order of participants on Challonge, and correct match ordering in json.") + raise Exception + # For a sanity check, to ensure you're publishing the match that you want to publish. + print(f'{player1} vs {player2}') + + player1_score = 0 + player2_score = 0 + + for game in match: + p_red = game[0] + p_blue = game[1] + map = game[2] + winner = p_red if game[3] == 1 else p_blue + replay = game[4] + + if winner == player1: + player1_score += 1 + else: + player2_score += 1 + + replayurl = f'http://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/{replay}.bc21' + await api_match.attach_url(replayurl) + + if player1_score > player2_score: + await api_match.report_winner(api_player1, f'{player1_score}-{player2_score}') + else: + await api_match.report_winner(api_player2, f'{player1_score}-{player2_score}') + + print(f'Match {match_no} reported!\n') + + +loop = asyncio.get_event_loop() +loop.run_until_complete(run()) +loop.close() diff --git a/infrastructure/tournament-util/csv_to_files.py b/infrastructure/tournament-util/csv_to_files.py index 1d44dadf..946faa31 100755 --- a/infrastructure/tournament-util/csv_to_files.py +++ b/infrastructure/tournament-util/csv_to_files.py @@ -11,6 +11,5 @@ f.readline() # Skip the title row for line in f.readlines(): team_id, team_name, team_score = line.split(',') - team_name = team_name[1:-1] # Remove quotation marks g.write(team_id+'\n') h.write(team_name+'\n') diff --git a/infrastructure/tournament-util/data/0-example/parsed.txt b/infrastructure/tournament-util/data/0-example/parsed.txt new file mode 100644 index 00000000..2d8786ed --- /dev/null +++ b/infrastructure/tournament-util/data/0-example/parsed.txt @@ -0,0 +1,6 @@ + testteam the redux -vs- Teh Dev | maptestsmall bluewon replay fc87d381b294fb59e34fdd6f3e6370 + Teh Dev -vs- testteam the redux | quadrants bluewon replay 94962a605adad55b5e9d79cc3cc713 + asdfsddtjggyftawehjjhsrsfghgsdf -vs- Teh Dev | circle bluewon replay 9c1d36955c80048c781a29ad80d522 + asdfsddtjggyftawehjjhsrsfghgsdf -vs- testteam the redux | circle bluewon replay 06097511c49d99846fef94190dab4e + Teh Dev -vs- testteam the redux | circle bluewon replay 051411ba85be3f8798aff26993a7e3 + testteam the redux -vs- Teh Dev | circle bluewon replay 41a8169ac95d36565c10c684445622 diff --git a/infrastructure/tournament-util/data/0-example/results.json b/infrastructure/tournament-util/data/0-example/results.json new file mode 100644 index 00000000..029992f5 --- /dev/null +++ b/infrastructure/tournament-util/data/0-example/results.json @@ -0,0 +1,54 @@ +[ + [ + [ + "testteam the redux", + "Teh Dev", + "maptestsmall", + 2, + "fc87d381b294fb59e34fdd6f3e6370" + ], + [ + "Teh Dev", + "testteam the redux", + "quadrants", + 2, + "94962a605adad55b5e9d79cc3cc713" + ] + ], + [ + [ + "asdfsddtjggyftawehjjhsrsfghgsdf", + "Teh Dev", + "circle", + 2, + "9c1d36955c80048c781a29ad80d522" + ] + ], + [ + [ + "asdfsddtjggyftawehjjhsrsfghgsdf", + "testteam the redux", + "circle", + 2, + "06097511c49d99846fef94190dab4e" + ] + ], + [ + [ + "Teh Dev", + "testteam the redux", + "circle", + 2, + "051411ba85be3f8798aff26993a7e3" + ] + ], + [ + [ + "testteam the redux", + "Teh Dev", + "circle", + 2, + "41a8169ac95d36565c10c684445622" + ] + ] +] \ No newline at end of file diff --git a/infrastructure/tournament-util/data/1-sprint1/replay_dump.json b/infrastructure/tournament-util/data/1-sprint1/replay_dump.json new file mode 100644 index 00000000..cf6a9685 --- /dev/null +++ b/infrastructure/tournament-util/data/1-sprint1/replay_dump.json @@ -0,0 +1 @@ +[[["Amoosed", "NeoHazard", "SlowMusic", 2, "4e68fe7ab6b6e496a6fc4a214b855d"], ["NeoHazard", "Amoosed", "Corridor", 2, "b3352046408750bb97adc2281b836a"], ["Amoosed", "NeoHazard", "Gridlock", 2, "efb06babb96565db2cad1fceb977ba"]], [["BeachBANDITS", "Pi over squared", "SlowMusic", 1, "ac9f36bc55ff8edfddd0539da127b1"], ["Pi over squared", "BeachBANDITS", "Corridor", 2, "2b064301799d2b60a443f12b51bbc4"], ["BeachBANDITS", "Pi over squared", "Gridlock", 1, "7f7ad5049ce3b594e5fbc75c208a19"]], [["Egg Clan", "polyteam version 2", "SlowMusic", 2, "7ce4e95730a958acb1ab21a05807cd"], ["polyteam version 2", "Egg Clan", "Corridor", 1, "d40f17a47ad353bba401a9d21c7eb6"], ["Egg Clan", "polyteam version 2", "Gridlock", 2, "8d5593694ba9884d47fc96325125c1"]], [["boib", "The Al Gore Rhythm", "SlowMusic", 2, "97220f5513b6996942db248eaccfbb"], ["The Al Gore Rhythm", "boib", "Corridor", 1, "94bea2ecb76d83a76fc9b21fc0d836"], ["boib", "The Al Gore Rhythm", "Gridlock", 2, "39679aa7625afa87cb7cdaee3b9342"]], [["Alpha Centauri", "ButterBois", "SlowMusic", 1, "72b421c1f4dbd2af05348d4595a615"], ["ButterBois", "Alpha Centauri", "Corridor", 2, "acae997799723509313823bb5f9dea"], ["Alpha Centauri", "ButterBois", "Gridlock", 1, "84f48d7045e50d9deead749215305c"]], [["Serpentine - M.A.R.S.", "The Lurkers in the Wire", "SlowMusic", 1, "15700ec0d11733be18687bdc5cd4c5"], ["The Lurkers in the Wire", "Serpentine - M.A.R.S.", "Corridor", 1, "07bf6b158f1ce2c7896f147908315a"], ["Serpentine - M.A.R.S.", "The Lurkers in the Wire", "Gridlock", 1, "cb490bf363d82307bc43dcdbbff779"]], [["Principia", "The Eager Sloths", "SlowMusic", 1, "aef6a3bb5f02ccaf29e2bc7d7e38fe"], ["The Eager Sloths", "Principia", "Corridor", 2, "8f8f1614bdd95426a2ec8867be7e75"], ["Principia", "The Eager Sloths", "Gridlock", 1, "614d6a2fbb6b4a6b08d0c391b9dbb2"]], [["DaMa", "Balloon Platoon", "SlowMusic", 1, "0a359a03d59f88b675e6f32b88ad68"], ["Balloon Platoon", "DaMa", "Corridor", 2, "1d5fe15e696f5f2acddbf316ec0383"], ["DaMa", "Balloon Platoon", "Gridlock", 1, "4320fdba3df68d2420e14e18ca07ae"]], [["helloMars", "java :ghosthug:", "SlowMusic", 2, "e605bab770f6965cb042cca18a7f26"], ["java :ghosthug:", "helloMars", "Corridor", 1, "043f4b3a668e5c4a8bf657903a92b2"], ["helloMars", "java :ghosthug:", "Gridlock", 2, "d080e9a958568493bcca25e34605b0"]], [["Team Confused", "JT5", "SlowMusic", 1, "06d31819a6d736da3417e13aa54d56"], ["JT5", "Team Confused", "Corridor", 2, "249400fcd2453e46b9e4329514946a"], ["Team Confused", "JT5", "Gridlock", 1, "f36a8e1042091208d57c0196a1d7eb"]], [["CHAD", "BearFish", "SlowMusic", 1, "f76ad509fd829c08b2bdd255d1151b"], ["BearFish", "CHAD", "Corridor", 2, "e65bc4afc7c99765f5287228e71d77"], ["CHAD", "BearFish", "Gridlock", 1, "5be9eb1959e242c9084527ae388c63"]], [["Python Waifu", "Intrepid losers", "SlowMusic", 2, "15eba1b76526770d3ece42bc74d22c"], ["Intrepid losers", "Python Waifu", "Corridor", 2, "1f7dd4865c7b5b4fdec48ec554b049"], ["Python Waifu", "Intrepid losers", "Gridlock", 1, "d5894d13d35df8d3da5f64dd939ff4"]], [["JavaScrapped", "holy choir", "SlowMusic", 1, "133644580e37276775d257e58ffe1b"], ["holy choir", "JavaScrapped", "Corridor", 2, "344c620f7be4783a9a48ebbb2c4343"], ["JavaScrapped", "holy choir", "Gridlock", 1, "e66f2dfd081b59b888863697d5ae4c"]], [["Goreteks", "Children of Talos", "SlowMusic", 1, "a7f9f2b96ae6b4f143bf1baf2ec35f"], ["Children of Talos", "Goreteks", "Corridor", 2, "a71b18b7d2734207e2982b62c3ae63"], ["Goreteks", "Children of Talos", "Gridlock", 1, "fee62bb3cfc804500076024d36b9ab"]], [["InfiniteLoop", "Team_of_One", "SlowMusic", 2, "cf5121bd2444f8371b896bc357fe98"], ["Team_of_One", "InfiniteLoop", "Corridor", 1, "0e4c73fb6e87f88e516dc5f3312632"], ["InfiniteLoop", "Team_of_One", "Gridlock", 2, "54ed6b350d3216022c188a65110d10"]], [["cuttlefish", "SeizeMeansOfSoftwareProduction", "SlowMusic", 1, "ec22af09313b4f9d86fbb7dc8e8949"], ["SeizeMeansOfSoftwareProduction", "cuttlefish", "Corridor", 2, "3cd6426e440f3c1e30fa5175965823"], ["cuttlefish", "SeizeMeansOfSoftwareProduction", "Gridlock", 1, "a7440d90ea252fbf63152d4df7cf32"]], [["Cavalier", "Rael Tarmo T\u00e4hvend", "SlowMusic", 1, "c42feb44629966d63ffab5bb4b5bf8"], ["Rael Tarmo T\u00e4hvend", "Cavalier", "Corridor", 2, "4ad3edb6e651537863fa37ff76ff5a"], ["Cavalier", "Rael Tarmo T\u00e4hvend", "Gridlock", 1, "694ff508861d7ee4cbdafcb9150800"]], [["PenguinBattler", "Quantum", "SlowMusic", 1, "6a3fdcef0ebbf93831daf7c76881ce"], ["Quantum", "PenguinBattler", "Corridor", 2, "b076bafb5b511b13e1ab3fe3795b77"], ["PenguinBattler", "Quantum", "Gridlock", 1, "c7d946efa55102f11bf03c93fcbd35"]], [["ginger cow", "\u300e100910\u300f", "SlowMusic", 1, "f4184dc6dccd266dc0128512c01f6e"], ["\u300e100910\u300f", "ginger cow", "Corridor", 2, "8bce17b0d8fbb1132d150b998d079b"], ["ginger cow", "\u300e100910\u300f", "Gridlock", 1, "9be0ec55206196d2d8eb38cf1b3d79"]], [["The Matarrhites", "Endless Downpour", "SlowMusic", 1, "73802e67600827838cff5f2e96a751"], ["Endless Downpour", "The Matarrhites", "Corridor", 2, "791f20a65c96cb9ecfd1898b66a5be"], ["The Matarrhites", "Endless Downpour", "Gridlock", 1, "c3b62e73bd50f559fa609ad5460d04"]], [["idrc", "CHINA", "SlowMusic", 1, "334acf16bd8a6f8725a6cfed812aea"], ["CHINA", "idrc", "Corridor", 2, "f568c1a1e4524e2cfc7155f3e7810e"], ["idrc", "CHINA", "Gridlock", 1, "8ecc9df8db6848621501306f37d37e"]], [["elongatedmuskrat", "JT6", "SlowMusic", 1, "a5cc30664790d38710bdaab895e70f"], ["JT6", "elongatedmuskrat", "Corridor", 2, "6916a4906d5973a86524627436893b"], ["elongatedmuskrat", "JT6", "Gridlock", 1, "8b965df23c4a7e51c5203e751b8437"]], [["Joe", "Handshakers x3", "SlowMusic", 1, "3e9259fdf6463b650d055418b28dd8"], ["Handshakers x3", "Joe", "Corridor", 2, "4c621236ce56ccdf99604b6fafa82c"], ["Joe", "Handshakers x3", "Gridlock", 1, "38a187c6afe133c453e3551df8aba6"]], [["Big Oh", "Oni Phantom Pog", "SlowMusic", 1, "4679f5e7a918bbb397d3e88be62490"], ["Oni Phantom Pog", "Big Oh", "Corridor", 2, "33865665ecd0b261cfde6cad6f1dde"], ["Big Oh", "Oni Phantom Pog", "Gridlock", 1, "78a19c953507551e7e8e6cc85d1342"]], [["0x5f3759df", "null", "SlowMusic", 1, "431f6b3fa895e567c6079f16693c63"], ["null", "0x5f3759df", "Corridor", 2, "1f26e1a8e08baa786f04ab685d1fb9"], ["0x5f3759df", "null", "Gridlock", 1, "2cf63a96de6979175758a32b7ae04b"]], [["pigeons", "JT3", "SlowMusic", 1, "830d9e603f42fd6255c6c7260dc860"], ["JT3", "pigeons", "Corridor", 2, "591d726b832e11e8ab37f1ccb339ab"], ["pigeons", "JT3", "Gridlock", 1, "e33bbb95cc5c8631b7f07b4ff6dd1e"]], [["Serpentine - Viper", "Snakes and Ladders", "SlowMusic", 1, "08c2c84b2860a34ebe8ff4f111d6da"], ["Snakes and Ladders", "Serpentine - Viper", "Corridor", 2, "02d006452462913bb63e4cf31774c1"], ["Serpentine - Viper", "Snakes and Ladders", "Gridlock", 1, "58a309702b816a5422456628005b45"]], [["lit", "Beginner", "SlowMusic", 1, "5c3d81e576a883bb02df5488c3d020"], ["Beginner", "lit", "Corridor", 2, "3106f347ac9d399b3aa65504cb8041"], ["lit", "Beginner", "Gridlock", 1, "3536bb2a0468e1af171d3cc839386e"]], [["Scopes Monkey Trial 1925", "JT2", "SlowMusic", 2, "7c068cbc9c1903cd96ebce2709e0f4"], ["JT2", "Scopes Monkey Trial 1925", "Corridor", 1, "df149a4cdfb976839344039727da52"], ["Scopes Monkey Trial 1925", "JT2", "Gridlock", 2, "8aa1590b33bb38bce1cb372cba1ba9"]], [["Hippocrene", "JT1", "SlowMusic", 1, "e72244918702746e653f1b06ed27a0"], ["JT1", "Hippocrene", "Corridor", 2, "45403de77328e216607176aef41a15"], ["Hippocrene", "JT1", "Gridlock", 1, "9fda89cbd8349609a71c4bb6a1c892"]], [["flexqueue", "GCI Gophers", "SlowMusic", 2, "4bddf643ea1491f7e709c75d5e2f86"], ["GCI Gophers", "flexqueue", "Corridor", 1, "24376c7d3fd75cd91b1ba8a7d34d8c"], ["flexqueue", "GCI Gophers", "Gridlock", 1, "a8e6982b3a7d2145f24ac3042b9b8b"]], [["CamelMan", "21 Codestreet", "SlowMusic", 2, "f23e642c6807c6eb31cbc8efe29229"], ["21 Codestreet", "CamelMan", "Corridor", 2, "d925716dbb1c3a10b71baa673fafdd"], ["CamelMan", "21 Codestreet", "Gridlock", 2, "a06a6850f8c6e7da7e77acf6ded9c6"]], [["The Method.", "Borscht Bot", "SlowMusic", 1, "c468e13928e759ecf47fb2727f5b82"], ["Borscht Bot", "The Method.", "Corridor", 2, "49f5f0dbad77aeb7a749f489376a81"], ["The Method.", "Borscht Bot", "Gridlock", 1, "1a1328ddfc69f099658df5c4527031"]], [["monky", "Solexa", "SlowMusic", 1, "6536360d3e26ecd5acf7ecb119618e"], ["Solexa", "monky", "Corridor", 2, "0e5a6c9b311b93fe532ae71daad6ea"], ["monky", "Solexa", "Gridlock", 1, "f909531f95f4f060b791e74f396554"]], [["MossyBot", "Homeless Coders", "SlowMusic", 1, "776c9f15ce34c27011a9bd88e9bf7a"], ["Homeless Coders", "MossyBot", "Corridor", 2, "35a6709cbc8f5d9a0bb08d2966a08f"], ["MossyBot", "Homeless Coders", "Gridlock", 1, "b2cc581f91fa00bdf6d4c4a355ab21"]], [["GHS Guardians", "CMU-MIT combo plater", "SlowMusic", 1, "ba2f3c3d0ee79cc72db0169311c7b9"], ["CMU-MIT combo plater", "GHS Guardians", "Corridor", 2, "db4902e027040ee3520f8ebf308a23"], ["GHS Guardians", "CMU-MIT combo plater", "Gridlock", 1, "5d917f2099103efb94da89edeae26e"]], [["Yeet Pull", "doesthisevenwork", "SlowMusic", 1, "fb8a724947a86601a2dba2697f42a5"], ["doesthisevenwork", "Yeet Pull", "Corridor", 2, "a6cbc58472043aa82a773246f55ba6"], ["Yeet Pull", "doesthisevenwork", "Gridlock", 1, "163660b764ec3bc91a924df92f71e8"]], [["Touhutippa", "BattlePath", "SlowMusic", 1, "0a0704bd5a2476b5c3e78fe5da22f1"], ["BattlePath", "Touhutippa", "Corridor", 2, "959a2950a12a2bc243c93919640e1f"], ["Touhutippa", "BattlePath", "Gridlock", 1, "b749a600531d854251ea1552190fd5"]], [["Hertzhaft", "HedgeTech", "SlowMusic", 1, "4a00a2888b3dda04f32d940654b0fd"], ["HedgeTech", "Hertzhaft", "Corridor", 2, "0116a4270defcebb399597456e4444"], ["Hertzhaft", "HedgeTech", "Gridlock", 1, "4fe3b0327a87217d6549b7c8d4e886"]], [["JT4", "69 Tons More Data", "SlowMusic", 1, "d2a57c8b3d1123de9dcaaed995d375"], ["69 Tons More Data", "JT4", "Corridor", 2, "2f51b173701ada29079b593639f21a"], ["JT4", "69 Tons More Data", "Gridlock", 1, "20f48d123ba9ff16e39de6e2fb611e"]], [["Veto", "Toot Toot Train", "SlowMusic", 1, "6cc35103615949b13d295d6538d51c"], ["Toot Toot Train", "Veto", "Corridor", 2, "de158e02e7970afd222ef48c0c9ef8"], ["Veto", "Toot Toot Train", "Gridlock", 1, "1c800d87b731633e7333a5dd13af60"]], [["YA BOI", "Burnerteam; passwordisqweasdqweasd", "SlowMusic", 1, "9570fdc575037869917206a9f8901c"], ["Burnerteam; passwordisqweasdqweasd", "YA BOI", "Corridor", 2, "05dbabc18b1e1343a7e6fbe20c5290"], ["YA BOI", "Burnerteam; passwordisqweasdqweasd", "Gridlock", 1, "01d683747a5d3bdb4399d04c93eece"]], [["blair blezers", "Sagittarius A* Algorithm", "SlowMusic", 1, "ccd7ecdbd6385e19e0d4ec7970f681"], ["Sagittarius A* Algorithm", "blair blezers", "Corridor", 2, "7b089f1f5a1feb3dc83ae83a256993"], ["blair blezers", "Sagittarius A* Algorithm", "Gridlock", 1, "90e07498297484ec879db5daa0a784"]], [["ACM @ UNCC", "MinneCal", "SlowMusic", 1, "0b64bef7fad0c69144131d22b4d037"], ["MinneCal", "ACM @ UNCC", "Corridor", 2, "fb2fbfff1900933152be2d4f6e8db2"], ["ACM @ UNCC", "MinneCal", "Gridlock", 1, "636680747e7aefb01c1cdd387d817e"]], [["ThotBot", "sinksanksunk", "SlowMusic", 1, "e3cc823d9c70ff143465ee035bd671"], ["sinksanksunk", "ThotBot", "Corridor", 2, "214dafd6a0768c208ed94e87d6b9aa"], ["ThotBot", "sinksanksunk", "Gridlock", 1, "50fb88aa53172148db35927d617178"]], [["$nowball", "CodeReapers", "Gridlock", 2, "c3e0cec57197f8aefaa213a9d85a11"], ["CodeReapers", "$nowball", "Arena", 1, "2f438063ed778e880db6b107ff5cc1"], ["$nowball", "CodeReapers", "ExesAndOhs", 2, "deee4137b52a975df508c86d36b403"]], [["The 501st", "The other team", "Gridlock", 2, "42e442e1448cf4aa5e53f7db865afd"], ["The other team", "The 501st", "Arena", 1, "ebdb09be67446b0bb7eea9157c9bb2"], ["The 501st", "The other team", "ExesAndOhs", 2, "386621ef51c53942f6e10cb6a076a5"]], [["01001000", "Team Awesome", "Gridlock", 2, "fe2c45b5e219d9d81d395468a33fcd"], ["Team Awesome", "01001000", "Arena", 1, "b418fbad77f19aaa098b25df071405"], ["01001000", "Team Awesome", "ExesAndOhs", 2, "6fd80b71700f9c9cf6b60cf7822962"]], [["Mycroft", "BossTweed", "Gridlock", 2, "b81e38dd03f67093d90de819f1903d"], ["BossTweed", "Mycroft", "Arena", 1, "4f906b049de725d40775b66a2da48a"], ["Mycroft", "BossTweed", "ExesAndOhs", 2, "79d986e165852df617f78686668dc3"]], [["Ctrl Alt Elite", "fishy yum", "Gridlock", 1, "bd32cd7262ccb19ae41159b677a05e"], ["fishy yum", "Ctrl Alt Elite", "Arena", 2, "96940008a63a9dad0e42a24b4601e0"], ["Ctrl Alt Elite", "fishy yum", "ExesAndOhs", 2, "c4964c24ba9ca0bd16c8990550e568"]], [["Broccoli Bros !", "ERO2", "Gridlock", 1, "5c19756616ac46ff09571eae12ebcd"], ["ERO2", "Broccoli Bros !", "Arena", 2, "6fb58bc654adba091fbb789a096cce"], ["Broccoli Bros !", "ERO2", "ExesAndOhs", 1, "4b7b40b66629535ca0f91a19ffa24f"]], [["Kansas City Asians", "Stepstool", "Gridlock", 1, "8136c42139f66736f42e99fbb84906"], ["Stepstool", "Kansas City Asians", "Arena", 2, "dbcae8fe2e552cc67caaba2d6bf600"], ["Kansas City Asians", "Stepstool", "ExesAndOhs", 1, "0f9cbd5012d8c3ad24d94dbbc93569"]], [["No Battlecode for Old Men", "Influencer Force", "Gridlock", 1, "645881a0279c6fe1c69243e36ffe8b"], ["Influencer Force", "No Battlecode for Old Men", "Arena", 1, "c3a7b60de622c863949b90e2db9cb6"], ["No Battlecode for Old Men", "Influencer Force", "ExesAndOhs", 1, "9166acd56b0730c7bb8ff6ba536503"]], [["armed pythons", "A214", "Gridlock", 2, "fbfaef7f645e0acd7515948046cdb4"], ["A214", "armed pythons", "Arena", 1, "f5abcc164560e96a5adf17a4cce895"], ["armed pythons", "A214", "ExesAndOhs", 2, "e6186fc321bb6f32c5953e0702fcbb"]], [["confused", "Soju", "Gridlock", 2, "8c00a45b5a7d96147bba18bee8f762"], ["Soju", "confused", "Arena", 2, "03563d74e4037fee34ba4e98bed714"], ["confused", "Soju", "ExesAndOhs", 1, "4b21deedf40c86b69354d5b26eeb93"]], [["(+[](){})();", "\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd", "Gridlock", 2, "2068a93dc656b294485c4c22abb70f"], ["\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd", "(+[](){})();", "Arena", 2, "a2ada8d11ba14e87d385b4e8cecdda"], ["(+[](){})();", "\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd\ufdfd", "ExesAndOhs", 1, "f4e14926c590f06402d25f13ec0a24"]], [["Ctrl Alt Defeat", "The Unladen Swallows", "Gridlock", 2, "6449dca66194609d09217128fcd074"], ["The Unladen Swallows", "Ctrl Alt Defeat", "Arena", 1, "dc3f6abdf172cc4d4c657e354efaa8"], ["Ctrl Alt Defeat", "The Unladen Swallows", "ExesAndOhs", 2, "a98e4933f5b9e8583389dfa1fe0745"]], [["bombocombo", "free boba", "Gridlock", 2, "3f2cee7472eba4cb293af956659f41"], ["free boba", "bombocombo", "Arena", 1, "8c892fda4686017888677e581e52df"], ["bombocombo", "free boba", "ExesAndOhs", 2, "bc6b33d25053ef96391eacda8e61ff"]], [["random", "What are you doing stepBot?", "Gridlock", 2, "074b64ee6dfeb63e9e096c1f006920"], ["What are you doing stepBot?", "random", "Arena", 1, "e38dff6fce4d940ca5feac669af2fd"], ["random", "What are you doing stepBot?", "ExesAndOhs", 1, "5ee44cfad76fa4a85a9e58644b678b"]], [["Team Nit(h)ya", "Coconut9", "Gridlock", 2, "a49f951d50985f22c4940e579d302d"], ["Coconut9", "Team Nit(h)ya", "Arena", 1, "9a0e23feebb68f8e6feb015059db4d"], ["Team Nit(h)ya", "Coconut9", "ExesAndOhs", 2, "14105d0d074b2f6f08f7b608fedbc0"]], [["Daedalus", "2b1y", "Gridlock", 1, "c6740d19fa6ecef91bd8eb6aeb67c6"], ["2b1y", "Daedalus", "Arena", 2, "1b6ac77b2d25756f82273913af3186"], ["Daedalus", "2b1y", "ExesAndOhs", 1, "c2c11e41508ee7f222167783048a15"]], [["Double J", "0K", "Gridlock", 2, "116cc9dba03e45c6de79eb8641d650"], ["0K", "Double J", "Arena", 1, "63959603d6f1fb0f12ff2c3fa5f0bc"], ["Double J", "0K", "ExesAndOhs", 2, "517e450bb135509636b531a5cdb93a"]], [["The Night's Watch", "Engineer Gaming", "Gridlock", 2, "f4aec9a65586bcd3617adbb47bbc0d"], ["Engineer Gaming", "The Night's Watch", "Arena", 1, "78592de32d3a4d2d1f2f0a9e4a46ca"], ["The Night's Watch", "Engineer Gaming", "ExesAndOhs", 2, "614d175ae3cb07f46794036cc1f212"]], [["Propaganda Machine", "Malott Fat Cats", "Gridlock", 1, "6d27815dda691ff55633baabd06af9"], ["Malott Fat Cats", "Propaganda Machine", "Arena", 1, "7434a4f26519f39c1011fdcef5aca9"], ["Propaganda Machine", "Malott Fat Cats", "ExesAndOhs", 2, "271b8ff119b494b87d04208766fb1f"]], [["babyducks", "NeoHazard", "Gridlock", 1, "0f33d7d04d019d4d218b2da285cb29"], ["NeoHazard", "babyducks", "Arena", 2, "27da4012e3d9a8aaf545b065b5991b"], ["babyducks", "NeoHazard", "ExesAndOhs", 1, "7788274440de7d5ccbf18a29c15ef3"]], [["Yeicor", "BeachBANDITS", "Gridlock", 1, "32d8a111e8043d82094c9cc3eedc8a"], ["BeachBANDITS", "Yeicor", "Arena", 2, "75cb625710f02b250af5149dc9754a"], ["Yeicor", "BeachBANDITS", "ExesAndOhs", 1, "7b4e212e28e72e4a3c032e202b7c49"]], [["Coast", "polyteam version 2", "Gridlock", 1, "9ecd0aa1ac8329009979c84912d968"], ["polyteam version 2", "Coast", "Arena", 2, "ee80b1f43ba18c35358fcfdb9a1cda"], ["Coast", "polyteam version 2", "ExesAndOhs", 1, "83eabb10b388cbab0ca998665df8e6"]], [["Mars Analytica", "The Al Gore Rhythm", "Gridlock", 2, "e61e9579178cd429ede4352b29c36c"], ["The Al Gore Rhythm", "Mars Analytica", "Arena", 2, "16a3660d18ab528e7e8eea3f59d697"], ["Mars Analytica", "The Al Gore Rhythm", "ExesAndOhs", 1, "5a496dfc61a2163f2d74b03352d718"]], [["Bytecode Mafia", "Alpha Centauri", "Gridlock", 1, "307734dbd0b6e06ecefe3dc395e45e"], ["Alpha Centauri", "Bytecode Mafia", "Arena", 2, "f84067d9a74f575ff2c7bf2d5346da"], ["Bytecode Mafia", "Alpha Centauri", "ExesAndOhs", 1, "96526c1e63b5122590c09590f34fa4"]], [["Chicken", "Serpentine - M.A.R.S.", "Gridlock", 1, "c29b11d5cbbba7d8d8fd75a93fdd0f"], ["Serpentine - M.A.R.S.", "Chicken", "Arena", 2, "1d209ea5b84f486459b0101a9073d1"], ["Chicken", "Serpentine - M.A.R.S.", "ExesAndOhs", 1, "ffe26bf366ad1dba4f2ccfb2fe6107"]], [["I am the Senate", "Principia", "Gridlock", 1, "2e6f97882619fd4a86cdf98283d8f6"], ["Principia", "I am the Senate", "Arena", 2, "71f0bf69faad0b732ea98e221deb77"], ["I am the Senate", "Principia", "ExesAndOhs", 1, "24be687d32b2cca6fbbe0d271f9504"]], [["Harvard sucks lol", "DaMa", "Gridlock", 2, "416ee25ba138294f3e39102675d8fd"], ["DaMa", "Harvard sucks lol", "Arena", 1, "777a676fde65f7e889af02b38d4031"], ["Harvard sucks lol", "DaMa", "ExesAndOhs", 2, "1de16129f02884d8053391c5280bc3"]], [["Chocolate Banana Cake", "java :ghosthug:", "Gridlock", 1, "29f0e79bc2d94a2360b3c63a016f2c"], ["java :ghosthug:", "Chocolate Banana Cake", "Arena", 2, "35ad8626b7d07b977bf292f36e5164"], ["Chocolate Banana Cake", "java :ghosthug:", "ExesAndOhs", 1, "4ad35e68b0aa5f83d428d9a6339535"]], [["Blue Dragon", "Team Confused", "Gridlock", 1, "03b597f6c23e84503e71518887f245"], ["Team Confused", "Blue Dragon", "Arena", 2, "039015c190e0805eeef1c5d1b76ee5"], ["Blue Dragon", "Team Confused", "ExesAndOhs", 1, "57cc46830b05c07d2cbdfa62c55734"]], [["Team Barcode", "CHAD", "Gridlock", 1, "0f32bc09300ecaaf7f4d0d41370a5f"], ["CHAD", "Team Barcode", "Arena", 2, "8237bf343be8177662558a65c5cb80"], ["Team Barcode", "CHAD", "ExesAndOhs", 1, "42416a16f0e87df945c817a5523c71"]], [["waffle", "Python Waifu", "Gridlock", 1, "d087d00b68feb538967ba11870740b"], ["Python Waifu", "waffle", "Arena", 2, "967a7f18fe2ad29104ea6c8636851a"], ["waffle", "Python Waifu", "ExesAndOhs", 1, "ce522597a83fea6636b0b92d3cc958"]], [["AntiVaxxKids", "JavaScrapped", "Gridlock", 1, "4b00bf03ee56786fe6a6e3e64e7983"], ["JavaScrapped", "AntiVaxxKids", "Arena", 2, "d3e7111b3185d1cb8b1a26652bb085"], ["AntiVaxxKids", "JavaScrapped", "ExesAndOhs", 1, "449e83150fc9afdfb2ae2ec99a188a"]], [["Galaga", "Goreteks", "Gridlock", 1, "e247987e2e5d1ab8b09291ed9f0ef7"], ["Goreteks", "Galaga", "Arena", 2, "a4b524da39905cd6ed78134b9021e3"], ["Galaga", "Goreteks", "ExesAndOhs", 1, "0525e63be7728b445b3d8dc7429d30"]], [["wololo", "Team_of_One", "Gridlock", 1, "0458af6b8544607c4241374d63a5c8"], ["Team_of_One", "wololo", "Arena", 2, "9f6757f94a8dbff9c44364f4368cf6"], ["wololo", "Team_of_One", "ExesAndOhs", 1, "256d1d68b9d9e58adfd9bcbc783493"]], [["Large Green Ogres", "cuttlefish", "Gridlock", 2, "c7849a5a1de309b86fd28869309cbb"], ["cuttlefish", "Large Green Ogres", "Arena", 1, "490e5deb12c5f591db3e90cd6d398e"], ["Large Green Ogres", "cuttlefish", "ExesAndOhs", 1, "6494b0d331125a576dd09584f17111"]], [["Lee's Morons", "Cavalier", "Gridlock", 1, "22ee94b1373bd6ce14d843c846e1c2"], ["Cavalier", "Lee's Morons", "Arena", 2, "7e2c254080290c49db26463fef4095"], ["Lee's Morons", "Cavalier", "ExesAndOhs", 1, "2329795beeddf1b43cbbff3fa1e8a7"]], [["Hard Coders", "PenguinBattler", "Gridlock", 1, "ea15f528a468e8357a04cc619e819a"], ["PenguinBattler", "Hard Coders", "Arena", 2, "06a61ea1902ffbec2705e5056bc8b9"], ["Hard Coders", "PenguinBattler", "ExesAndOhs", 1, "0695ae5196c012b3035642daa4ae44"]], [["Kryptonite", "ginger cow", "Gridlock", 1, "dbb525a56ae45baaa788e6577cc67d"], ["ginger cow", "Kryptonite", "Arena", 2, "2ebf109f32a9e9eeaba10a6d292ed6"], ["Kryptonite", "ginger cow", "ExesAndOhs", 1, "7c914ffce99c482dd1480b37bedf39"]], [["Pain", "The Matarrhites", "Gridlock", 1, "3ef4fb69b2d51b4c9ce77258c3efad"], ["The Matarrhites", "Pain", "Arena", 2, "46a88213bf3aa44d0f4258c06feb8b"], ["Pain", "The Matarrhites", "ExesAndOhs", 1, "92a1aaf461b5f2bbe2f2cc7af953a2"]], [["camel_case", "idrc", "Gridlock", 1, "73db97de6e73a7fc61087f5ec61cbb"], ["idrc", "camel_case", "Arena", 2, "43883745d469ff486d2f575fd3c96f"], ["camel_case", "idrc", "ExesAndOhs", 1, "26f2bcdde696a794d3964b9d2b3611"]], [["tooOldForThis", "elongatedmuskrat", "Gridlock", 1, "b05cd83540165b8bdcde80f97390f4"], ["elongatedmuskrat", "tooOldForThis", "Arena", 2, "5cf653a898809519c4f0a30a881c6b"], ["tooOldForThis", "elongatedmuskrat", "ExesAndOhs", 1, "39b7df9bf5585f1318154efc995683"]], [["Oculus", "Joe", "Gridlock", 1, "1d97848bb64bfbe2ad25822a76c9dd"], ["Joe", "Oculus", "Arena", 2, "80e7ea29149becc56c955d09db9ac4"], ["Oculus", "Joe", "ExesAndOhs", 1, "466f843583fa8a50ba8a23e72355ea"]], [["Super Cow Powers", "Big Oh", "Gridlock", 1, "c0762c9c1c2d5dadbb05ba8d00ebe6"], ["Big Oh", "Super Cow Powers", "Arena", 2, "7923e022799ff2c5c31c9ff923ff7b"], ["Super Cow Powers", "Big Oh", "ExesAndOhs", 1, "fac2fc9af5becebfb87d3b78a1e110"]], [["Play C. Holder", "0x5f3759df", "Gridlock", 1, "f1255f766948fb1654ed70777e85f4"], ["0x5f3759df", "Play C. Holder", "Arena", 2, "1c171b36a23508fdeb4aba824bbe28"], ["Play C. Holder", "0x5f3759df", "ExesAndOhs", 1, "c9f258086316d119400fb575ef49c8"]], [["Kruskal's Kompadres", "pigeons", "Gridlock", 1, "f5dc451face0b7ac40e56d42d5f0ac"], ["pigeons", "Kruskal's Kompadres", "Arena", 1, "a296a9498e24d6186de67f3a27330c"], ["Kruskal's Kompadres", "pigeons", "ExesAndOhs", 2, "6eaae0428a6c2d93fb632bc2ad6bcb"]], [["Rua!!", "Serpentine - Viper", "Gridlock", 1, "ec538ce61a99d271cdf87b69b002ed"], ["Serpentine - Viper", "Rua!!", "Arena", 2, "f7caaf59d1db8840df5d821e26ac05"], ["Rua!!", "Serpentine - Viper", "ExesAndOhs", 1, "294ebfa740bade85b2ecd0cc012afd"]], [["Huge L Club", "lit", "Gridlock", 1, "adc6225d057979c27110e27a0dfb35"], ["lit", "Huge L Club", "Arena", 2, "dbd9e6c163408e24e94ba7585a57a9"], ["Huge L Club", "lit", "ExesAndOhs", 1, "33317515569db77be0afca4a755bd7"]], [["3 Musketeers", "JT2", "Gridlock", 1, "9a971f49c324d0f48b2f9b544c7314"], ["JT2", "3 Musketeers", "Arena", 2, "5e566698da6a1c7fae477f120d8d26"], ["3 Musketeers", "JT2", "ExesAndOhs", 1, "8573aa43017bab5255ab6fc4836b27"]], [["StepZero", "Hippocrene", "Gridlock", 1, "4a3fba97253353bb8172a3e51f5848"], ["Hippocrene", "StepZero", "Arena", 2, "9c67c84c33d290313554928e073987"], ["StepZero", "Hippocrene", "ExesAndOhs", 1, "95d3cf81a53b49877536ec57769fea"]], [["bumbum", "GCI Gophers", "Gridlock", 1, "45ab1b7f413abab8474ed8327729c9"], ["GCI Gophers", "bumbum", "Arena", 2, "a29dc8b30f2d2f61ee0167087bfada"], ["bumbum", "GCI Gophers", "ExesAndOhs", 1, "b425d32f62f730fbdff96a77923f74"]], [["Code Not Found", "21 Codestreet", "Gridlock", 1, "991c193fb0e17995ab811e00be92a9"], ["21 Codestreet", "Code Not Found", "Arena", 2, "2205ff2a7b0d3a679a0db87992cbb8"], ["Code Not Found", "21 Codestreet", "ExesAndOhs", 1, "5edaf58bddde142a3e7689eb86e6ed"]], [["Atom", "The Method.", "Gridlock", 1, "bc56b9f63ee01b82937ef958008686"], ["The Method.", "Atom", "Arena", 1, "3442b12fa3a2f548112aa9784e2624"], ["Atom", "The Method.", "ExesAndOhs", 2, "487495076dcdee35da24aaa8950933"]], [["Blue Steel", "monky", "Gridlock", 1, "6c0244b72f8ae78003b5cf2e28b9e3"], ["monky", "Blue Steel", "Arena", 2, "a2791f067d4ca42f33d98dbbca66dc"], ["Blue Steel", "monky", "ExesAndOhs", 1, "f1ac64d530bfe797982fa0f54c24ca"]], [["Nikola", "MossyBot", "Gridlock", 1, "87b2025df45d424cdb59f6cd8f6019"], ["MossyBot", "Nikola", "Arena", 2, "4f75fcb298bfafd0e376125ce89d8a"], ["Nikola", "MossyBot", "ExesAndOhs", 1, "52baf77b8628c9338922685e8807b3"]], [["Download More RAM", "GHS Guardians", "Gridlock", 1, "968b71a31e906f1253410857af8a17"], ["GHS Guardians", "Download More RAM", "Arena", 2, "e35ce37458d31fb33c345394f445bd"], ["Download More RAM", "GHS Guardians", "ExesAndOhs", 1, "1cbc93906bcf3bea8b22ff47646f8e"]], [["The Patriots", "Yeet Pull", "Gridlock", 1, "9e0e2d5c724ab07ebe5852f87b807e"], ["Yeet Pull", "The Patriots", "Arena", 2, "eaa1b1d3256f818d7437ce4459a9d0"], ["The Patriots", "Yeet Pull", "ExesAndOhs", 1, "110b65efa6ec49eac5ce929e342271"]], [["Dis Team", "Touhutippa", "Gridlock", 1, "ff8c45ebc302890f63d99f85a6e98b"], ["Touhutippa", "Dis Team", "Arena", 2, "a9d9e4b72421042190f53422150934"], ["Dis Team", "Touhutippa", "ExesAndOhs", 1, "1067430dca5c80c7d18565b2f6c744"]], [["Ripples", "Hertzhaft", "Gridlock", 1, "489deb8b5f6d0980a2d1491f0aed7f"], ["Hertzhaft", "Ripples", "Arena", 2, "8ba1d5eb3a4b389b4165ed1a752834"], ["Ripples", "Hertzhaft", "ExesAndOhs", 1, "a613330f989aff5770f61485ce2a42"]], [["Producing Perfection", "JT4", "Gridlock", 1, "cbb9bead7441fd8e700dce34e97b4c"], ["JT4", "Producing Perfection", "Arena", 2, "fff02b8884d659ae8cf9b664a35185"], ["Producing Perfection", "JT4", "ExesAndOhs", 1, "f0442566aafc066369d18221d56d04"]], [["sucks2BU", "Veto", "Gridlock", 2, "a588c70e14513a8890444efa5d1adf"], ["Veto", "sucks2BU", "Arena", 1, "dd2ae34510601d95692eb034731457"], ["sucks2BU", "Veto", "ExesAndOhs", 2, "67746ea0b9c65a561fecc3319f7aaf"]], [["Up For A While", "YA BOI", "Gridlock", 2, "8646ed68984912d6b16f3933386193"], ["YA BOI", "Up For A While", "Arena", 2, "59c6639e65dba079d01a3ac4dd32dc"], ["Up For A While", "YA BOI", "ExesAndOhs", 1, "ea6c8b83abb9410b173ebf5b6253b2"]], [["Chop Suey", "blair blezers", "Gridlock", 2, "8b331b688ffbc4d1d12e8217cdf9f9"], ["blair blezers", "Chop Suey", "Arena", 1, "92d71188f9f4d220b46306fe0b7111"], ["Chop Suey", "blair blezers", "ExesAndOhs", 2, "387d678f95db6a64fcca62c21ed018"]], [["remotED", "ACM @ UNCC", "Gridlock", 1, "94232d3c837dc3bbd92f61a3a7db9d"], ["ACM @ UNCC", "remotED", "Arena", 1, "1aead85d9ffdcf0fc149d9881e9a18"], ["remotED", "ACM @ UNCC", "ExesAndOhs", 1, "df9069f32a7676188be8de2700aa15"]], [["BASHA ESPORTS", "ThotBot", "Gridlock", 2, "7339e5ee4c83a94f0b3befa176b972"], ["ThotBot", "BASHA ESPORTS", "Arena", 1, "3341c64ee7f85d9c24427cb9a720d4"], ["BASHA ESPORTS", "ThotBot", "ExesAndOhs", 2, "b0026f8ab7c9b0032cf4673ec6a45c"]], [["babyducks", "CodeReapers", "ExesAndOhs", 1, "0c909ec529025cf56129afc5a97704"], ["CodeReapers", "babyducks", "Bog", 2, "af579089839d857ed5bd9812798d7e"], ["babyducks", "CodeReapers", "Chevron", 1, "ea0b6aa0bea72fbb50aed527ac939d"]], [["Yeicor", "Coast", "ExesAndOhs", 2, "579aed9c6405b48779f9724812ca79"], ["Coast", "Yeicor", "Bog", 1, "5b0550d88083c0bd7562ad6b51eb4d"], ["Yeicor", "Coast", "Chevron", 2, "edc04146ce6bfaf3312facc53b1530"]], [["Mars Analytica", "The other team", "ExesAndOhs", 1, "8d22eaeea375d863bb5a1aa8acfe58"], ["The other team", "Mars Analytica", "Bog", 2, "c25e7db3e99fddebbce3ccdb2597ea"], ["Mars Analytica", "The other team", "Chevron", 1, "f348ac48ab27db746dc4c0d02c2c0d"]], [["Bytecode Mafia", "Team Awesome", "ExesAndOhs", 1, "b5e42bf2871773d03f3f35d7e00962"], ["Team Awesome", "Bytecode Mafia", "Bog", 2, "6ad869ce21450484d9246ed65f1c33"], ["Bytecode Mafia", "Team Awesome", "Chevron", 1, "511c8b30e8191e0f4a48f349ad710c"]], [["Chicken", "BossTweed", "ExesAndOhs", 1, "d61b8c70cf01547635e4ae4421ff91"], ["BossTweed", "Chicken", "Bog", 2, "99671457e2a4147005fd0c7029020a"], ["Chicken", "BossTweed", "Chevron", 1, "aaf6abcb3cc1026aec0b125b0696fd"]], [["I am the Senate", "DaMa", "ExesAndOhs", 2, "46811ca18ad261dbd3765ebc172e74"], ["DaMa", "I am the Senate", "Bog", 1, "eb39f0a492302f7889f1ce60dfa9a7"], ["I am the Senate", "DaMa", "Chevron", 1, "e2f7a56e0f74a9152a6b0a0e67b95a"]], [["Chocolate Banana Cake", "Ctrl Alt Elite", "ExesAndOhs", 1, "d3ad7c1e2732ea109185f7c93937f5"], ["Ctrl Alt Elite", "Chocolate Banana Cake", "Bog", 2, "8577d724ad1e3c71e1d96005f71193"], ["Chocolate Banana Cake", "Ctrl Alt Elite", "Chevron", 1, "589b679732c3e9780bc7d9dcc21809"]], [["Blue Dragon", "Team Barcode", "ExesAndOhs", 2, "0e6dc32a1d83a9a9a57d6199c8dd22"], ["Team Barcode", "Blue Dragon", "Bog", 2, "6f2a60bdfdecda7e0559614b72a17a"], ["Blue Dragon", "Team Barcode", "Chevron", 2, "a9d3308e2f907614b2ad20a58fb710"]], [["waffle", "Broccoli Bros !", "ExesAndOhs", 1, "dd079c9ede562d37ee90a6bf408cb2"], ["Broccoli Bros !", "waffle", "Bog", 2, "bf7cae120ca34d62f2860eb6306145"], ["waffle", "Broccoli Bros !", "Chevron", 1, "b0d66f44b26909e3a584a18d479430"]], [["AntiVaxxKids", "Galaga", "ExesAndOhs", 1, "5497eee5b0a7c3089f4193b60da310"], ["Galaga", "AntiVaxxKids", "Bog", 2, "555fe97ef0876403d5d392b410fdea"], ["AntiVaxxKids", "Galaga", "Chevron", 1, "10f93ad5bf84f6a6b303bbae563b71"]], [["wololo", "Kansas City Asians", "ExesAndOhs", 1, "41ab25579262c9525af9f943ec4430"], ["Kansas City Asians", "wololo", "Bog", 1, "886ae0d7b32a8adf5da8c7fec8f0b0"], ["wololo", "Kansas City Asians", "Chevron", 2, "10a04f7b8112679b0fc8036c3c39d9"]], [["cuttlefish", "Lee's Morons", "ExesAndOhs", 2, "7eee15b21046352842095471270797"], ["Lee's Morons", "cuttlefish", "Bog", 1, "02508f42909f8dc6aa7720c5161e74"], ["cuttlefish", "Lee's Morons", "Chevron", 2, "62341af9fc6c945ceb19b31a7c4aea"]], [["Hard Coders", "No Battlecode for Old Men", "ExesAndOhs", 1, "bc587c7387d0ba2e9396f4c8a61edb"], ["No Battlecode for Old Men", "Hard Coders", "Bog", 2, "cb344935b2827c904f699db665560b"], ["Hard Coders", "No Battlecode for Old Men", "Chevron", 1, "03d0933e37b1fe40ebc64090c3337e"]], [["Kryptonite", "Pain", "ExesAndOhs", 1, "d1f030ae4709db43c69169c08aed82"], ["Pain", "Kryptonite", "Bog", 2, "71e26937cfeefeac42206419247135"], ["Kryptonite", "Pain", "Chevron", 1, "cce94659d1c846222ed252782535ba"]], [["camel_case", "A214", "ExesAndOhs", 1, "501bcd34ba81394dcc2f31db3d75fb"], ["A214", "camel_case", "Bog", 2, "fac9ae25801e9eb5cc15b9bb7515b8"], ["camel_case", "A214", "Chevron", 1, "c3a3b715ebc5fb7e1ccbb589a8a657"]], [["tooOldForThis", "Oculus", "ExesAndOhs", 1, "7a61675ed375b21ff64153815412dd"], ["Oculus", "tooOldForThis", "Bog", 2, "db1fc171a8ac2fb3cd2a3bc3d14452"], ["tooOldForThis", "Oculus", "Chevron", 1, "b43fcbfda963e8d58b6986c78924b9"]], [["Super Cow Powers", "confused", "ExesAndOhs", 1, "f4dfbe7b70f265de7f0101c03b1990"], ["confused", "Super Cow Powers", "Bog", 2, "a1dee719fc137c6fa196a403850756"], ["Super Cow Powers", "confused", "Chevron", 1, "36f632b0c830d601c5a4d34b6dee2d"]], [["Play C. Holder", "pigeons", "ExesAndOhs", 2, "05348b0c9a064578dbab2eee9ef025"], ["pigeons", "Play C. Holder", "Bog", 1, "9d45571b95855e172323427c43be6a"], ["Play C. Holder", "pigeons", "Chevron", 1, "1e286a11d4b1e8e951a08f76387c8f"]], [["Rua!!", "(+[](){})();", "ExesAndOhs", 2, "2a60a3d21b5a6e9ffd64cf0d7721e4"], ["(+[](){})();", "Rua!!", "Bog", 2, "bfb75930648f831d4cbd497bb841e6"], ["Rua!!", "(+[](){})();", "Chevron", 1, "67af049be381aaf177f3842d4287bc"]], [["Huge L Club", "The Unladen Swallows", "ExesAndOhs", 1, "0c21755bfbcfb2f14ca81d726ee5bc"], ["The Unladen Swallows", "Huge L Club", "Bog", 2, "d56636c1416648f6f2a74361cf8b7e"], ["Huge L Club", "The Unladen Swallows", "Chevron", 1, "08a5a2dc5def0b568a4311c8d1dab6"]], [["3 Musketeers", "free boba", "ExesAndOhs", 1, "5265ad079c79e087c1ccefce01f804"], ["free boba", "3 Musketeers", "Bog", 2, "5be1f2d113369694f59ce6a4ebf7fd"], ["3 Musketeers", "free boba", "Chevron", 1, "6990108583403fc6e2be9f6e588b93"]], [["StepZero", "bumbum", "ExesAndOhs", 1, "022241fceaf7ca9a000ee93145513e"], ["bumbum", "StepZero", "Bog", 2, "17fcbe03de01b9cfb52768aa79b11e"], ["StepZero", "bumbum", "Chevron", 1, "8419ea381dfb3a6dda24c5a1e4e07f"]], [["Code Not Found", "What are you doing stepBot?", "ExesAndOhs", 2, "21385bdaf9eccd695baf0052f583cc"], ["What are you doing stepBot?", "Code Not Found", "Bog", 2, "33fa77d9b3247c641026ad99f8877f"], ["Code Not Found", "What are you doing stepBot?", "Chevron", 1, "6434ed05329392870fb2a67790b2c1"]], [["The Method.", "Blue Steel", "ExesAndOhs", 2, "10bde96addde86524368cb7a59d9f5"], ["Blue Steel", "The Method.", "Bog", 1, "60b03467942cf1b398542710891225"], ["The Method.", "Blue Steel", "Chevron", 2, "8b83daa01ee17d0deabeddd71190c2"]], [["Nikola", "Coconut9", "ExesAndOhs", 1, "851a0314b23b17be31d6003fad8444"], ["Coconut9", "Nikola", "Bog", 2, "57cadbea3910197b65569ca7675409"], ["Nikola", "Coconut9", "Chevron", 1, "58d8561109d039081848af3340d4b3"]], [["Download More RAM", "The Patriots", "ExesAndOhs", 1, "ed02197927b849ebfa6dec7063ec6b"], ["The Patriots", "Download More RAM", "Bog", 2, "88a9f8a4710f72f0fa0fd48ffdd7df"], ["Download More RAM", "The Patriots", "Chevron", 1, "c681104115288afea779d1e44695d6"]], [["Dis Team", "Daedalus", "ExesAndOhs", 1, "f71658ce0e3c25536a8330cbfbf4a5"], ["Daedalus", "Dis Team", "Bog", 2, "f2fe71704b50aaf1786f3caae19674"], ["Dis Team", "Daedalus", "Chevron", 1, "d62e1dfa42f9e7922cd585f5986132"]], [["Ripples", "0K", "ExesAndOhs", 1, "4c458b0a626dc9c1633428bb9a2634"], ["0K", "Ripples", "Bog", 2, "7e6c284c74b9edae73c9f27991dd99"], ["Ripples", "0K", "Chevron", 2, "dd3a4710bed854669523d711c824ba"]], [["Producing Perfection", "Engineer Gaming", "ExesAndOhs", 1, "fc11a02893546ec7a021d34e18a938"], ["Engineer Gaming", "Producing Perfection", "Bog", 2, "f8a6690c747de6fd8eda1a066e2a70"], ["Producing Perfection", "Engineer Gaming", "Chevron", 1, "ee40ca00a7e99f909ae18e0cb8f3da"]], [["Veto", "Up For A While", "ExesAndOhs", 1, "6960009ded2047c134d2c7f6eb3c0e"], ["Up For A While", "Veto", "Bog", 2, "0ba21993f235e0072dff7de1cb3bc4"], ["Veto", "Up For A While", "Chevron", 2, "6b0705f3eaa807500961cb8fe75441"]], [["blair blezers", "Malott Fat Cats", "ExesAndOhs", 1, "d7bbbd92f93fc6c81daf64dabc4e3c"], ["Malott Fat Cats", "blair blezers", "Bog", 1, "b6c25530c6e33da10af6cbf6abec99"], ["blair blezers", "Malott Fat Cats", "Chevron", 2, "7aaaeae16853b8c4de9620dea818ba"]], [["remotED", "ThotBot", "ExesAndOhs", 1, "f2125fd661fb7edc148899baaa7959"], ["ThotBot", "remotED", "Bog", 1, "caa249dbecdd4fb472dc425d7a6b6a"], ["remotED", "ThotBot", "Chevron", 1, "fdc3ccc949ea6b387c4112808f39fd"]], [["babyducks", "Coast", "Chevron", 1, "ba31fb94bb56253f3c815f8b1410ce"], ["Coast", "babyducks", "CrossStitch", 1, "eebe3f1c6a4a81ab60076aeca90ecb"], ["babyducks", "Coast", "CrownJewels", 1, "2b57056e04e2387133b1d5ab0b0991"]], [["Mars Analytica", "Bytecode Mafia", "Chevron", 1, "5f7ec0f068ac73d9d98de4006572da"], ["Bytecode Mafia", "Mars Analytica", "CrossStitch", 2, "c2de2b6a24976536db0cb841a290ad"], ["Mars Analytica", "Bytecode Mafia", "CrownJewels", 2, "1655404cc8df0b56def13a122c75e7"]], [["Chicken", "DaMa", "Chevron", 1, "a65d534870c5229ca6423b68c13ace"], ["DaMa", "Chicken", "CrossStitch", 2, "9edc7a4174040a012618d532618552"], ["Chicken", "DaMa", "CrownJewels", 1, "49da8c34c2f6e2b396359615eb54da"]], [["Chocolate Banana Cake", "Team Barcode", "Chevron", 2, "5eff0c192a84cb5324a84f11a828a4"], ["Team Barcode", "Chocolate Banana Cake", "CrossStitch", 1, "f942d1b9cff2687961d35406211c90"], ["Chocolate Banana Cake", "Team Barcode", "CrownJewels", 1, "af6385645df4b8cb59dec456b74b2e"]], [["waffle", "AntiVaxxKids", "Chevron", 1, "8c2bcef17704f12b5f7af2ec49cfdc"], ["AntiVaxxKids", "waffle", "CrossStitch", 2, "b8dcdd11799979cca176e47c776dd3"], ["waffle", "AntiVaxxKids", "CrownJewels", 1, "0568f56e3ad3be5c4bca31fd131011"]], [["Kansas City Asians", "Lee's Morons", "Chevron", 2, "a862d702b27986f18231ca8ea95863"], ["Lee's Morons", "Kansas City Asians", "CrossStitch", 1, "ae0e28a2fb15a33468251851cd2c87"], ["Kansas City Asians", "Lee's Morons", "CrownJewels", 2, "2f9f0ad470db3cfc81448c3d4f99be"]], [["Hard Coders", "Kryptonite", "Chevron", 2, "8ee187176d60359acd7dad513a74a6"], ["Kryptonite", "Hard Coders", "CrossStitch", 2, "c9da9ff91e49938df8ca76ca3bfb52"], ["Hard Coders", "Kryptonite", "CrownJewels", 2, "c70146607a429ddff4dc580ac36299"]], [["camel_case", "tooOldForThis", "Chevron", 1, "41f40a352dc394d6b40a96948681a5"], ["tooOldForThis", "camel_case", "CrossStitch", 1, "00836188f372dbd03fcf25f3afd7ee"], ["camel_case", "tooOldForThis", "CrownJewels", 1, "423fdb754ce3de38705328c8d187ce"]], [["Super Cow Powers", "pigeons", "Chevron", 1, "4913473aedc877d3745d444e53267a"], ["pigeons", "Super Cow Powers", "CrossStitch", 2, "41162ea1e474bfc59f832fcc190191"], ["Super Cow Powers", "pigeons", "CrownJewels", 1, "449445dec8a0c29060c6affb47ba27"]], [["Rua!!", "Huge L Club", "Chevron", 1, "c9b7fcb86af087ee187b95dd6aa3ef"], ["Huge L Club", "Rua!!", "CrossStitch", 2, "660c6a0ab8420e9aebca44830ff0de"], ["Rua!!", "Huge L Club", "CrownJewels", 2, "17f9401653865c6d25ab2000d80d8a"]], [["3 Musketeers", "StepZero", "Chevron", 1, "09636d2c31d464873706a5340bc67c"], ["StepZero", "3 Musketeers", "CrossStitch", 2, "82141563bb94ac70bafa34b5f25ffc"], ["3 Musketeers", "StepZero", "CrownJewels", 1, "74abb881f0286f8e676dbed31056a7"]], [["Code Not Found", "Blue Steel", "Chevron", 1, "bd4a65357952ec08525124763a8c26"], ["Blue Steel", "Code Not Found", "CrossStitch", 1, "9e476e2e03d8d99835e68e16ee2a92"], ["Code Not Found", "Blue Steel", "CrownJewels", 2, "860e0cba96928b2d67eefe731a4840"]], [["Nikola", "Download More RAM", "Chevron", 2, "8ca9e4c7750f9ab3a8b699ed95147c"], ["Download More RAM", "Nikola", "CrossStitch", 2, "de75d7f5f873ce5b77ab4bc7e72553"], ["Nikola", "Download More RAM", "CrownJewels", 2, "046413c1233738e9e2fd13ce29cf6c"]], [["Dis Team", "Ripples", "Chevron", 1, "d5028d1e70367330574ce71525cd84"], ["Ripples", "Dis Team", "CrossStitch", 2, "f94bcdf2f33300bc857b1be073d889"], ["Dis Team", "Ripples", "CrownJewels", 2, "18af2775d6a936e3d37bbd74f71100"]], [["Producing Perfection", "Veto", "Chevron", 1, "9eb4182fe7d871f5803b779fd236a9"], ["Veto", "Producing Perfection", "CrossStitch", 2, "9de7cf111587d4f0e2a3748f6c05e4"], ["Producing Perfection", "Veto", "CrownJewels", 1, "c1f24fdea939307c11faa7978a6f9b"]], [["Malott Fat Cats", "remotED", "Chevron", 2, "a7ecb60bf5ee5d5d0f4a3d475ce805"], ["remotED", "Malott Fat Cats", "CrossStitch", 1, "3bbc75d27becb6dfc064d28dd34634"], ["Malott Fat Cats", "remotED", "CrownJewels", 2, "3c56ddfee960cfdb495323d6a17fd9"]], [["babyducks", "Mars Analytica", "CrownJewels", 1, "c8d1b602f9a67d35d038991f9fc4a3"], ["Mars Analytica", "babyducks", "Cow", 2, "9b7bce1ad0efa869848c0c2fdb5169"], ["babyducks", "Mars Analytica", "Branches", 1, "960184dd05cd5145a345303e469eaf"]], [["Chicken", "Team Barcode", "CrownJewels", 1, "7f2b3699cd3aab7c5658bb14813851"], ["Team Barcode", "Chicken", "Cow", 2, "e452686e275b1b21eaf7f81e33ff88"], ["Chicken", "Team Barcode", "Branches", 1, "df9812b835a5010ad11dcd90b4df50"]], [["waffle", "Lee's Morons", "CrownJewels", 1, "77eafcdd8b4bbba77a6dfed879f119"], ["Lee's Morons", "waffle", "Cow", 2, "e91ab9ef81423261f933111de4d2c9"], ["waffle", "Lee's Morons", "Branches", 1, "65bcbc78f68924d5770d9a2f99aa81"]], [["Kryptonite", "camel_case", "CrownJewels", 1, "7a6e5f11c019772555a0c371b6dd81"], ["camel_case", "Kryptonite", "Cow", 2, "4b470c1a445a8e57bff7f248aa4338"], ["Kryptonite", "camel_case", "Branches", 1, "6f7bd0ffc463b34e72cbc550930f1b"]], [["Super Cow Powers", "Rua!!", "CrownJewels", 1, "65bf80e805200eab4c15841c87df6f"], ["Rua!!", "Super Cow Powers", "Cow", 2, "8a6ea307a56a569dfa83a8989b27c7"], ["Super Cow Powers", "Rua!!", "Branches", 1, "394c65e85bbe7b3ff1059d6812d581"]], [["3 Musketeers", "Blue Steel", "CrownJewels", 1, "f830bc3fc373c161baf1e777fbcbec"], ["Blue Steel", "3 Musketeers", "Cow", 2, "f8e9809360a2dcecb7e74e688bcaee"], ["3 Musketeers", "Blue Steel", "Branches", 1, "343c11636e213103ce8b68d01084ce"]], [["Download More RAM", "Dis Team", "CrownJewels", 2, "22be024c0b3cc4c454bd0d08137a23"], ["Dis Team", "Download More RAM", "Cow", 1, "c74b88fd034d72d0b8ba969cc952e6"], ["Download More RAM", "Dis Team", "Branches", 1, "74ae39d307074b177fdce96b69078a"]], [["Producing Perfection", "remotED", "CrownJewels", 1, "6363c8db4c8a72ce65da9912f41a77"], ["remotED", "Producing Perfection", "Cow", 2, "b2da277f365f7465ecdbda0bf31799"], ["Producing Perfection", "remotED", "Branches", 1, "ef1936d20e319c6524703435968d76"]], [["babyducks", "Chicken", "Branches", 1, "e932b72f70a484455a879bbd0164f1"], ["Chicken", "babyducks", "Andromeda", 2, "1142f78b7e58e8f4116ec4649f6a11"], ["babyducks", "Chicken", "FiveOfHearts", 1, "380019a7e14f76541c6027d3faa2d8"]], [["waffle", "Kryptonite", "Branches", 1, "87a698bbe39c11f9b67d87124535b6"], ["Kryptonite", "waffle", "Andromeda", 2, "9b853a833b78e6afdeb219308f4674"], ["waffle", "Kryptonite", "FiveOfHearts", 1, "872f3ecb929b5e32ff376fb984a5bc"]], [["Super Cow Powers", "3 Musketeers", "Branches", 1, "c308e3e2917e470ba8a1211890a92b"], ["3 Musketeers", "Super Cow Powers", "Andromeda", 2, "dc97fb822d01d1ffd7f358d4c6f4ab"], ["Super Cow Powers", "3 Musketeers", "FiveOfHearts", 1, "f42acba5680ebba1ac0cb8562c9326"]], [["Dis Team", "Producing Perfection", "Branches", 2, "ff4bf1c12dbb1c8125e59714728950"], ["Producing Perfection", "Dis Team", "Andromeda", 1, "bd8a324f514d8c69cf5f234ff1ca5e"], ["Dis Team", "Producing Perfection", "FiveOfHearts", 2, "ed1923f7ba85f2481c7ea0d995a75b"]], [["babyducks", "waffle", "FiveOfHearts", 1, "5eaf0f18abb49eebbee465894eea9b"], ["waffle", "babyducks", "Rainbow", 2, "e3aafe5c91ecc8520b4e8a52fd7ef2"], ["babyducks", "waffle", "Illusion", 1, "f83bee62b7a70e6323bc96a620894e"]], [["Super Cow Powers", "Producing Perfection", "FiveOfHearts", 1, "f1c847903038ac8796e09378d6c16f"], ["Producing Perfection", "Super Cow Powers", "Rainbow", 2, "e630c7f313ac4661cdad77ed9b29d4"], ["Super Cow Powers", "Producing Perfection", "Illusion", 1, "07c3e65dd56902bc0428ceca897d5d"]], [["babyducks", "Super Cow Powers", "Illusion", 2, "2d862833a6366076aa64839c2d843f"], ["Super Cow Powers", "babyducks", "Snowflake", 1, "ae2ec48c78a79112e3abe6ec8baee4"], ["babyducks", "Super Cow Powers", "NotAPuzzle", 1, "f8ce4525b058cc10a5011961cab46d"]]] \ No newline at end of file diff --git a/infrastructure/tournament-util/data/1-sprint1/replay_dump_parsed.txt b/infrastructure/tournament-util/data/1-sprint1/replay_dump_parsed.txt new file mode 100644 index 00000000..01d68977 --- /dev/null +++ b/infrastructure/tournament-util/data/1-sprint1/replay_dump_parsed.txt @@ -0,0 +1,2408 @@ +Amoosed -vs- NeoHazard +winner: NeoHazard +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4e68fe7ab6b6e496a6fc4a214b855d.bc21 + +NeoHazard -vs- Amoosed +winner: Amoosed +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b3352046408750bb97adc2281b836a.bc21 + +Amoosed -vs- NeoHazard +winner: NeoHazard +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/efb06babb96565db2cad1fceb977ba.bc21 + + + +BeachBANDITS -vs- Pi over squared +winner: BeachBANDITS +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ac9f36bc55ff8edfddd0539da127b1.bc21 + +Pi over squared -vs- BeachBANDITS +winner: BeachBANDITS +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2b064301799d2b60a443f12b51bbc4.bc21 + +BeachBANDITS -vs- Pi over squared +winner: BeachBANDITS +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7f7ad5049ce3b594e5fbc75c208a19.bc21 + + + +Egg Clan -vs- polyteam version 2 +winner: polyteam version 2 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7ce4e95730a958acb1ab21a05807cd.bc21 + +polyteam version 2 -vs- Egg Clan +winner: polyteam version 2 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d40f17a47ad353bba401a9d21c7eb6.bc21 + +Egg Clan -vs- polyteam version 2 +winner: polyteam version 2 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8d5593694ba9884d47fc96325125c1.bc21 + + + +boib -vs- The Al Gore Rhythm +winner: The Al Gore Rhythm +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/97220f5513b6996942db248eaccfbb.bc21 + +The Al Gore Rhythm -vs- boib +winner: The Al Gore Rhythm +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/94bea2ecb76d83a76fc9b21fc0d836.bc21 + +boib -vs- The Al Gore Rhythm +winner: The Al Gore Rhythm +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/39679aa7625afa87cb7cdaee3b9342.bc21 + + + +Alpha Centauri -vs- ButterBois +winner: Alpha Centauri +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/72b421c1f4dbd2af05348d4595a615.bc21 + +ButterBois -vs- Alpha Centauri +winner: Alpha Centauri +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/acae997799723509313823bb5f9dea.bc21 + +Alpha Centauri -vs- ButterBois +winner: Alpha Centauri +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/84f48d7045e50d9deead749215305c.bc21 + + + +Serpentine - M.A.R.S. -vs- The Lurkers in the Wire +winner: Serpentine - M.A.R.S. +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/15700ec0d11733be18687bdc5cd4c5.bc21 + +The Lurkers in the Wire -vs- Serpentine - M.A.R.S. +winner: The Lurkers in the Wire +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/07bf6b158f1ce2c7896f147908315a.bc21 + +Serpentine - M.A.R.S. -vs- The Lurkers in the Wire +winner: Serpentine - M.A.R.S. +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/cb490bf363d82307bc43dcdbbff779.bc21 + + + +Principia -vs- The Eager Sloths +winner: Principia +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/aef6a3bb5f02ccaf29e2bc7d7e38fe.bc21 + +The Eager Sloths -vs- Principia +winner: Principia +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8f8f1614bdd95426a2ec8867be7e75.bc21 + +Principia -vs- The Eager Sloths +winner: Principia +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/614d6a2fbb6b4a6b08d0c391b9dbb2.bc21 + + + +DaMa -vs- Balloon Platoon +winner: DaMa +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0a359a03d59f88b675e6f32b88ad68.bc21 + +Balloon Platoon -vs- DaMa +winner: DaMa +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1d5fe15e696f5f2acddbf316ec0383.bc21 + +DaMa -vs- Balloon Platoon +winner: DaMa +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4320fdba3df68d2420e14e18ca07ae.bc21 + + + +helloMars -vs- java :ghosthug: +winner: java :ghosthug: +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e605bab770f6965cb042cca18a7f26.bc21 + +java :ghosthug: -vs- helloMars +winner: java :ghosthug: +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/043f4b3a668e5c4a8bf657903a92b2.bc21 + +helloMars -vs- java :ghosthug: +winner: java :ghosthug: +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d080e9a958568493bcca25e34605b0.bc21 + + + +Team Confused -vs- JT5 +winner: Team Confused +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/06d31819a6d736da3417e13aa54d56.bc21 + +JT5 -vs- Team Confused +winner: Team Confused +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/249400fcd2453e46b9e4329514946a.bc21 + +Team Confused -vs- JT5 +winner: Team Confused +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f36a8e1042091208d57c0196a1d7eb.bc21 + + + +CHAD -vs- BearFish +winner: CHAD +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f76ad509fd829c08b2bdd255d1151b.bc21 + +BearFish -vs- CHAD +winner: CHAD +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e65bc4afc7c99765f5287228e71d77.bc21 + +CHAD -vs- BearFish +winner: CHAD +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5be9eb1959e242c9084527ae388c63.bc21 + + + +Python Waifu -vs- Intrepid losers +winner: Intrepid losers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/15eba1b76526770d3ece42bc74d22c.bc21 + +Intrepid losers -vs- Python Waifu +winner: Python Waifu +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1f7dd4865c7b5b4fdec48ec554b049.bc21 + +Python Waifu -vs- Intrepid losers +winner: Python Waifu +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d5894d13d35df8d3da5f64dd939ff4.bc21 + + + +JavaScrapped -vs- holy choir +winner: JavaScrapped +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/133644580e37276775d257e58ffe1b.bc21 + +holy choir -vs- JavaScrapped +winner: JavaScrapped +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/344c620f7be4783a9a48ebbb2c4343.bc21 + +JavaScrapped -vs- holy choir +winner: JavaScrapped +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e66f2dfd081b59b888863697d5ae4c.bc21 + + + +Goreteks -vs- Children of Talos +winner: Goreteks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a7f9f2b96ae6b4f143bf1baf2ec35f.bc21 + +Children of Talos -vs- Goreteks +winner: Goreteks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a71b18b7d2734207e2982b62c3ae63.bc21 + +Goreteks -vs- Children of Talos +winner: Goreteks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/fee62bb3cfc804500076024d36b9ab.bc21 + + + +InfiniteLoop -vs- Team_of_One +winner: Team_of_One +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/cf5121bd2444f8371b896bc357fe98.bc21 + +Team_of_One -vs- InfiniteLoop +winner: Team_of_One +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0e4c73fb6e87f88e516dc5f3312632.bc21 + +InfiniteLoop -vs- Team_of_One +winner: Team_of_One +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/54ed6b350d3216022c188a65110d10.bc21 + + + +cuttlefish -vs- SeizeMeansOfSoftwareProduction +winner: cuttlefish +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ec22af09313b4f9d86fbb7dc8e8949.bc21 + +SeizeMeansOfSoftwareProduction -vs- cuttlefish +winner: cuttlefish +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/3cd6426e440f3c1e30fa5175965823.bc21 + +cuttlefish -vs- SeizeMeansOfSoftwareProduction +winner: cuttlefish +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a7440d90ea252fbf63152d4df7cf32.bc21 + + + +Cavalier -vs- Rael Tarmo Tähvend +winner: Cavalier +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c42feb44629966d63ffab5bb4b5bf8.bc21 + +Rael Tarmo Tähvend -vs- Cavalier +winner: Cavalier +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4ad3edb6e651537863fa37ff76ff5a.bc21 + +Cavalier -vs- Rael Tarmo Tähvend +winner: Cavalier +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/694ff508861d7ee4cbdafcb9150800.bc21 + + + +PenguinBattler -vs- Quantum +winner: PenguinBattler +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6a3fdcef0ebbf93831daf7c76881ce.bc21 + +Quantum -vs- PenguinBattler +winner: PenguinBattler +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b076bafb5b511b13e1ab3fe3795b77.bc21 + +PenguinBattler -vs- Quantum +winner: PenguinBattler +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c7d946efa55102f11bf03c93fcbd35.bc21 + + + +ginger cow -vs- 『100910』 +winner: ginger cow +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f4184dc6dccd266dc0128512c01f6e.bc21 + +『100910』 -vs- ginger cow +winner: ginger cow +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8bce17b0d8fbb1132d150b998d079b.bc21 + +ginger cow -vs- 『100910』 +winner: ginger cow +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9be0ec55206196d2d8eb38cf1b3d79.bc21 + + + +The Matarrhites -vs- Endless Downpour +winner: The Matarrhites +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/73802e67600827838cff5f2e96a751.bc21 + +Endless Downpour -vs- The Matarrhites +winner: The Matarrhites +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/791f20a65c96cb9ecfd1898b66a5be.bc21 + +The Matarrhites -vs- Endless Downpour +winner: The Matarrhites +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c3b62e73bd50f559fa609ad5460d04.bc21 + + + +idrc -vs- CHINA +winner: idrc +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/334acf16bd8a6f8725a6cfed812aea.bc21 + +CHINA -vs- idrc +winner: idrc +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f568c1a1e4524e2cfc7155f3e7810e.bc21 + +idrc -vs- CHINA +winner: idrc +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8ecc9df8db6848621501306f37d37e.bc21 + + + +elongatedmuskrat -vs- JT6 +winner: elongatedmuskrat +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a5cc30664790d38710bdaab895e70f.bc21 + +JT6 -vs- elongatedmuskrat +winner: elongatedmuskrat +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6916a4906d5973a86524627436893b.bc21 + +elongatedmuskrat -vs- JT6 +winner: elongatedmuskrat +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8b965df23c4a7e51c5203e751b8437.bc21 + + + +Joe -vs- Handshakers x3 +winner: Joe +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/3e9259fdf6463b650d055418b28dd8.bc21 + +Handshakers x3 -vs- Joe +winner: Joe +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4c621236ce56ccdf99604b6fafa82c.bc21 + +Joe -vs- Handshakers x3 +winner: Joe +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/38a187c6afe133c453e3551df8aba6.bc21 + + + +Big Oh -vs- Oni Phantom Pog +winner: Big Oh +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4679f5e7a918bbb397d3e88be62490.bc21 + +Oni Phantom Pog -vs- Big Oh +winner: Big Oh +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/33865665ecd0b261cfde6cad6f1dde.bc21 + +Big Oh -vs- Oni Phantom Pog +winner: Big Oh +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/78a19c953507551e7e8e6cc85d1342.bc21 + + + +0x5f3759df -vs- null +winner: 0x5f3759df +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/431f6b3fa895e567c6079f16693c63.bc21 + +null -vs- 0x5f3759df +winner: 0x5f3759df +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1f26e1a8e08baa786f04ab685d1fb9.bc21 + +0x5f3759df -vs- null +winner: 0x5f3759df +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2cf63a96de6979175758a32b7ae04b.bc21 + + + +pigeons -vs- JT3 +winner: pigeons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/830d9e603f42fd6255c6c7260dc860.bc21 + +JT3 -vs- pigeons +winner: pigeons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/591d726b832e11e8ab37f1ccb339ab.bc21 + +pigeons -vs- JT3 +winner: pigeons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e33bbb95cc5c8631b7f07b4ff6dd1e.bc21 + + + +Serpentine - Viper -vs- Snakes and Ladders +winner: Serpentine - Viper +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/08c2c84b2860a34ebe8ff4f111d6da.bc21 + +Snakes and Ladders -vs- Serpentine - Viper +winner: Serpentine - Viper +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/02d006452462913bb63e4cf31774c1.bc21 + +Serpentine - Viper -vs- Snakes and Ladders +winner: Serpentine - Viper +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/58a309702b816a5422456628005b45.bc21 + + + +lit -vs- Beginner +winner: lit +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5c3d81e576a883bb02df5488c3d020.bc21 + +Beginner -vs- lit +winner: lit +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/3106f347ac9d399b3aa65504cb8041.bc21 + +lit -vs- Beginner +winner: lit +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/3536bb2a0468e1af171d3cc839386e.bc21 + + + +Scopes Monkey Trial 1925 -vs- JT2 +winner: JT2 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7c068cbc9c1903cd96ebce2709e0f4.bc21 + +JT2 -vs- Scopes Monkey Trial 1925 +winner: JT2 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/df149a4cdfb976839344039727da52.bc21 + +Scopes Monkey Trial 1925 -vs- JT2 +winner: JT2 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8aa1590b33bb38bce1cb372cba1ba9.bc21 + + + +Hippocrene -vs- JT1 +winner: Hippocrene +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e72244918702746e653f1b06ed27a0.bc21 + +JT1 -vs- Hippocrene +winner: Hippocrene +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/45403de77328e216607176aef41a15.bc21 + +Hippocrene -vs- JT1 +winner: Hippocrene +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9fda89cbd8349609a71c4bb6a1c892.bc21 + + + +flexqueue -vs- GCI Gophers +winner: GCI Gophers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4bddf643ea1491f7e709c75d5e2f86.bc21 + +GCI Gophers -vs- flexqueue +winner: GCI Gophers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/24376c7d3fd75cd91b1ba8a7d34d8c.bc21 + +flexqueue -vs- GCI Gophers +winner: flexqueue +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a8e6982b3a7d2145f24ac3042b9b8b.bc21 + + + +CamelMan -vs- 21 Codestreet +winner: 21 Codestreet +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f23e642c6807c6eb31cbc8efe29229.bc21 + +21 Codestreet -vs- CamelMan +winner: CamelMan +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d925716dbb1c3a10b71baa673fafdd.bc21 + +CamelMan -vs- 21 Codestreet +winner: 21 Codestreet +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a06a6850f8c6e7da7e77acf6ded9c6.bc21 + + + +The Method. -vs- Borscht Bot +winner: The Method. +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c468e13928e759ecf47fb2727f5b82.bc21 + +Borscht Bot -vs- The Method. +winner: The Method. +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/49f5f0dbad77aeb7a749f489376a81.bc21 + +The Method. -vs- Borscht Bot +winner: The Method. +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1a1328ddfc69f099658df5c4527031.bc21 + + + +monky -vs- Solexa +winner: monky +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6536360d3e26ecd5acf7ecb119618e.bc21 + +Solexa -vs- monky +winner: monky +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0e5a6c9b311b93fe532ae71daad6ea.bc21 + +monky -vs- Solexa +winner: monky +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f909531f95f4f060b791e74f396554.bc21 + + + +MossyBot -vs- Homeless Coders +winner: MossyBot +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/776c9f15ce34c27011a9bd88e9bf7a.bc21 + +Homeless Coders -vs- MossyBot +winner: MossyBot +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/35a6709cbc8f5d9a0bb08d2966a08f.bc21 + +MossyBot -vs- Homeless Coders +winner: MossyBot +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b2cc581f91fa00bdf6d4c4a355ab21.bc21 + + + +GHS Guardians -vs- CMU-MIT combo plater +winner: GHS Guardians +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ba2f3c3d0ee79cc72db0169311c7b9.bc21 + +CMU-MIT combo plater -vs- GHS Guardians +winner: GHS Guardians +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/db4902e027040ee3520f8ebf308a23.bc21 + +GHS Guardians -vs- CMU-MIT combo plater +winner: GHS Guardians +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5d917f2099103efb94da89edeae26e.bc21 + + + +Yeet Pull -vs- doesthisevenwork +winner: Yeet Pull +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/fb8a724947a86601a2dba2697f42a5.bc21 + +doesthisevenwork -vs- Yeet Pull +winner: Yeet Pull +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a6cbc58472043aa82a773246f55ba6.bc21 + +Yeet Pull -vs- doesthisevenwork +winner: Yeet Pull +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/163660b764ec3bc91a924df92f71e8.bc21 + + + +Touhutippa -vs- BattlePath +winner: Touhutippa +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0a0704bd5a2476b5c3e78fe5da22f1.bc21 + +BattlePath -vs- Touhutippa +winner: Touhutippa +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/959a2950a12a2bc243c93919640e1f.bc21 + +Touhutippa -vs- BattlePath +winner: Touhutippa +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b749a600531d854251ea1552190fd5.bc21 + + + +Hertzhaft -vs- HedgeTech +winner: Hertzhaft +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4a00a2888b3dda04f32d940654b0fd.bc21 + +HedgeTech -vs- Hertzhaft +winner: Hertzhaft +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0116a4270defcebb399597456e4444.bc21 + +Hertzhaft -vs- HedgeTech +winner: Hertzhaft +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4fe3b0327a87217d6549b7c8d4e886.bc21 + + + +JT4 -vs- 69 Tons More Data +winner: JT4 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d2a57c8b3d1123de9dcaaed995d375.bc21 + +69 Tons More Data -vs- JT4 +winner: JT4 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2f51b173701ada29079b593639f21a.bc21 + +JT4 -vs- 69 Tons More Data +winner: JT4 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/20f48d123ba9ff16e39de6e2fb611e.bc21 + + + +Veto -vs- Toot Toot Train +winner: Veto +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6cc35103615949b13d295d6538d51c.bc21 + +Toot Toot Train -vs- Veto +winner: Veto +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/de158e02e7970afd222ef48c0c9ef8.bc21 + +Veto -vs- Toot Toot Train +winner: Veto +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1c800d87b731633e7333a5dd13af60.bc21 + + + +YA BOI -vs- Burnerteam; passwordisqweasdqweasd +winner: YA BOI +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9570fdc575037869917206a9f8901c.bc21 + +Burnerteam; passwordisqweasdqweasd -vs- YA BOI +winner: YA BOI +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/05dbabc18b1e1343a7e6fbe20c5290.bc21 + +YA BOI -vs- Burnerteam; passwordisqweasdqweasd +winner: YA BOI +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/01d683747a5d3bdb4399d04c93eece.bc21 + + + +blair blezers -vs- Sagittarius A* Algorithm +winner: blair blezers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ccd7ecdbd6385e19e0d4ec7970f681.bc21 + +Sagittarius A* Algorithm -vs- blair blezers +winner: blair blezers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7b089f1f5a1feb3dc83ae83a256993.bc21 + +blair blezers -vs- Sagittarius A* Algorithm +winner: blair blezers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/90e07498297484ec879db5daa0a784.bc21 + + + +ACM @ UNCC -vs- MinneCal +winner: ACM @ UNCC +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0b64bef7fad0c69144131d22b4d037.bc21 + +MinneCal -vs- ACM @ UNCC +winner: ACM @ UNCC +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/fb2fbfff1900933152be2d4f6e8db2.bc21 + +ACM @ UNCC -vs- MinneCal +winner: ACM @ UNCC +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/636680747e7aefb01c1cdd387d817e.bc21 + + + +ThotBot -vs- sinksanksunk +winner: ThotBot +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e3cc823d9c70ff143465ee035bd671.bc21 + +sinksanksunk -vs- ThotBot +winner: ThotBot +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/214dafd6a0768c208ed94e87d6b9aa.bc21 + +ThotBot -vs- sinksanksunk +winner: ThotBot +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/50fb88aa53172148db35927d617178.bc21 + + + +$nowball -vs- CodeReapers +winner: CodeReapers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c3e0cec57197f8aefaa213a9d85a11.bc21 + +CodeReapers -vs- $nowball +winner: CodeReapers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2f438063ed778e880db6b107ff5cc1.bc21 + +$nowball -vs- CodeReapers +winner: CodeReapers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/deee4137b52a975df508c86d36b403.bc21 + + + +The 501st -vs- The other team +winner: The other team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/42e442e1448cf4aa5e53f7db865afd.bc21 + +The other team -vs- The 501st +winner: The other team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ebdb09be67446b0bb7eea9157c9bb2.bc21 + +The 501st -vs- The other team +winner: The other team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/386621ef51c53942f6e10cb6a076a5.bc21 + + + +01001000 -vs- Team Awesome +winner: Team Awesome +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/fe2c45b5e219d9d81d395468a33fcd.bc21 + +Team Awesome -vs- 01001000 +winner: Team Awesome +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b418fbad77f19aaa098b25df071405.bc21 + +01001000 -vs- Team Awesome +winner: Team Awesome +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6fd80b71700f9c9cf6b60cf7822962.bc21 + + + +Mycroft -vs- BossTweed +winner: BossTweed +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b81e38dd03f67093d90de819f1903d.bc21 + +BossTweed -vs- Mycroft +winner: BossTweed +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4f906b049de725d40775b66a2da48a.bc21 + +Mycroft -vs- BossTweed +winner: BossTweed +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/79d986e165852df617f78686668dc3.bc21 + + + +Ctrl Alt Elite -vs- fishy yum +winner: Ctrl Alt Elite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/bd32cd7262ccb19ae41159b677a05e.bc21 + +fishy yum -vs- Ctrl Alt Elite +winner: Ctrl Alt Elite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/96940008a63a9dad0e42a24b4601e0.bc21 + +Ctrl Alt Elite -vs- fishy yum +winner: fishy yum +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c4964c24ba9ca0bd16c8990550e568.bc21 + + + +Broccoli Bros ! -vs- ERO2 +winner: Broccoli Bros ! +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5c19756616ac46ff09571eae12ebcd.bc21 + +ERO2 -vs- Broccoli Bros ! +winner: Broccoli Bros ! +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6fb58bc654adba091fbb789a096cce.bc21 + +Broccoli Bros ! -vs- ERO2 +winner: Broccoli Bros ! +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4b7b40b66629535ca0f91a19ffa24f.bc21 + + + +Kansas City Asians -vs- Stepstool +winner: Kansas City Asians +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8136c42139f66736f42e99fbb84906.bc21 + +Stepstool -vs- Kansas City Asians +winner: Kansas City Asians +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/dbcae8fe2e552cc67caaba2d6bf600.bc21 + +Kansas City Asians -vs- Stepstool +winner: Kansas City Asians +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0f9cbd5012d8c3ad24d94dbbc93569.bc21 + + + +No Battlecode for Old Men -vs- Influencer Force +winner: No Battlecode for Old Men +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/645881a0279c6fe1c69243e36ffe8b.bc21 + +Influencer Force -vs- No Battlecode for Old Men +winner: Influencer Force +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c3a7b60de622c863949b90e2db9cb6.bc21 + +No Battlecode for Old Men -vs- Influencer Force +winner: No Battlecode for Old Men +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9166acd56b0730c7bb8ff6ba536503.bc21 + + + +armed pythons -vs- A214 +winner: A214 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/fbfaef7f645e0acd7515948046cdb4.bc21 + +A214 -vs- armed pythons +winner: A214 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f5abcc164560e96a5adf17a4cce895.bc21 + +armed pythons -vs- A214 +winner: A214 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e6186fc321bb6f32c5953e0702fcbb.bc21 + + + +confused -vs- Soju +winner: Soju +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8c00a45b5a7d96147bba18bee8f762.bc21 + +Soju -vs- confused +winner: confused +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/03563d74e4037fee34ba4e98bed714.bc21 + +confused -vs- Soju +winner: confused +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4b21deedf40c86b69354d5b26eeb93.bc21 + + + +(+[](){})(); -vs- ﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽ +winner: ﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽ +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2068a93dc656b294485c4c22abb70f.bc21 + +﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽ -vs- (+[](){})(); +winner: (+[](){})(); +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a2ada8d11ba14e87d385b4e8cecdda.bc21 + +(+[](){})(); -vs- ﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽ +winner: (+[](){})(); +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f4e14926c590f06402d25f13ec0a24.bc21 + + + +Ctrl Alt Defeat -vs- The Unladen Swallows +winner: The Unladen Swallows +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6449dca66194609d09217128fcd074.bc21 + +The Unladen Swallows -vs- Ctrl Alt Defeat +winner: The Unladen Swallows +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/dc3f6abdf172cc4d4c657e354efaa8.bc21 + +Ctrl Alt Defeat -vs- The Unladen Swallows +winner: The Unladen Swallows +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a98e4933f5b9e8583389dfa1fe0745.bc21 + + + +bombocombo -vs- free boba +winner: free boba +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/3f2cee7472eba4cb293af956659f41.bc21 + +free boba -vs- bombocombo +winner: free boba +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8c892fda4686017888677e581e52df.bc21 + +bombocombo -vs- free boba +winner: free boba +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/bc6b33d25053ef96391eacda8e61ff.bc21 + + + +random -vs- What are you doing stepBot? +winner: What are you doing stepBot? +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/074b64ee6dfeb63e9e096c1f006920.bc21 + +What are you doing stepBot? -vs- random +winner: What are you doing stepBot? +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e38dff6fce4d940ca5feac669af2fd.bc21 + +random -vs- What are you doing stepBot? +winner: random +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5ee44cfad76fa4a85a9e58644b678b.bc21 + + + +Team Nit(h)ya -vs- Coconut9 +winner: Coconut9 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a49f951d50985f22c4940e579d302d.bc21 + +Coconut9 -vs- Team Nit(h)ya +winner: Coconut9 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9a0e23feebb68f8e6feb015059db4d.bc21 + +Team Nit(h)ya -vs- Coconut9 +winner: Coconut9 +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/14105d0d074b2f6f08f7b608fedbc0.bc21 + + + +Daedalus -vs- 2b1y +winner: Daedalus +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c6740d19fa6ecef91bd8eb6aeb67c6.bc21 + +2b1y -vs- Daedalus +winner: Daedalus +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1b6ac77b2d25756f82273913af3186.bc21 + +Daedalus -vs- 2b1y +winner: Daedalus +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c2c11e41508ee7f222167783048a15.bc21 + + + +Double J -vs- 0K +winner: 0K +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/116cc9dba03e45c6de79eb8641d650.bc21 + +0K -vs- Double J +winner: 0K +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/63959603d6f1fb0f12ff2c3fa5f0bc.bc21 + +Double J -vs- 0K +winner: 0K +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/517e450bb135509636b531a5cdb93a.bc21 + + + +The Night's Watch -vs- Engineer Gaming +winner: Engineer Gaming +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f4aec9a65586bcd3617adbb47bbc0d.bc21 + +Engineer Gaming -vs- The Night's Watch +winner: Engineer Gaming +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/78592de32d3a4d2d1f2f0a9e4a46ca.bc21 + +The Night's Watch -vs- Engineer Gaming +winner: Engineer Gaming +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/614d175ae3cb07f46794036cc1f212.bc21 + + + +Propaganda Machine -vs- Malott Fat Cats +winner: Propaganda Machine +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6d27815dda691ff55633baabd06af9.bc21 + +Malott Fat Cats -vs- Propaganda Machine +winner: Malott Fat Cats +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7434a4f26519f39c1011fdcef5aca9.bc21 + +Propaganda Machine -vs- Malott Fat Cats +winner: Malott Fat Cats +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/271b8ff119b494b87d04208766fb1f.bc21 + + + +babyducks -vs- NeoHazard +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0f33d7d04d019d4d218b2da285cb29.bc21 + +NeoHazard -vs- babyducks +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/27da4012e3d9a8aaf545b065b5991b.bc21 + +babyducks -vs- NeoHazard +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7788274440de7d5ccbf18a29c15ef3.bc21 + + + +Yeicor -vs- BeachBANDITS +winner: Yeicor +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/32d8a111e8043d82094c9cc3eedc8a.bc21 + +BeachBANDITS -vs- Yeicor +winner: Yeicor +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/75cb625710f02b250af5149dc9754a.bc21 + +Yeicor -vs- BeachBANDITS +winner: Yeicor +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7b4e212e28e72e4a3c032e202b7c49.bc21 + + + +Coast -vs- polyteam version 2 +winner: Coast +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9ecd0aa1ac8329009979c84912d968.bc21 + +polyteam version 2 -vs- Coast +winner: Coast +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ee80b1f43ba18c35358fcfdb9a1cda.bc21 + +Coast -vs- polyteam version 2 +winner: Coast +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/83eabb10b388cbab0ca998665df8e6.bc21 + + + +Mars Analytica -vs- The Al Gore Rhythm +winner: The Al Gore Rhythm +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e61e9579178cd429ede4352b29c36c.bc21 + +The Al Gore Rhythm -vs- Mars Analytica +winner: Mars Analytica +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/16a3660d18ab528e7e8eea3f59d697.bc21 + +Mars Analytica -vs- The Al Gore Rhythm +winner: Mars Analytica +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5a496dfc61a2163f2d74b03352d718.bc21 + + + +Bytecode Mafia -vs- Alpha Centauri +winner: Bytecode Mafia +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/307734dbd0b6e06ecefe3dc395e45e.bc21 + +Alpha Centauri -vs- Bytecode Mafia +winner: Bytecode Mafia +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f84067d9a74f575ff2c7bf2d5346da.bc21 + +Bytecode Mafia -vs- Alpha Centauri +winner: Bytecode Mafia +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/96526c1e63b5122590c09590f34fa4.bc21 + + + +Chicken -vs- Serpentine - M.A.R.S. +winner: Chicken +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c29b11d5cbbba7d8d8fd75a93fdd0f.bc21 + +Serpentine - M.A.R.S. -vs- Chicken +winner: Chicken +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1d209ea5b84f486459b0101a9073d1.bc21 + +Chicken -vs- Serpentine - M.A.R.S. +winner: Chicken +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ffe26bf366ad1dba4f2ccfb2fe6107.bc21 + + + +I am the Senate -vs- Principia +winner: I am the Senate +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2e6f97882619fd4a86cdf98283d8f6.bc21 + +Principia -vs- I am the Senate +winner: I am the Senate +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/71f0bf69faad0b732ea98e221deb77.bc21 + +I am the Senate -vs- Principia +winner: I am the Senate +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/24be687d32b2cca6fbbe0d271f9504.bc21 + + + +Harvard sucks lol -vs- DaMa +winner: DaMa +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/416ee25ba138294f3e39102675d8fd.bc21 + +DaMa -vs- Harvard sucks lol +winner: DaMa +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/777a676fde65f7e889af02b38d4031.bc21 + +Harvard sucks lol -vs- DaMa +winner: DaMa +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1de16129f02884d8053391c5280bc3.bc21 + + + +Chocolate Banana Cake -vs- java :ghosthug: +winner: Chocolate Banana Cake +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/29f0e79bc2d94a2360b3c63a016f2c.bc21 + +java :ghosthug: -vs- Chocolate Banana Cake +winner: Chocolate Banana Cake +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/35ad8626b7d07b977bf292f36e5164.bc21 + +Chocolate Banana Cake -vs- java :ghosthug: +winner: Chocolate Banana Cake +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4ad35e68b0aa5f83d428d9a6339535.bc21 + + + +Blue Dragon -vs- Team Confused +winner: Blue Dragon +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/03b597f6c23e84503e71518887f245.bc21 + +Team Confused -vs- Blue Dragon +winner: Blue Dragon +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/039015c190e0805eeef1c5d1b76ee5.bc21 + +Blue Dragon -vs- Team Confused +winner: Blue Dragon +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/57cc46830b05c07d2cbdfa62c55734.bc21 + + + +Team Barcode -vs- CHAD +winner: Team Barcode +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0f32bc09300ecaaf7f4d0d41370a5f.bc21 + +CHAD -vs- Team Barcode +winner: Team Barcode +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8237bf343be8177662558a65c5cb80.bc21 + +Team Barcode -vs- CHAD +winner: Team Barcode +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/42416a16f0e87df945c817a5523c71.bc21 + + + +waffle -vs- Python Waifu +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d087d00b68feb538967ba11870740b.bc21 + +Python Waifu -vs- waffle +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/967a7f18fe2ad29104ea6c8636851a.bc21 + +waffle -vs- Python Waifu +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ce522597a83fea6636b0b92d3cc958.bc21 + + + +AntiVaxxKids -vs- JavaScrapped +winner: AntiVaxxKids +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4b00bf03ee56786fe6a6e3e64e7983.bc21 + +JavaScrapped -vs- AntiVaxxKids +winner: AntiVaxxKids +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d3e7111b3185d1cb8b1a26652bb085.bc21 + +AntiVaxxKids -vs- JavaScrapped +winner: AntiVaxxKids +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/449e83150fc9afdfb2ae2ec99a188a.bc21 + + + +Galaga -vs- Goreteks +winner: Galaga +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e247987e2e5d1ab8b09291ed9f0ef7.bc21 + +Goreteks -vs- Galaga +winner: Galaga +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a4b524da39905cd6ed78134b9021e3.bc21 + +Galaga -vs- Goreteks +winner: Galaga +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0525e63be7728b445b3d8dc7429d30.bc21 + + + +wololo -vs- Team_of_One +winner: wololo +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0458af6b8544607c4241374d63a5c8.bc21 + +Team_of_One -vs- wololo +winner: wololo +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9f6757f94a8dbff9c44364f4368cf6.bc21 + +wololo -vs- Team_of_One +winner: wololo +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/256d1d68b9d9e58adfd9bcbc783493.bc21 + + + +Large Green Ogres -vs- cuttlefish +winner: cuttlefish +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c7849a5a1de309b86fd28869309cbb.bc21 + +cuttlefish -vs- Large Green Ogres +winner: cuttlefish +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/490e5deb12c5f591db3e90cd6d398e.bc21 + +Large Green Ogres -vs- cuttlefish +winner: Large Green Ogres +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6494b0d331125a576dd09584f17111.bc21 + + + +Lee's Morons -vs- Cavalier +winner: Lee's Morons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/22ee94b1373bd6ce14d843c846e1c2.bc21 + +Cavalier -vs- Lee's Morons +winner: Lee's Morons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7e2c254080290c49db26463fef4095.bc21 + +Lee's Morons -vs- Cavalier +winner: Lee's Morons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2329795beeddf1b43cbbff3fa1e8a7.bc21 + + + +Hard Coders -vs- PenguinBattler +winner: Hard Coders +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ea15f528a468e8357a04cc619e819a.bc21 + +PenguinBattler -vs- Hard Coders +winner: Hard Coders +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/06a61ea1902ffbec2705e5056bc8b9.bc21 + +Hard Coders -vs- PenguinBattler +winner: Hard Coders +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0695ae5196c012b3035642daa4ae44.bc21 + + + +Kryptonite -vs- ginger cow +winner: Kryptonite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/dbb525a56ae45baaa788e6577cc67d.bc21 + +ginger cow -vs- Kryptonite +winner: Kryptonite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2ebf109f32a9e9eeaba10a6d292ed6.bc21 + +Kryptonite -vs- ginger cow +winner: Kryptonite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7c914ffce99c482dd1480b37bedf39.bc21 + + + +Pain -vs- The Matarrhites +winner: Pain +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/3ef4fb69b2d51b4c9ce77258c3efad.bc21 + +The Matarrhites -vs- Pain +winner: Pain +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/46a88213bf3aa44d0f4258c06feb8b.bc21 + +Pain -vs- The Matarrhites +winner: Pain +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/92a1aaf461b5f2bbe2f2cc7af953a2.bc21 + + + +camel_case -vs- idrc +winner: camel_case +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/73db97de6e73a7fc61087f5ec61cbb.bc21 + +idrc -vs- camel_case +winner: camel_case +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/43883745d469ff486d2f575fd3c96f.bc21 + +camel_case -vs- idrc +winner: camel_case +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/26f2bcdde696a794d3964b9d2b3611.bc21 + + + +tooOldForThis -vs- elongatedmuskrat +winner: tooOldForThis +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b05cd83540165b8bdcde80f97390f4.bc21 + +elongatedmuskrat -vs- tooOldForThis +winner: tooOldForThis +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5cf653a898809519c4f0a30a881c6b.bc21 + +tooOldForThis -vs- elongatedmuskrat +winner: tooOldForThis +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/39b7df9bf5585f1318154efc995683.bc21 + + + +Oculus -vs- Joe +winner: Oculus +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1d97848bb64bfbe2ad25822a76c9dd.bc21 + +Joe -vs- Oculus +winner: Oculus +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/80e7ea29149becc56c955d09db9ac4.bc21 + +Oculus -vs- Joe +winner: Oculus +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/466f843583fa8a50ba8a23e72355ea.bc21 + + + +Super Cow Powers -vs- Big Oh +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c0762c9c1c2d5dadbb05ba8d00ebe6.bc21 + +Big Oh -vs- Super Cow Powers +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7923e022799ff2c5c31c9ff923ff7b.bc21 + +Super Cow Powers -vs- Big Oh +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/fac2fc9af5becebfb87d3b78a1e110.bc21 + + + +Play C. Holder -vs- 0x5f3759df +winner: Play C. Holder +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f1255f766948fb1654ed70777e85f4.bc21 + +0x5f3759df -vs- Play C. Holder +winner: Play C. Holder +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1c171b36a23508fdeb4aba824bbe28.bc21 + +Play C. Holder -vs- 0x5f3759df +winner: Play C. Holder +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c9f258086316d119400fb575ef49c8.bc21 + + + +Kruskal's Kompadres -vs- pigeons +winner: Kruskal's Kompadres +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f5dc451face0b7ac40e56d42d5f0ac.bc21 + +pigeons -vs- Kruskal's Kompadres +winner: pigeons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a296a9498e24d6186de67f3a27330c.bc21 + +Kruskal's Kompadres -vs- pigeons +winner: pigeons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6eaae0428a6c2d93fb632bc2ad6bcb.bc21 + + + +Rua!! -vs- Serpentine - Viper +winner: Rua!! +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ec538ce61a99d271cdf87b69b002ed.bc21 + +Serpentine - Viper -vs- Rua!! +winner: Rua!! +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f7caaf59d1db8840df5d821e26ac05.bc21 + +Rua!! -vs- Serpentine - Viper +winner: Rua!! +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/294ebfa740bade85b2ecd0cc012afd.bc21 + + + +Huge L Club -vs- lit +winner: Huge L Club +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/adc6225d057979c27110e27a0dfb35.bc21 + +lit -vs- Huge L Club +winner: Huge L Club +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/dbd9e6c163408e24e94ba7585a57a9.bc21 + +Huge L Club -vs- lit +winner: Huge L Club +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/33317515569db77be0afca4a755bd7.bc21 + + + +3 Musketeers -vs- JT2 +winner: 3 Musketeers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9a971f49c324d0f48b2f9b544c7314.bc21 + +JT2 -vs- 3 Musketeers +winner: 3 Musketeers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5e566698da6a1c7fae477f120d8d26.bc21 + +3 Musketeers -vs- JT2 +winner: 3 Musketeers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8573aa43017bab5255ab6fc4836b27.bc21 + + + +StepZero -vs- Hippocrene +winner: StepZero +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4a3fba97253353bb8172a3e51f5848.bc21 + +Hippocrene -vs- StepZero +winner: StepZero +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9c67c84c33d290313554928e073987.bc21 + +StepZero -vs- Hippocrene +winner: StepZero +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/95d3cf81a53b49877536ec57769fea.bc21 + + + +bumbum -vs- GCI Gophers +winner: bumbum +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/45ab1b7f413abab8474ed8327729c9.bc21 + +GCI Gophers -vs- bumbum +winner: bumbum +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a29dc8b30f2d2f61ee0167087bfada.bc21 + +bumbum -vs- GCI Gophers +winner: bumbum +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b425d32f62f730fbdff96a77923f74.bc21 + + + +Code Not Found -vs- 21 Codestreet +winner: Code Not Found +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/991c193fb0e17995ab811e00be92a9.bc21 + +21 Codestreet -vs- Code Not Found +winner: Code Not Found +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2205ff2a7b0d3a679a0db87992cbb8.bc21 + +Code Not Found -vs- 21 Codestreet +winner: Code Not Found +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5edaf58bddde142a3e7689eb86e6ed.bc21 + + + +Atom -vs- The Method. +winner: Atom +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/bc56b9f63ee01b82937ef958008686.bc21 + +The Method. -vs- Atom +winner: The Method. +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/3442b12fa3a2f548112aa9784e2624.bc21 + +Atom -vs- The Method. +winner: The Method. +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/487495076dcdee35da24aaa8950933.bc21 + + + +Blue Steel -vs- monky +winner: Blue Steel +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6c0244b72f8ae78003b5cf2e28b9e3.bc21 + +monky -vs- Blue Steel +winner: Blue Steel +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a2791f067d4ca42f33d98dbbca66dc.bc21 + +Blue Steel -vs- monky +winner: Blue Steel +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f1ac64d530bfe797982fa0f54c24ca.bc21 + + + +Nikola -vs- MossyBot +winner: Nikola +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/87b2025df45d424cdb59f6cd8f6019.bc21 + +MossyBot -vs- Nikola +winner: Nikola +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4f75fcb298bfafd0e376125ce89d8a.bc21 + +Nikola -vs- MossyBot +winner: Nikola +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/52baf77b8628c9338922685e8807b3.bc21 + + + +Download More RAM -vs- GHS Guardians +winner: Download More RAM +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/968b71a31e906f1253410857af8a17.bc21 + +GHS Guardians -vs- Download More RAM +winner: Download More RAM +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e35ce37458d31fb33c345394f445bd.bc21 + +Download More RAM -vs- GHS Guardians +winner: Download More RAM +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1cbc93906bcf3bea8b22ff47646f8e.bc21 + + + +The Patriots -vs- Yeet Pull +winner: The Patriots +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9e0e2d5c724ab07ebe5852f87b807e.bc21 + +Yeet Pull -vs- The Patriots +winner: The Patriots +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/eaa1b1d3256f818d7437ce4459a9d0.bc21 + +The Patriots -vs- Yeet Pull +winner: The Patriots +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/110b65efa6ec49eac5ce929e342271.bc21 + + + +Dis Team -vs- Touhutippa +winner: Dis Team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ff8c45ebc302890f63d99f85a6e98b.bc21 + +Touhutippa -vs- Dis Team +winner: Dis Team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a9d9e4b72421042190f53422150934.bc21 + +Dis Team -vs- Touhutippa +winner: Dis Team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1067430dca5c80c7d18565b2f6c744.bc21 + + + +Ripples -vs- Hertzhaft +winner: Ripples +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/489deb8b5f6d0980a2d1491f0aed7f.bc21 + +Hertzhaft -vs- Ripples +winner: Ripples +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8ba1d5eb3a4b389b4165ed1a752834.bc21 + +Ripples -vs- Hertzhaft +winner: Ripples +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a613330f989aff5770f61485ce2a42.bc21 + + + +Producing Perfection -vs- JT4 +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/cbb9bead7441fd8e700dce34e97b4c.bc21 + +JT4 -vs- Producing Perfection +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/fff02b8884d659ae8cf9b664a35185.bc21 + +Producing Perfection -vs- JT4 +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f0442566aafc066369d18221d56d04.bc21 + + + +sucks2BU -vs- Veto +winner: Veto +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a588c70e14513a8890444efa5d1adf.bc21 + +Veto -vs- sucks2BU +winner: Veto +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/dd2ae34510601d95692eb034731457.bc21 + +sucks2BU -vs- Veto +winner: Veto +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/67746ea0b9c65a561fecc3319f7aaf.bc21 + + + +Up For A While -vs- YA BOI +winner: YA BOI +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8646ed68984912d6b16f3933386193.bc21 + +YA BOI -vs- Up For A While +winner: Up For A While +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/59c6639e65dba079d01a3ac4dd32dc.bc21 + +Up For A While -vs- YA BOI +winner: Up For A While +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ea6c8b83abb9410b173ebf5b6253b2.bc21 + + + +Chop Suey -vs- blair blezers +winner: blair blezers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8b331b688ffbc4d1d12e8217cdf9f9.bc21 + +blair blezers -vs- Chop Suey +winner: blair blezers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/92d71188f9f4d220b46306fe0b7111.bc21 + +Chop Suey -vs- blair blezers +winner: blair blezers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/387d678f95db6a64fcca62c21ed018.bc21 + + + +remotED -vs- ACM @ UNCC +winner: remotED +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/94232d3c837dc3bbd92f61a3a7db9d.bc21 + +ACM @ UNCC -vs- remotED +winner: ACM @ UNCC +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1aead85d9ffdcf0fc149d9881e9a18.bc21 + +remotED -vs- ACM @ UNCC +winner: remotED +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/df9069f32a7676188be8de2700aa15.bc21 + + + +BASHA ESPORTS -vs- ThotBot +winner: ThotBot +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7339e5ee4c83a94f0b3befa176b972.bc21 + +ThotBot -vs- BASHA ESPORTS +winner: ThotBot +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/3341c64ee7f85d9c24427cb9a720d4.bc21 + +BASHA ESPORTS -vs- ThotBot +winner: ThotBot +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b0026f8ab7c9b0032cf4673ec6a45c.bc21 + + + +babyducks -vs- CodeReapers +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0c909ec529025cf56129afc5a97704.bc21 + +CodeReapers -vs- babyducks +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/af579089839d857ed5bd9812798d7e.bc21 + +babyducks -vs- CodeReapers +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ea0b6aa0bea72fbb50aed527ac939d.bc21 + + + +Yeicor -vs- Coast +winner: Coast +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/579aed9c6405b48779f9724812ca79.bc21 + +Coast -vs- Yeicor +winner: Coast +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5b0550d88083c0bd7562ad6b51eb4d.bc21 + +Yeicor -vs- Coast +winner: Coast +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/edc04146ce6bfaf3312facc53b1530.bc21 + + + +Mars Analytica -vs- The other team +winner: Mars Analytica +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8d22eaeea375d863bb5a1aa8acfe58.bc21 + +The other team -vs- Mars Analytica +winner: Mars Analytica +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c25e7db3e99fddebbce3ccdb2597ea.bc21 + +Mars Analytica -vs- The other team +winner: Mars Analytica +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f348ac48ab27db746dc4c0d02c2c0d.bc21 + + + +Bytecode Mafia -vs- Team Awesome +winner: Bytecode Mafia +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b5e42bf2871773d03f3f35d7e00962.bc21 + +Team Awesome -vs- Bytecode Mafia +winner: Bytecode Mafia +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6ad869ce21450484d9246ed65f1c33.bc21 + +Bytecode Mafia -vs- Team Awesome +winner: Bytecode Mafia +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/511c8b30e8191e0f4a48f349ad710c.bc21 + + + +Chicken -vs- BossTweed +winner: Chicken +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d61b8c70cf01547635e4ae4421ff91.bc21 + +BossTweed -vs- Chicken +winner: Chicken +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/99671457e2a4147005fd0c7029020a.bc21 + +Chicken -vs- BossTweed +winner: Chicken +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/aaf6abcb3cc1026aec0b125b0696fd.bc21 + + + +I am the Senate -vs- DaMa +winner: DaMa +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/46811ca18ad261dbd3765ebc172e74.bc21 + +DaMa -vs- I am the Senate +winner: DaMa +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/eb39f0a492302f7889f1ce60dfa9a7.bc21 + +I am the Senate -vs- DaMa +winner: I am the Senate +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e2f7a56e0f74a9152a6b0a0e67b95a.bc21 + + + +Chocolate Banana Cake -vs- Ctrl Alt Elite +winner: Chocolate Banana Cake +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d3ad7c1e2732ea109185f7c93937f5.bc21 + +Ctrl Alt Elite -vs- Chocolate Banana Cake +winner: Chocolate Banana Cake +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8577d724ad1e3c71e1d96005f71193.bc21 + +Chocolate Banana Cake -vs- Ctrl Alt Elite +winner: Chocolate Banana Cake +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/589b679732c3e9780bc7d9dcc21809.bc21 + + + +Blue Dragon -vs- Team Barcode +winner: Team Barcode +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0e6dc32a1d83a9a9a57d6199c8dd22.bc21 + +Team Barcode -vs- Blue Dragon +winner: Blue Dragon +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6f2a60bdfdecda7e0559614b72a17a.bc21 + +Blue Dragon -vs- Team Barcode +winner: Team Barcode +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a9d3308e2f907614b2ad20a58fb710.bc21 + + + +waffle -vs- Broccoli Bros ! +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/dd079c9ede562d37ee90a6bf408cb2.bc21 + +Broccoli Bros ! -vs- waffle +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/bf7cae120ca34d62f2860eb6306145.bc21 + +waffle -vs- Broccoli Bros ! +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b0d66f44b26909e3a584a18d479430.bc21 + + + +AntiVaxxKids -vs- Galaga +winner: AntiVaxxKids +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5497eee5b0a7c3089f4193b60da310.bc21 + +Galaga -vs- AntiVaxxKids +winner: AntiVaxxKids +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/555fe97ef0876403d5d392b410fdea.bc21 + +AntiVaxxKids -vs- Galaga +winner: AntiVaxxKids +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/10f93ad5bf84f6a6b303bbae563b71.bc21 + + + +wololo -vs- Kansas City Asians +winner: wololo +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/41ab25579262c9525af9f943ec4430.bc21 + +Kansas City Asians -vs- wololo +winner: Kansas City Asians +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/886ae0d7b32a8adf5da8c7fec8f0b0.bc21 + +wololo -vs- Kansas City Asians +winner: Kansas City Asians +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/10a04f7b8112679b0fc8036c3c39d9.bc21 + + + +cuttlefish -vs- Lee's Morons +winner: Lee's Morons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7eee15b21046352842095471270797.bc21 + +Lee's Morons -vs- cuttlefish +winner: Lee's Morons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/02508f42909f8dc6aa7720c5161e74.bc21 + +cuttlefish -vs- Lee's Morons +winner: Lee's Morons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/62341af9fc6c945ceb19b31a7c4aea.bc21 + + + +Hard Coders -vs- No Battlecode for Old Men +winner: Hard Coders +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/bc587c7387d0ba2e9396f4c8a61edb.bc21 + +No Battlecode for Old Men -vs- Hard Coders +winner: Hard Coders +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/cb344935b2827c904f699db665560b.bc21 + +Hard Coders -vs- No Battlecode for Old Men +winner: Hard Coders +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/03d0933e37b1fe40ebc64090c3337e.bc21 + + + +Kryptonite -vs- Pain +winner: Kryptonite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d1f030ae4709db43c69169c08aed82.bc21 + +Pain -vs- Kryptonite +winner: Kryptonite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/71e26937cfeefeac42206419247135.bc21 + +Kryptonite -vs- Pain +winner: Kryptonite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/cce94659d1c846222ed252782535ba.bc21 + + + +camel_case -vs- A214 +winner: camel_case +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/501bcd34ba81394dcc2f31db3d75fb.bc21 + +A214 -vs- camel_case +winner: camel_case +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/fac9ae25801e9eb5cc15b9bb7515b8.bc21 + +camel_case -vs- A214 +winner: camel_case +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c3a3b715ebc5fb7e1ccbb589a8a657.bc21 + + + +tooOldForThis -vs- Oculus +winner: tooOldForThis +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7a61675ed375b21ff64153815412dd.bc21 + +Oculus -vs- tooOldForThis +winner: tooOldForThis +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/db1fc171a8ac2fb3cd2a3bc3d14452.bc21 + +tooOldForThis -vs- Oculus +winner: tooOldForThis +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b43fcbfda963e8d58b6986c78924b9.bc21 + + + +Super Cow Powers -vs- confused +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f4dfbe7b70f265de7f0101c03b1990.bc21 + +confused -vs- Super Cow Powers +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a1dee719fc137c6fa196a403850756.bc21 + +Super Cow Powers -vs- confused +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/36f632b0c830d601c5a4d34b6dee2d.bc21 + + + +Play C. Holder -vs- pigeons +winner: pigeons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/05348b0c9a064578dbab2eee9ef025.bc21 + +pigeons -vs- Play C. Holder +winner: pigeons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9d45571b95855e172323427c43be6a.bc21 + +Play C. Holder -vs- pigeons +winner: Play C. Holder +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1e286a11d4b1e8e951a08f76387c8f.bc21 + + + +Rua!! -vs- (+[](){})(); +winner: (+[](){})(); +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2a60a3d21b5a6e9ffd64cf0d7721e4.bc21 + +(+[](){})(); -vs- Rua!! +winner: Rua!! +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/bfb75930648f831d4cbd497bb841e6.bc21 + +Rua!! -vs- (+[](){})(); +winner: Rua!! +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/67af049be381aaf177f3842d4287bc.bc21 + + + +Huge L Club -vs- The Unladen Swallows +winner: Huge L Club +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0c21755bfbcfb2f14ca81d726ee5bc.bc21 + +The Unladen Swallows -vs- Huge L Club +winner: Huge L Club +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d56636c1416648f6f2a74361cf8b7e.bc21 + +Huge L Club -vs- The Unladen Swallows +winner: Huge L Club +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/08a5a2dc5def0b568a4311c8d1dab6.bc21 + + + +3 Musketeers -vs- free boba +winner: 3 Musketeers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5265ad079c79e087c1ccefce01f804.bc21 + +free boba -vs- 3 Musketeers +winner: 3 Musketeers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5be1f2d113369694f59ce6a4ebf7fd.bc21 + +3 Musketeers -vs- free boba +winner: 3 Musketeers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6990108583403fc6e2be9f6e588b93.bc21 + + + +StepZero -vs- bumbum +winner: StepZero +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/022241fceaf7ca9a000ee93145513e.bc21 + +bumbum -vs- StepZero +winner: StepZero +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/17fcbe03de01b9cfb52768aa79b11e.bc21 + +StepZero -vs- bumbum +winner: StepZero +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8419ea381dfb3a6dda24c5a1e4e07f.bc21 + + + +Code Not Found -vs- What are you doing stepBot? +winner: What are you doing stepBot? +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/21385bdaf9eccd695baf0052f583cc.bc21 + +What are you doing stepBot? -vs- Code Not Found +winner: Code Not Found +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/33fa77d9b3247c641026ad99f8877f.bc21 + +Code Not Found -vs- What are you doing stepBot? +winner: Code Not Found +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6434ed05329392870fb2a67790b2c1.bc21 + + + +The Method. -vs- Blue Steel +winner: Blue Steel +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/10bde96addde86524368cb7a59d9f5.bc21 + +Blue Steel -vs- The Method. +winner: Blue Steel +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/60b03467942cf1b398542710891225.bc21 + +The Method. -vs- Blue Steel +winner: Blue Steel +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8b83daa01ee17d0deabeddd71190c2.bc21 + + + +Nikola -vs- Coconut9 +winner: Nikola +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/851a0314b23b17be31d6003fad8444.bc21 + +Coconut9 -vs- Nikola +winner: Nikola +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/57cadbea3910197b65569ca7675409.bc21 + +Nikola -vs- Coconut9 +winner: Nikola +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/58d8561109d039081848af3340d4b3.bc21 + + + +Download More RAM -vs- The Patriots +winner: Download More RAM +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ed02197927b849ebfa6dec7063ec6b.bc21 + +The Patriots -vs- Download More RAM +winner: Download More RAM +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/88a9f8a4710f72f0fa0fd48ffdd7df.bc21 + +Download More RAM -vs- The Patriots +winner: Download More RAM +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c681104115288afea779d1e44695d6.bc21 + + + +Dis Team -vs- Daedalus +winner: Dis Team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f71658ce0e3c25536a8330cbfbf4a5.bc21 + +Daedalus -vs- Dis Team +winner: Dis Team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f2fe71704b50aaf1786f3caae19674.bc21 + +Dis Team -vs- Daedalus +winner: Dis Team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d62e1dfa42f9e7922cd585f5986132.bc21 + + + +Ripples -vs- 0K +winner: Ripples +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4c458b0a626dc9c1633428bb9a2634.bc21 + +0K -vs- Ripples +winner: Ripples +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7e6c284c74b9edae73c9f27991dd99.bc21 + +Ripples -vs- 0K +winner: 0K +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/dd3a4710bed854669523d711c824ba.bc21 + + + +Producing Perfection -vs- Engineer Gaming +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/fc11a02893546ec7a021d34e18a938.bc21 + +Engineer Gaming -vs- Producing Perfection +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f8a6690c747de6fd8eda1a066e2a70.bc21 + +Producing Perfection -vs- Engineer Gaming +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ee40ca00a7e99f909ae18e0cb8f3da.bc21 + + + +Veto -vs- Up For A While +winner: Veto +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6960009ded2047c134d2c7f6eb3c0e.bc21 + +Up For A While -vs- Veto +winner: Veto +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0ba21993f235e0072dff7de1cb3bc4.bc21 + +Veto -vs- Up For A While +winner: Up For A While +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6b0705f3eaa807500961cb8fe75441.bc21 + + + +blair blezers -vs- Malott Fat Cats +winner: blair blezers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d7bbbd92f93fc6c81daf64dabc4e3c.bc21 + +Malott Fat Cats -vs- blair blezers +winner: Malott Fat Cats +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b6c25530c6e33da10af6cbf6abec99.bc21 + +blair blezers -vs- Malott Fat Cats +winner: Malott Fat Cats +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7aaaeae16853b8c4de9620dea818ba.bc21 + + + +remotED -vs- ThotBot +winner: remotED +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f2125fd661fb7edc148899baaa7959.bc21 + +ThotBot -vs- remotED +winner: ThotBot +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/caa249dbecdd4fb472dc425d7a6b6a.bc21 + +remotED -vs- ThotBot +winner: remotED +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/fdc3ccc949ea6b387c4112808f39fd.bc21 + + + +babyducks -vs- Coast +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ba31fb94bb56253f3c815f8b1410ce.bc21 + +Coast -vs- babyducks +winner: Coast +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/eebe3f1c6a4a81ab60076aeca90ecb.bc21 + +babyducks -vs- Coast +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2b57056e04e2387133b1d5ab0b0991.bc21 + + + +Mars Analytica -vs- Bytecode Mafia +winner: Mars Analytica +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5f7ec0f068ac73d9d98de4006572da.bc21 + +Bytecode Mafia -vs- Mars Analytica +winner: Mars Analytica +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c2de2b6a24976536db0cb841a290ad.bc21 + +Mars Analytica -vs- Bytecode Mafia +winner: Bytecode Mafia +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1655404cc8df0b56def13a122c75e7.bc21 + + + +Chicken -vs- DaMa +winner: Chicken +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a65d534870c5229ca6423b68c13ace.bc21 + +DaMa -vs- Chicken +winner: Chicken +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9edc7a4174040a012618d532618552.bc21 + +Chicken -vs- DaMa +winner: Chicken +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/49da8c34c2f6e2b396359615eb54da.bc21 + + + +Chocolate Banana Cake -vs- Team Barcode +winner: Team Barcode +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5eff0c192a84cb5324a84f11a828a4.bc21 + +Team Barcode -vs- Chocolate Banana Cake +winner: Team Barcode +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f942d1b9cff2687961d35406211c90.bc21 + +Chocolate Banana Cake -vs- Team Barcode +winner: Chocolate Banana Cake +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/af6385645df4b8cb59dec456b74b2e.bc21 + + + +waffle -vs- AntiVaxxKids +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8c2bcef17704f12b5f7af2ec49cfdc.bc21 + +AntiVaxxKids -vs- waffle +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b8dcdd11799979cca176e47c776dd3.bc21 + +waffle -vs- AntiVaxxKids +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/0568f56e3ad3be5c4bca31fd131011.bc21 + + + +Kansas City Asians -vs- Lee's Morons +winner: Lee's Morons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a862d702b27986f18231ca8ea95863.bc21 + +Lee's Morons -vs- Kansas City Asians +winner: Lee's Morons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ae0e28a2fb15a33468251851cd2c87.bc21 + +Kansas City Asians -vs- Lee's Morons +winner: Lee's Morons +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2f9f0ad470db3cfc81448c3d4f99be.bc21 + + + +Hard Coders -vs- Kryptonite +winner: Kryptonite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8ee187176d60359acd7dad513a74a6.bc21 + +Kryptonite -vs- Hard Coders +winner: Hard Coders +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c9da9ff91e49938df8ca76ca3bfb52.bc21 + +Hard Coders -vs- Kryptonite +winner: Kryptonite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c70146607a429ddff4dc580ac36299.bc21 + + + +camel_case -vs- tooOldForThis +winner: camel_case +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/41f40a352dc394d6b40a96948681a5.bc21 + +tooOldForThis -vs- camel_case +winner: tooOldForThis +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/00836188f372dbd03fcf25f3afd7ee.bc21 + +camel_case -vs- tooOldForThis +winner: camel_case +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/423fdb754ce3de38705328c8d187ce.bc21 + + + +Super Cow Powers -vs- pigeons +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4913473aedc877d3745d444e53267a.bc21 + +pigeons -vs- Super Cow Powers +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/41162ea1e474bfc59f832fcc190191.bc21 + +Super Cow Powers -vs- pigeons +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/449445dec8a0c29060c6affb47ba27.bc21 + + + +Rua!! -vs- Huge L Club +winner: Rua!! +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c9b7fcb86af087ee187b95dd6aa3ef.bc21 + +Huge L Club -vs- Rua!! +winner: Rua!! +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/660c6a0ab8420e9aebca44830ff0de.bc21 + +Rua!! -vs- Huge L Club +winner: Huge L Club +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/17f9401653865c6d25ab2000d80d8a.bc21 + + + +3 Musketeers -vs- StepZero +winner: 3 Musketeers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/09636d2c31d464873706a5340bc67c.bc21 + +StepZero -vs- 3 Musketeers +winner: 3 Musketeers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/82141563bb94ac70bafa34b5f25ffc.bc21 + +3 Musketeers -vs- StepZero +winner: 3 Musketeers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/74abb881f0286f8e676dbed31056a7.bc21 + + + +Code Not Found -vs- Blue Steel +winner: Code Not Found +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/bd4a65357952ec08525124763a8c26.bc21 + +Blue Steel -vs- Code Not Found +winner: Blue Steel +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9e476e2e03d8d99835e68e16ee2a92.bc21 + +Code Not Found -vs- Blue Steel +winner: Blue Steel +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/860e0cba96928b2d67eefe731a4840.bc21 + + + +Nikola -vs- Download More RAM +winner: Download More RAM +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8ca9e4c7750f9ab3a8b699ed95147c.bc21 + +Download More RAM -vs- Nikola +winner: Nikola +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/de75d7f5f873ce5b77ab4bc7e72553.bc21 + +Nikola -vs- Download More RAM +winner: Download More RAM +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/046413c1233738e9e2fd13ce29cf6c.bc21 + + + +Dis Team -vs- Ripples +winner: Dis Team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/d5028d1e70367330574ce71525cd84.bc21 + +Ripples -vs- Dis Team +winner: Dis Team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f94bcdf2f33300bc857b1be073d889.bc21 + +Dis Team -vs- Ripples +winner: Ripples +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/18af2775d6a936e3d37bbd74f71100.bc21 + + + +Producing Perfection -vs- Veto +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9eb4182fe7d871f5803b779fd236a9.bc21 + +Veto -vs- Producing Perfection +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9de7cf111587d4f0e2a3748f6c05e4.bc21 + +Producing Perfection -vs- Veto +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c1f24fdea939307c11faa7978a6f9b.bc21 + + + +Malott Fat Cats -vs- remotED +winner: remotED +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/a7ecb60bf5ee5d5d0f4a3d475ce805.bc21 + +remotED -vs- Malott Fat Cats +winner: remotED +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/3bbc75d27becb6dfc064d28dd34634.bc21 + +Malott Fat Cats -vs- remotED +winner: remotED +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/3c56ddfee960cfdb495323d6a17fd9.bc21 + + + +babyducks -vs- Mars Analytica +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c8d1b602f9a67d35d038991f9fc4a3.bc21 + +Mars Analytica -vs- babyducks +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9b7bce1ad0efa869848c0c2fdb5169.bc21 + +babyducks -vs- Mars Analytica +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/960184dd05cd5145a345303e469eaf.bc21 + + + +Chicken -vs- Team Barcode +winner: Chicken +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7f2b3699cd3aab7c5658bb14813851.bc21 + +Team Barcode -vs- Chicken +winner: Chicken +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e452686e275b1b21eaf7f81e33ff88.bc21 + +Chicken -vs- Team Barcode +winner: Chicken +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/df9812b835a5010ad11dcd90b4df50.bc21 + + + +waffle -vs- Lee's Morons +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/77eafcdd8b4bbba77a6dfed879f119.bc21 + +Lee's Morons -vs- waffle +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e91ab9ef81423261f933111de4d2c9.bc21 + +waffle -vs- Lee's Morons +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/65bcbc78f68924d5770d9a2f99aa81.bc21 + + + +Kryptonite -vs- camel_case +winner: Kryptonite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/7a6e5f11c019772555a0c371b6dd81.bc21 + +camel_case -vs- Kryptonite +winner: Kryptonite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/4b470c1a445a8e57bff7f248aa4338.bc21 + +Kryptonite -vs- camel_case +winner: Kryptonite +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6f7bd0ffc463b34e72cbc550930f1b.bc21 + + + +Super Cow Powers -vs- Rua!! +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/65bf80e805200eab4c15841c87df6f.bc21 + +Rua!! -vs- Super Cow Powers +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/8a6ea307a56a569dfa83a8989b27c7.bc21 + +Super Cow Powers -vs- Rua!! +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/394c65e85bbe7b3ff1059d6812d581.bc21 + + + +3 Musketeers -vs- Blue Steel +winner: 3 Musketeers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f830bc3fc373c161baf1e777fbcbec.bc21 + +Blue Steel -vs- 3 Musketeers +winner: 3 Musketeers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f8e9809360a2dcecb7e74e688bcaee.bc21 + +3 Musketeers -vs- Blue Steel +winner: 3 Musketeers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/343c11636e213103ce8b68d01084ce.bc21 + + + +Download More RAM -vs- Dis Team +winner: Dis Team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/22be024c0b3cc4c454bd0d08137a23.bc21 + +Dis Team -vs- Download More RAM +winner: Dis Team +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c74b88fd034d72d0b8ba969cc952e6.bc21 + +Download More RAM -vs- Dis Team +winner: Download More RAM +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/74ae39d307074b177fdce96b69078a.bc21 + + + +Producing Perfection -vs- remotED +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/6363c8db4c8a72ce65da9912f41a77.bc21 + +remotED -vs- Producing Perfection +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/b2da277f365f7465ecdbda0bf31799.bc21 + +Producing Perfection -vs- remotED +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ef1936d20e319c6524703435968d76.bc21 + + + +babyducks -vs- Chicken +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e932b72f70a484455a879bbd0164f1.bc21 + +Chicken -vs- babyducks +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/1142f78b7e58e8f4116ec4649f6a11.bc21 + +babyducks -vs- Chicken +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/380019a7e14f76541c6027d3faa2d8.bc21 + + + +waffle -vs- Kryptonite +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/87a698bbe39c11f9b67d87124535b6.bc21 + +Kryptonite -vs- waffle +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/9b853a833b78e6afdeb219308f4674.bc21 + +waffle -vs- Kryptonite +winner: waffle +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/872f3ecb929b5e32ff376fb984a5bc.bc21 + + + +Super Cow Powers -vs- 3 Musketeers +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/c308e3e2917e470ba8a1211890a92b.bc21 + +3 Musketeers -vs- Super Cow Powers +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/dc97fb822d01d1ffd7f358d4c6f4ab.bc21 + +Super Cow Powers -vs- 3 Musketeers +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f42acba5680ebba1ac0cb8562c9326.bc21 + + + +Dis Team -vs- Producing Perfection +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ff4bf1c12dbb1c8125e59714728950.bc21 + +Producing Perfection -vs- Dis Team +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/bd8a324f514d8c69cf5f234ff1ca5e.bc21 + +Dis Team -vs- Producing Perfection +winner: Producing Perfection +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ed1923f7ba85f2481c7ea0d995a75b.bc21 + + + +babyducks -vs- waffle +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/5eaf0f18abb49eebbee465894eea9b.bc21 + +waffle -vs- babyducks +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e3aafe5c91ecc8520b4e8a52fd7ef2.bc21 + +babyducks -vs- waffle +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f83bee62b7a70e6323bc96a620894e.bc21 + + + +Super Cow Powers -vs- Producing Perfection +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f1c847903038ac8796e09378d6c16f.bc21 + +Producing Perfection -vs- Super Cow Powers +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/e630c7f313ac4661cdad77ed9b29d4.bc21 + +Super Cow Powers -vs- Producing Perfection +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/07c3e65dd56902bc0428ceca897d5d.bc21 + + + +babyducks -vs- Super Cow Powers +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/2d862833a6366076aa64839c2d843f.bc21 + +Super Cow Powers -vs- babyducks +winner: Super Cow Powers +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/ae2ec48c78a79112e3abe6ec8baee4.bc21 + +babyducks -vs- Super Cow Powers +winner: babyducks +https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/f8ce4525b058cc10a5011961cab46d.bc21 + + + diff --git a/infrastructure/tournament-util/data/1-sprint1/replay_links_only.txt b/infrastructure/tournament-util/data/1-sprint1/replay_links_only.txt new file mode 100644 index 00000000..8f543d22 --- /dev/null +++ b/infrastructure/tournament-util/data/1-sprint1/replay_links_only.txt @@ -0,0 +1,688 @@ +4e68fe7ab6b6e496a6fc4a214b855d +b3352046408750bb97adc2281b836a +efb06babb96565db2cad1fceb977ba + +ac9f36bc55ff8edfddd0539da127b1 +2b064301799d2b60a443f12b51bbc4 +7f7ad5049ce3b594e5fbc75c208a19 + +7ce4e95730a958acb1ab21a05807cd +d40f17a47ad353bba401a9d21c7eb6 +8d5593694ba9884d47fc96325125c1 + +97220f5513b6996942db248eaccfbb +94bea2ecb76d83a76fc9b21fc0d836 +39679aa7625afa87cb7cdaee3b9342 + +72b421c1f4dbd2af05348d4595a615 +acae997799723509313823bb5f9dea +84f48d7045e50d9deead749215305c + +15700ec0d11733be18687bdc5cd4c5 +07bf6b158f1ce2c7896f147908315a +cb490bf363d82307bc43dcdbbff779 + +aef6a3bb5f02ccaf29e2bc7d7e38fe +8f8f1614bdd95426a2ec8867be7e75 +614d6a2fbb6b4a6b08d0c391b9dbb2 + +0a359a03d59f88b675e6f32b88ad68 +1d5fe15e696f5f2acddbf316ec0383 +4320fdba3df68d2420e14e18ca07ae + +e605bab770f6965cb042cca18a7f26 +043f4b3a668e5c4a8bf657903a92b2 +d080e9a958568493bcca25e34605b0 + +06d31819a6d736da3417e13aa54d56 +249400fcd2453e46b9e4329514946a +f36a8e1042091208d57c0196a1d7eb + +f76ad509fd829c08b2bdd255d1151b +e65bc4afc7c99765f5287228e71d77 +5be9eb1959e242c9084527ae388c63 + +15eba1b76526770d3ece42bc74d22c +1f7dd4865c7b5b4fdec48ec554b049 +d5894d13d35df8d3da5f64dd939ff4 + +133644580e37276775d257e58ffe1b +344c620f7be4783a9a48ebbb2c4343 +e66f2dfd081b59b888863697d5ae4c + +a7f9f2b96ae6b4f143bf1baf2ec35f +a71b18b7d2734207e2982b62c3ae63 +fee62bb3cfc804500076024d36b9ab + +cf5121bd2444f8371b896bc357fe98 +0e4c73fb6e87f88e516dc5f3312632 +54ed6b350d3216022c188a65110d10 + +ec22af09313b4f9d86fbb7dc8e8949 +3cd6426e440f3c1e30fa5175965823 +a7440d90ea252fbf63152d4df7cf32 + +c42feb44629966d63ffab5bb4b5bf8 +4ad3edb6e651537863fa37ff76ff5a +694ff508861d7ee4cbdafcb9150800 + +6a3fdcef0ebbf93831daf7c76881ce +b076bafb5b511b13e1ab3fe3795b77 +c7d946efa55102f11bf03c93fcbd35 + +f4184dc6dccd266dc0128512c01f6e +8bce17b0d8fbb1132d150b998d079b +9be0ec55206196d2d8eb38cf1b3d79 + +73802e67600827838cff5f2e96a751 +791f20a65c96cb9ecfd1898b66a5be +c3b62e73bd50f559fa609ad5460d04 + +334acf16bd8a6f8725a6cfed812aea +f568c1a1e4524e2cfc7155f3e7810e +8ecc9df8db6848621501306f37d37e + +a5cc30664790d38710bdaab895e70f +6916a4906d5973a86524627436893b +8b965df23c4a7e51c5203e751b8437 + +3e9259fdf6463b650d055418b28dd8 +4c621236ce56ccdf99604b6fafa82c +38a187c6afe133c453e3551df8aba6 + +4679f5e7a918bbb397d3e88be62490 +33865665ecd0b261cfde6cad6f1dde +78a19c953507551e7e8e6cc85d1342 + +431f6b3fa895e567c6079f16693c63 +1f26e1a8e08baa786f04ab685d1fb9 +2cf63a96de6979175758a32b7ae04b + +830d9e603f42fd6255c6c7260dc860 +591d726b832e11e8ab37f1ccb339ab +e33bbb95cc5c8631b7f07b4ff6dd1e + +08c2c84b2860a34ebe8ff4f111d6da +02d006452462913bb63e4cf31774c1 +58a309702b816a5422456628005b45 + +5c3d81e576a883bb02df5488c3d020 +3106f347ac9d399b3aa65504cb8041 +3536bb2a0468e1af171d3cc839386e + +7c068cbc9c1903cd96ebce2709e0f4 +df149a4cdfb976839344039727da52 +8aa1590b33bb38bce1cb372cba1ba9 + +e72244918702746e653f1b06ed27a0 +45403de77328e216607176aef41a15 +9fda89cbd8349609a71c4bb6a1c892 + +4bddf643ea1491f7e709c75d5e2f86 +24376c7d3fd75cd91b1ba8a7d34d8c +a8e6982b3a7d2145f24ac3042b9b8b + +f23e642c6807c6eb31cbc8efe29229 +d925716dbb1c3a10b71baa673fafdd +a06a6850f8c6e7da7e77acf6ded9c6 + +c468e13928e759ecf47fb2727f5b82 +49f5f0dbad77aeb7a749f489376a81 +1a1328ddfc69f099658df5c4527031 + +6536360d3e26ecd5acf7ecb119618e +0e5a6c9b311b93fe532ae71daad6ea +f909531f95f4f060b791e74f396554 + +776c9f15ce34c27011a9bd88e9bf7a +35a6709cbc8f5d9a0bb08d2966a08f +b2cc581f91fa00bdf6d4c4a355ab21 + +ba2f3c3d0ee79cc72db0169311c7b9 +db4902e027040ee3520f8ebf308a23 +5d917f2099103efb94da89edeae26e + +fb8a724947a86601a2dba2697f42a5 +a6cbc58472043aa82a773246f55ba6 +163660b764ec3bc91a924df92f71e8 + +0a0704bd5a2476b5c3e78fe5da22f1 +959a2950a12a2bc243c93919640e1f +b749a600531d854251ea1552190fd5 + +4a00a2888b3dda04f32d940654b0fd +0116a4270defcebb399597456e4444 +4fe3b0327a87217d6549b7c8d4e886 + +d2a57c8b3d1123de9dcaaed995d375 +2f51b173701ada29079b593639f21a +20f48d123ba9ff16e39de6e2fb611e + +6cc35103615949b13d295d6538d51c +de158e02e7970afd222ef48c0c9ef8 +1c800d87b731633e7333a5dd13af60 + +9570fdc575037869917206a9f8901c +05dbabc18b1e1343a7e6fbe20c5290 +01d683747a5d3bdb4399d04c93eece + +ccd7ecdbd6385e19e0d4ec7970f681 +7b089f1f5a1feb3dc83ae83a256993 +90e07498297484ec879db5daa0a784 + +0b64bef7fad0c69144131d22b4d037 +fb2fbfff1900933152be2d4f6e8db2 +636680747e7aefb01c1cdd387d817e + +e3cc823d9c70ff143465ee035bd671 +214dafd6a0768c208ed94e87d6b9aa +50fb88aa53172148db35927d617178 + +c3e0cec57197f8aefaa213a9d85a11 +2f438063ed778e880db6b107ff5cc1 +deee4137b52a975df508c86d36b403 + +42e442e1448cf4aa5e53f7db865afd +ebdb09be67446b0bb7eea9157c9bb2 +386621ef51c53942f6e10cb6a076a5 + +fe2c45b5e219d9d81d395468a33fcd +b418fbad77f19aaa098b25df071405 +6fd80b71700f9c9cf6b60cf7822962 + +b81e38dd03f67093d90de819f1903d +4f906b049de725d40775b66a2da48a +79d986e165852df617f78686668dc3 + +bd32cd7262ccb19ae41159b677a05e +96940008a63a9dad0e42a24b4601e0 +c4964c24ba9ca0bd16c8990550e568 + +5c19756616ac46ff09571eae12ebcd +6fb58bc654adba091fbb789a096cce +4b7b40b66629535ca0f91a19ffa24f + +8136c42139f66736f42e99fbb84906 +dbcae8fe2e552cc67caaba2d6bf600 +0f9cbd5012d8c3ad24d94dbbc93569 + +645881a0279c6fe1c69243e36ffe8b +c3a7b60de622c863949b90e2db9cb6 +9166acd56b0730c7bb8ff6ba536503 + +fbfaef7f645e0acd7515948046cdb4 +f5abcc164560e96a5adf17a4cce895 +e6186fc321bb6f32c5953e0702fcbb + +8c00a45b5a7d96147bba18bee8f762 +03563d74e4037fee34ba4e98bed714 +4b21deedf40c86b69354d5b26eeb93 + +2068a93dc656b294485c4c22abb70f +a2ada8d11ba14e87d385b4e8cecdda +f4e14926c590f06402d25f13ec0a24 + +6449dca66194609d09217128fcd074 +dc3f6abdf172cc4d4c657e354efaa8 +a98e4933f5b9e8583389dfa1fe0745 + +3f2cee7472eba4cb293af956659f41 +8c892fda4686017888677e581e52df +bc6b33d25053ef96391eacda8e61ff + +074b64ee6dfeb63e9e096c1f006920 +e38dff6fce4d940ca5feac669af2fd +5ee44cfad76fa4a85a9e58644b678b + +a49f951d50985f22c4940e579d302d +9a0e23feebb68f8e6feb015059db4d +14105d0d074b2f6f08f7b608fedbc0 + +c6740d19fa6ecef91bd8eb6aeb67c6 +1b6ac77b2d25756f82273913af3186 +c2c11e41508ee7f222167783048a15 + +116cc9dba03e45c6de79eb8641d650 +63959603d6f1fb0f12ff2c3fa5f0bc +517e450bb135509636b531a5cdb93a + +f4aec9a65586bcd3617adbb47bbc0d +78592de32d3a4d2d1f2f0a9e4a46ca +614d175ae3cb07f46794036cc1f212 + +6d27815dda691ff55633baabd06af9 +7434a4f26519f39c1011fdcef5aca9 +271b8ff119b494b87d04208766fb1f + +0f33d7d04d019d4d218b2da285cb29 +27da4012e3d9a8aaf545b065b5991b +7788274440de7d5ccbf18a29c15ef3 + +32d8a111e8043d82094c9cc3eedc8a +75cb625710f02b250af5149dc9754a +7b4e212e28e72e4a3c032e202b7c49 + +9ecd0aa1ac8329009979c84912d968 +ee80b1f43ba18c35358fcfdb9a1cda +83eabb10b388cbab0ca998665df8e6 + +e61e9579178cd429ede4352b29c36c +16a3660d18ab528e7e8eea3f59d697 +5a496dfc61a2163f2d74b03352d718 + +307734dbd0b6e06ecefe3dc395e45e +f84067d9a74f575ff2c7bf2d5346da +96526c1e63b5122590c09590f34fa4 + +c29b11d5cbbba7d8d8fd75a93fdd0f +1d209ea5b84f486459b0101a9073d1 +ffe26bf366ad1dba4f2ccfb2fe6107 + +2e6f97882619fd4a86cdf98283d8f6 +71f0bf69faad0b732ea98e221deb77 +24be687d32b2cca6fbbe0d271f9504 + +416ee25ba138294f3e39102675d8fd +777a676fde65f7e889af02b38d4031 +1de16129f02884d8053391c5280bc3 + +29f0e79bc2d94a2360b3c63a016f2c +35ad8626b7d07b977bf292f36e5164 +4ad35e68b0aa5f83d428d9a6339535 + +03b597f6c23e84503e71518887f245 +039015c190e0805eeef1c5d1b76ee5 +57cc46830b05c07d2cbdfa62c55734 + +0f32bc09300ecaaf7f4d0d41370a5f +8237bf343be8177662558a65c5cb80 +42416a16f0e87df945c817a5523c71 + +d087d00b68feb538967ba11870740b +967a7f18fe2ad29104ea6c8636851a +ce522597a83fea6636b0b92d3cc958 + +4b00bf03ee56786fe6a6e3e64e7983 +d3e7111b3185d1cb8b1a26652bb085 +449e83150fc9afdfb2ae2ec99a188a + +e247987e2e5d1ab8b09291ed9f0ef7 +a4b524da39905cd6ed78134b9021e3 +0525e63be7728b445b3d8dc7429d30 + +0458af6b8544607c4241374d63a5c8 +9f6757f94a8dbff9c44364f4368cf6 +256d1d68b9d9e58adfd9bcbc783493 + +c7849a5a1de309b86fd28869309cbb +490e5deb12c5f591db3e90cd6d398e +6494b0d331125a576dd09584f17111 + +22ee94b1373bd6ce14d843c846e1c2 +7e2c254080290c49db26463fef4095 +2329795beeddf1b43cbbff3fa1e8a7 + +ea15f528a468e8357a04cc619e819a +06a61ea1902ffbec2705e5056bc8b9 +0695ae5196c012b3035642daa4ae44 + +dbb525a56ae45baaa788e6577cc67d +2ebf109f32a9e9eeaba10a6d292ed6 +7c914ffce99c482dd1480b37bedf39 + +3ef4fb69b2d51b4c9ce77258c3efad +46a88213bf3aa44d0f4258c06feb8b +92a1aaf461b5f2bbe2f2cc7af953a2 + +73db97de6e73a7fc61087f5ec61cbb +43883745d469ff486d2f575fd3c96f +26f2bcdde696a794d3964b9d2b3611 + +b05cd83540165b8bdcde80f97390f4 +5cf653a898809519c4f0a30a881c6b +39b7df9bf5585f1318154efc995683 + +1d97848bb64bfbe2ad25822a76c9dd +80e7ea29149becc56c955d09db9ac4 +466f843583fa8a50ba8a23e72355ea + +c0762c9c1c2d5dadbb05ba8d00ebe6 +7923e022799ff2c5c31c9ff923ff7b +fac2fc9af5becebfb87d3b78a1e110 + +f1255f766948fb1654ed70777e85f4 +1c171b36a23508fdeb4aba824bbe28 +c9f258086316d119400fb575ef49c8 + +f5dc451face0b7ac40e56d42d5f0ac +a296a9498e24d6186de67f3a27330c +6eaae0428a6c2d93fb632bc2ad6bcb + +ec538ce61a99d271cdf87b69b002ed +f7caaf59d1db8840df5d821e26ac05 +294ebfa740bade85b2ecd0cc012afd + +adc6225d057979c27110e27a0dfb35 +dbd9e6c163408e24e94ba7585a57a9 +33317515569db77be0afca4a755bd7 + +9a971f49c324d0f48b2f9b544c7314 +5e566698da6a1c7fae477f120d8d26 +8573aa43017bab5255ab6fc4836b27 + +4a3fba97253353bb8172a3e51f5848 +9c67c84c33d290313554928e073987 +95d3cf81a53b49877536ec57769fea + +45ab1b7f413abab8474ed8327729c9 +a29dc8b30f2d2f61ee0167087bfada +b425d32f62f730fbdff96a77923f74 + +991c193fb0e17995ab811e00be92a9 +2205ff2a7b0d3a679a0db87992cbb8 +5edaf58bddde142a3e7689eb86e6ed + +bc56b9f63ee01b82937ef958008686 +3442b12fa3a2f548112aa9784e2624 +487495076dcdee35da24aaa8950933 + +6c0244b72f8ae78003b5cf2e28b9e3 +a2791f067d4ca42f33d98dbbca66dc +f1ac64d530bfe797982fa0f54c24ca + +87b2025df45d424cdb59f6cd8f6019 +4f75fcb298bfafd0e376125ce89d8a +52baf77b8628c9338922685e8807b3 + +968b71a31e906f1253410857af8a17 +e35ce37458d31fb33c345394f445bd +1cbc93906bcf3bea8b22ff47646f8e + +9e0e2d5c724ab07ebe5852f87b807e +eaa1b1d3256f818d7437ce4459a9d0 +110b65efa6ec49eac5ce929e342271 + +ff8c45ebc302890f63d99f85a6e98b +a9d9e4b72421042190f53422150934 +1067430dca5c80c7d18565b2f6c744 + +489deb8b5f6d0980a2d1491f0aed7f +8ba1d5eb3a4b389b4165ed1a752834 +a613330f989aff5770f61485ce2a42 + +cbb9bead7441fd8e700dce34e97b4c +fff02b8884d659ae8cf9b664a35185 +f0442566aafc066369d18221d56d04 + +a588c70e14513a8890444efa5d1adf +dd2ae34510601d95692eb034731457 +67746ea0b9c65a561fecc3319f7aaf + +8646ed68984912d6b16f3933386193 +59c6639e65dba079d01a3ac4dd32dc +ea6c8b83abb9410b173ebf5b6253b2 + +8b331b688ffbc4d1d12e8217cdf9f9 +92d71188f9f4d220b46306fe0b7111 +387d678f95db6a64fcca62c21ed018 + +94232d3c837dc3bbd92f61a3a7db9d +1aead85d9ffdcf0fc149d9881e9a18 +df9069f32a7676188be8de2700aa15 + +7339e5ee4c83a94f0b3befa176b972 +3341c64ee7f85d9c24427cb9a720d4 +b0026f8ab7c9b0032cf4673ec6a45c + +0c909ec529025cf56129afc5a97704 +af579089839d857ed5bd9812798d7e +ea0b6aa0bea72fbb50aed527ac939d + +579aed9c6405b48779f9724812ca79 +5b0550d88083c0bd7562ad6b51eb4d +edc04146ce6bfaf3312facc53b1530 + +8d22eaeea375d863bb5a1aa8acfe58 +c25e7db3e99fddebbce3ccdb2597ea +f348ac48ab27db746dc4c0d02c2c0d + +b5e42bf2871773d03f3f35d7e00962 +6ad869ce21450484d9246ed65f1c33 +511c8b30e8191e0f4a48f349ad710c + +d61b8c70cf01547635e4ae4421ff91 +99671457e2a4147005fd0c7029020a +aaf6abcb3cc1026aec0b125b0696fd + +46811ca18ad261dbd3765ebc172e74 +eb39f0a492302f7889f1ce60dfa9a7 +e2f7a56e0f74a9152a6b0a0e67b95a + +d3ad7c1e2732ea109185f7c93937f5 +8577d724ad1e3c71e1d96005f71193 +589b679732c3e9780bc7d9dcc21809 + +0e6dc32a1d83a9a9a57d6199c8dd22 +6f2a60bdfdecda7e0559614b72a17a +a9d3308e2f907614b2ad20a58fb710 + +dd079c9ede562d37ee90a6bf408cb2 +bf7cae120ca34d62f2860eb6306145 +b0d66f44b26909e3a584a18d479430 + +5497eee5b0a7c3089f4193b60da310 +555fe97ef0876403d5d392b410fdea +10f93ad5bf84f6a6b303bbae563b71 + +41ab25579262c9525af9f943ec4430 +886ae0d7b32a8adf5da8c7fec8f0b0 +10a04f7b8112679b0fc8036c3c39d9 + +7eee15b21046352842095471270797 +02508f42909f8dc6aa7720c5161e74 +62341af9fc6c945ceb19b31a7c4aea + +bc587c7387d0ba2e9396f4c8a61edb +cb344935b2827c904f699db665560b +03d0933e37b1fe40ebc64090c3337e + +d1f030ae4709db43c69169c08aed82 +71e26937cfeefeac42206419247135 +cce94659d1c846222ed252782535ba + +501bcd34ba81394dcc2f31db3d75fb +fac9ae25801e9eb5cc15b9bb7515b8 +c3a3b715ebc5fb7e1ccbb589a8a657 + +7a61675ed375b21ff64153815412dd +db1fc171a8ac2fb3cd2a3bc3d14452 +b43fcbfda963e8d58b6986c78924b9 + +f4dfbe7b70f265de7f0101c03b1990 +a1dee719fc137c6fa196a403850756 +36f632b0c830d601c5a4d34b6dee2d + +05348b0c9a064578dbab2eee9ef025 +9d45571b95855e172323427c43be6a +1e286a11d4b1e8e951a08f76387c8f + +2a60a3d21b5a6e9ffd64cf0d7721e4 +bfb75930648f831d4cbd497bb841e6 +67af049be381aaf177f3842d4287bc + +0c21755bfbcfb2f14ca81d726ee5bc +d56636c1416648f6f2a74361cf8b7e +08a5a2dc5def0b568a4311c8d1dab6 + +5265ad079c79e087c1ccefce01f804 +5be1f2d113369694f59ce6a4ebf7fd +6990108583403fc6e2be9f6e588b93 + +022241fceaf7ca9a000ee93145513e +17fcbe03de01b9cfb52768aa79b11e +8419ea381dfb3a6dda24c5a1e4e07f + +21385bdaf9eccd695baf0052f583cc +33fa77d9b3247c641026ad99f8877f +6434ed05329392870fb2a67790b2c1 + +10bde96addde86524368cb7a59d9f5 +60b03467942cf1b398542710891225 +8b83daa01ee17d0deabeddd71190c2 + +851a0314b23b17be31d6003fad8444 +57cadbea3910197b65569ca7675409 +58d8561109d039081848af3340d4b3 + +ed02197927b849ebfa6dec7063ec6b +88a9f8a4710f72f0fa0fd48ffdd7df +c681104115288afea779d1e44695d6 + +f71658ce0e3c25536a8330cbfbf4a5 +f2fe71704b50aaf1786f3caae19674 +d62e1dfa42f9e7922cd585f5986132 + +4c458b0a626dc9c1633428bb9a2634 +7e6c284c74b9edae73c9f27991dd99 +dd3a4710bed854669523d711c824ba + +fc11a02893546ec7a021d34e18a938 +f8a6690c747de6fd8eda1a066e2a70 +ee40ca00a7e99f909ae18e0cb8f3da + +6960009ded2047c134d2c7f6eb3c0e +0ba21993f235e0072dff7de1cb3bc4 +6b0705f3eaa807500961cb8fe75441 + +d7bbbd92f93fc6c81daf64dabc4e3c +b6c25530c6e33da10af6cbf6abec99 +7aaaeae16853b8c4de9620dea818ba + +f2125fd661fb7edc148899baaa7959 +caa249dbecdd4fb472dc425d7a6b6a +fdc3ccc949ea6b387c4112808f39fd + +ba31fb94bb56253f3c815f8b1410ce +eebe3f1c6a4a81ab60076aeca90ecb +2b57056e04e2387133b1d5ab0b0991 + +5f7ec0f068ac73d9d98de4006572da +c2de2b6a24976536db0cb841a290ad +1655404cc8df0b56def13a122c75e7 + +a65d534870c5229ca6423b68c13ace +9edc7a4174040a012618d532618552 +49da8c34c2f6e2b396359615eb54da + +5eff0c192a84cb5324a84f11a828a4 +f942d1b9cff2687961d35406211c90 +af6385645df4b8cb59dec456b74b2e + +8c2bcef17704f12b5f7af2ec49cfdc +b8dcdd11799979cca176e47c776dd3 +0568f56e3ad3be5c4bca31fd131011 + +a862d702b27986f18231ca8ea95863 +ae0e28a2fb15a33468251851cd2c87 +2f9f0ad470db3cfc81448c3d4f99be + +8ee187176d60359acd7dad513a74a6 +c9da9ff91e49938df8ca76ca3bfb52 +c70146607a429ddff4dc580ac36299 + +41f40a352dc394d6b40a96948681a5 +00836188f372dbd03fcf25f3afd7ee +423fdb754ce3de38705328c8d187ce + +4913473aedc877d3745d444e53267a +41162ea1e474bfc59f832fcc190191 +449445dec8a0c29060c6affb47ba27 + +c9b7fcb86af087ee187b95dd6aa3ef +660c6a0ab8420e9aebca44830ff0de +17f9401653865c6d25ab2000d80d8a + +09636d2c31d464873706a5340bc67c +82141563bb94ac70bafa34b5f25ffc +74abb881f0286f8e676dbed31056a7 + +bd4a65357952ec08525124763a8c26 +9e476e2e03d8d99835e68e16ee2a92 +860e0cba96928b2d67eefe731a4840 + +8ca9e4c7750f9ab3a8b699ed95147c +de75d7f5f873ce5b77ab4bc7e72553 +046413c1233738e9e2fd13ce29cf6c + +d5028d1e70367330574ce71525cd84 +f94bcdf2f33300bc857b1be073d889 +18af2775d6a936e3d37bbd74f71100 + +9eb4182fe7d871f5803b779fd236a9 +9de7cf111587d4f0e2a3748f6c05e4 +c1f24fdea939307c11faa7978a6f9b + +a7ecb60bf5ee5d5d0f4a3d475ce805 +3bbc75d27becb6dfc064d28dd34634 +3c56ddfee960cfdb495323d6a17fd9 + +c8d1b602f9a67d35d038991f9fc4a3 +9b7bce1ad0efa869848c0c2fdb5169 +960184dd05cd5145a345303e469eaf + +7f2b3699cd3aab7c5658bb14813851 +e452686e275b1b21eaf7f81e33ff88 +df9812b835a5010ad11dcd90b4df50 + +77eafcdd8b4bbba77a6dfed879f119 +e91ab9ef81423261f933111de4d2c9 +65bcbc78f68924d5770d9a2f99aa81 + +7a6e5f11c019772555a0c371b6dd81 +4b470c1a445a8e57bff7f248aa4338 +6f7bd0ffc463b34e72cbc550930f1b + +65bf80e805200eab4c15841c87df6f +8a6ea307a56a569dfa83a8989b27c7 +394c65e85bbe7b3ff1059d6812d581 + +f830bc3fc373c161baf1e777fbcbec +f8e9809360a2dcecb7e74e688bcaee +343c11636e213103ce8b68d01084ce + +22be024c0b3cc4c454bd0d08137a23 +c74b88fd034d72d0b8ba969cc952e6 +74ae39d307074b177fdce96b69078a + +6363c8db4c8a72ce65da9912f41a77 +b2da277f365f7465ecdbda0bf31799 +ef1936d20e319c6524703435968d76 + +e932b72f70a484455a879bbd0164f1 +1142f78b7e58e8f4116ec4649f6a11 +380019a7e14f76541c6027d3faa2d8 + +87a698bbe39c11f9b67d87124535b6 +9b853a833b78e6afdeb219308f4674 +872f3ecb929b5e32ff376fb984a5bc + +c308e3e2917e470ba8a1211890a92b +dc97fb822d01d1ffd7f358d4c6f4ab +f42acba5680ebba1ac0cb8562c9326 + +ff4bf1c12dbb1c8125e59714728950 +bd8a324f514d8c69cf5f234ff1ca5e +ed1923f7ba85f2481c7ea0d995a75b + +5eaf0f18abb49eebbee465894eea9b +e3aafe5c91ecc8520b4e8a52fd7ef2 +f83bee62b7a70e6323bc96a620894e + +f1c847903038ac8796e09378d6c16f +e630c7f313ac4661cdad77ed9b29d4 +07c3e65dd56902bc0428ceca897d5d + +2d862833a6366076aa64839c2d843f +ae2ec48c78a79112e3abe6ec8baee4 +f8ce4525b058cc10a5011961cab46d + diff --git a/infrastructure/tournament-util/match_list.py b/infrastructure/tournament-util/match_list.py index f4c09ec4..9dad10ea 100755 --- a/infrastructure/tournament-util/match_list.py +++ b/infrastructure/tournament-util/match_list.py @@ -9,14 +9,24 @@ if match is not None: for game in match: if game[3] == 1: - winner = 'redwon' + # winner = 'redwon' + winner = game[0] elif game[3] == 2: - winner = 'bluewon' + # winner = 'bluewon' + winner = game[1] else: raise ValueError('Invalid winner: {}'.format(game[3])) - print ('{} -vs- {} | {} {} replay {}'.format( - game[0].rjust(40), # Red team - game[1].ljust(40), # Blue team - game[2].ljust(30), # Map name - winner.ljust(8), # Winner status - game[4])) # Replay id + # print ('{} -vs- {}'.format( + # game[0], # Red team + # game[1])) # Blue team + # print('winner: {}'.format( + # winner)) + # replay_link = 'https://2021.battlecode.org/visualizer.html?tournamentMode&https://2021.battlecode.org/replays/' + game[4] + '.bc21' + replay_link = game[4] + print(replay_link) + # blank line to separate games + # print() + # more blank lines to separate matches + # print() + print() + diff --git a/infrastructure/tournament-util/prep_tournament.sql b/infrastructure/tournament-util/prep_tournament.sql new file mode 100644 index 00000000..ffd8b94d --- /dev/null +++ b/infrastructure/tournament-util/prep_tournament.sql @@ -0,0 +1,22 @@ +-- MAKE A BACKUP ON GCLOUD BEFORE RUNNING THIS +-- Also run this in steps not as a file + +-- 1: Set submissions_enabled to False in api_league +update api_league set submissions_enabled=FALSE; + +-- 2: Change `tour_seed_id` to the current tournament +update api_teamsubmission set tour_seed_id = last_1_id; + +-- 3: Add the tournament to the tournaments table +insert into api_tournament (id, "name", style, date_time, divisions, stream_link, hidden, league_id) +values (2, 'Seeding', 'doubleelim', CURRENTDATE, '{college}', STREAMLINK, True, 0); + + + +-- Get the emails of winning teams +SELECT email from api_user left join api_team_users on api_team_users.user_id = api_user.id +left join api_team on api_team_users.team_id = api_team.id +WHERE api_team."name" in ('wining', 'team', 'names'); + + -- Lock in submissions for more advanced tournaments +UPDATE api_teamsubmission SET tour_intl_qual_id=last_1_id FROM api_team WHERE api_team.id=api_teamsubmission.team_id AND api_team.international AND api_team.student; \ No newline at end of file diff --git a/infrastructure/tournament.Dockerfile b/infrastructure/tournament.Dockerfile index ac894764..81ccf3b1 100644 --- a/infrastructure/tournament.Dockerfile +++ b/infrastructure/tournament.Dockerfile @@ -4,7 +4,7 @@ FROM bc21-env RUN pip3 install --upgrade \ requests -# COPY config.py util.py bracketlib.py team_pk team_names tournament_server.py app/ +COPY config.py util.py bracketlib.py team_pk team_names maps.json tournament_server.py app/ WORKDIR app -CMD ./tournament_server.py +CMD python3 tournament_server.py 0 team_pk team_names maps.json diff --git a/infrastructure/worker.Dockerfile b/infrastructure/worker.Dockerfile index d9fa5109..0ddfc522 100644 --- a/infrastructure/worker.Dockerfile +++ b/infrastructure/worker.Dockerfile @@ -22,7 +22,7 @@ RUN pip3 install --upgrade \ google-cloud-storage # # Initialise box and game dependencies -# COPY box box/ -# RUN cd box && ./gradlew --no-daemon build && rm -rf build src +COPY box box/ +RUN cd box && ./gradlew --no-daemon build && rm -rf build src -# COPY app/config.py app/subscription.py app/util.py app/gcloud-key.json app/ +COPY app/config.py app/subscription.py app/util.py app/gcloud-key.json app/ diff --git a/infrastructure/worker/app/compile_server.py b/infrastructure/worker/app/compile_server.py index 8b549607..f82ed2f6 100755 --- a/infrastructure/worker/app/compile_server.py +++ b/infrastructure/worker/app/compile_server.py @@ -11,12 +11,14 @@ from google.cloud import storage -def compile_report_result(submissionid, result): + +def compile_report_result(submissionid, result, reason=''): """Sends the result of the run to the API endpoint""" try: auth_token = util.get_api_auth_token() response = requests.patch(url=api_compile_update(submissionid), data={ - 'compilation_status': result + # Error message field not implemented yet + 'compilation_status': result #, 'error_msg': reason }, headers={ 'Authorization': 'Bearer {}'.format(auth_token) }) @@ -28,9 +30,19 @@ def compile_report_result(submissionid, result): def compile_log_error(submissionid, reason): """Reports a server-side error to the backend and terminates with failure""" logging.error(reason) - compile_report_result(submissionid, COMPILE_ERROR) + compile_report_result(submissionid, COMPILE_ERROR, reason=reason) sys.exit(1) +def compile_log_fail(submissionid, reason): + """Reports a compilation failure to the backend""" + logging.error(reason) + compile_report_result(submissionid, COMPILE_FAIL, reason=reason) + +def compile_log_success(submissionid): + """Reports a server-side success to the backend""" + logging.info('Compilation succeeded') + compile_report_result(submissionid, COMPILE_SUCCESS) + def compile_worker(submissionid): """ Performs a compilation job as specified in submissionid @@ -67,6 +79,8 @@ def compile_worker(submissionid): except: compile_log_error(submissionid, 'Could not retrieve source file from bucket') + + # Decompress submission archive result = util.monitor_command( ['unzip', 'source.zip', '-d', sourcedir], @@ -89,7 +103,7 @@ def compile_worker(submissionid): packages = os.listdir(classdir) except: # No classes were generated after compiling - compile_report_result(submissionid, COMPILE_FAILED) + compile_log_fail(submissionid, 'No classes generated') else: if result[0] == 0 and len(packages) == 1: # Compress compiled classes @@ -105,13 +119,11 @@ def compile_worker(submissionid): bucket.blob(os.path.join(submissionid, 'player.zip')).upload_from_file(file_obj) except: compile_log_error(submissionid, 'Could not send executable to bucket') - logging.info('Compilation succeeded') - compile_report_result(submissionid, COMPILE_SUCCESS) + compile_log_success(submissionid) else: compile_log_error(submissionid, 'Could not compress compiled classes') else: - logging.info('Compilation failed') - compile_report_result(submissionid, COMPILE_FAILED) + compile_log_fail(submissionid, 'Compilation process failed, or no classes generated') finally: # Clean up working directory try: @@ -123,4 +135,4 @@ def compile_worker(submissionid): if __name__ == '__main__': - subscription.subscribe(GCLOUD_SUB_COMPILE_NAME, compile_worker, give_up=True) + subscription.subscribe(GCLOUD_SUB_COMPILE_NAME, compile_worker, give_up=False) diff --git a/infrastructure/worker/app/config.py b/infrastructure/worker/app/config.py index d990730c..07c8ef6f 100644 --- a/infrastructure/worker/app/config.py +++ b/infrastructure/worker/app/config.py @@ -33,9 +33,12 @@ # Compilation API specifications + +COMPILE_INPROGRESS = 0 COMPILE_SUCCESS = 1 -COMPILE_FAILED = 2 -COMPILE_ERROR = 3 +COMPILE_FAIL = 2 +COMPILE_ERROR = 3 #error somewhere along the way + def api_compile_update(submissionid): """ Returns the API link for reporting the compilation status diff --git a/infrastructure/worker/app/game_server.py b/infrastructure/worker/app/game_server.py index 425d3d59..11d3443f 100755 --- a/infrastructure/worker/app/game_server.py +++ b/infrastructure/worker/app/game_server.py @@ -11,14 +11,17 @@ from google.cloud import storage -def game_report_result(gametype, gameid, result, winscore=None, losescore=None): +def game_report_result(gametype, gameid, result, winscore=None, losescore=None, new_replay=None, reason=''): + """Sends the result of the run to the API endpoint""" try: auth_token = util.get_api_auth_token() response = requests.patch(url=api_game_update(gametype, gameid), data={ 'status': result, 'winscore': winscore, - 'losescore': losescore + 'losescore': losescore, + 'new_replay': new_replay, + 'error_msg': reason }, headers={ 'Authorization': 'Bearer {}'.format(auth_token) }) @@ -27,12 +30,16 @@ def game_report_result(gametype, gameid, result, winscore=None, losescore=None): logging.critical('Could not report result to API endpoint', exc_info=e) sys.exit(1) -def game_log_error(gametype, gameid, reason): +def game_log_error(gametype, gameid, reason): #For when the game fails and it is our fault """Reports a server-side error to the backend and terminates with failure""" logging.error(reason) - game_report_result(gametype, gameid, GAME_ERROR) + game_report_result(gametype, gameid, GAME_ERROR, reason=reason) sys.exit(1) +def game_log_fail(gametype, gameid, reason): #For when the game fails and its not our fault + logging.error(reason) + game_report_result(gametype, gameid, GAME_ERROR, reason=reason) + def game_worker(gameinfo): """ Runs a game as specified by the message @@ -45,6 +52,8 @@ def game_worker(gameinfo): name2: string, team name of the blue player maps: string, comma separated list of maps replay: string, a unique identifier for the name of the replay + tourmode: string, "True" to use an expiremental tournament mode + Filesystem structure: /box/ @@ -73,6 +82,10 @@ def game_worker(gameinfo): player2 = gameinfo['player2'] maps = gameinfo['maps'] replay = gameinfo['replay'] + tourmode = False + + if 'tourmode' in gameinfo and gameinfo['tourmode'] == 'True': + tourmode = True # For reverse-compatibility if 'name1' in gameinfo: @@ -83,8 +96,10 @@ def game_worker(gameinfo): teamname2 = gameinfo['name2'] else: teamname2 = player2 - except: + except Exception as ex: game_log_error(gametype, gameid, 'Game information in incorrect format') + + rootdir = os.path.join('/', 'box') classdir = os.path.join(rootdir, 'classes') @@ -92,14 +107,21 @@ def game_worker(gameinfo): try: # Obtain player executables - try: - os.mkdir(classdir) - with open(os.path.join(classdir, 'player1.zip'), 'wb') as file_obj: - bucket.get_blob(os.path.join(player1, 'player.zip')).download_to_file(file_obj) - with open(os.path.join(classdir, 'player2.zip'), 'wb') as file_obj: - bucket.get_blob(os.path.join(player2, 'player.zip')).download_to_file(file_obj) - except: - game_log_error(gametype, gameid, 'Could not retrieve submissions from bucket') + attempts = 10 + for i in range(attempts): + try: + os.mkdir(classdir) + with open(os.path.join(classdir, 'player1.zip'), 'wb') as file_obj: + bucket.get_blob(os.path.join(player1, 'player.zip')).download_to_file(file_obj) + with open(os.path.join(classdir, 'player2.zip'), 'wb') as file_obj: + bucket.get_blob(os.path.join(player2, 'player.zip')).download_to_file(file_obj) + + break # exit loop and continue on our merry way + except: + if i >= attempts-1: #Tried {attempts} times, give up + game_log_error(gametype, gameid, 'Could not retrieve submissions from bucket') + else: #Try again + logging.warn('Could not retrieve submissions from bucket, retrying...') # Decompress zip archives of player classes try: @@ -134,65 +156,87 @@ def game_worker(gameinfo): # Update distribution util.pull_distribution(rootdir, lambda: game_log_error(gametype, gameid, 'Could not pull distribution')) - # Execute game - result = util.monitor_command( - ['./gradlew', 'run', - '-PteamA={}'.format(teamname1), - '-PteamB={}'.format(teamname2), - '-PclassLocationA={}'.format(os.path.join(classdir, 'player1')), - '-PclassLocationB={}'.format(os.path.join(classdir, 'player2')), - '-PpackageNameA={}'.format(package1), - '-PpackageNameB={}'.format(package2), - '-Pmaps={}'.format(maps), - '-Preplay=replay.bc20' - ], - cwd=rootdir, - timeout=TIMEOUT_GAME) - - if result[0] != 0: - game_log_error(gametype, gameid, 'Game execution had non-zero return code') - - # Upload replay file - bucket = client.get_bucket(GCLOUD_BUCKET_REPLAY) - try: - with open(os.path.join(rootdir, 'replay.bc20'), 'rb') as file_obj: - bucket.blob(os.path.join('replays', '{}.bc20'.format(replay))).upload_from_file(file_obj) - except: - game_log_error(gametype, gameid, 'Could not send replay file to bucket') - - # Interpret game result - server_output = result[1].split('\n') - - wins = [0, 0] - try: - # Read the winner of each game from the engine - for line in server_output: - if re.fullmatch(GAME_WINNER, line): - game_winner = line[line.rfind('wins')-3] - assert (game_winner == 'A' or game_winner == 'B') - if game_winner == 'A': - wins[0] += 1 - elif game_winner == 'B': - wins[1] += 1 - # We should have as many game wins as games played - assert (wins[0] + wins[1] == len(maps.split(','))) - logging.info('Game ended. Result {}:{}'.format(wins[0], wins[1])) - except: - game_log_error(gametype, gameid, 'Could not determine winner') + # Prep maps + # We want the maps as a list, so we can iterate. + # For tournament mode, we want to split up map list into separate parts so we can run on each map individually; + # For regular mode, we want to pass the map list in as a string of comma-separated maps, + # but we wrap it in a list so we can "iterate" still while retaining old behavior. + if tourmode: + maps = maps.split(',') else: - if wins[0] > wins[1]: - game_report_result(gametype, gameid, GAME_REDWON, wins[0], wins[1]) - elif wins[1] > wins[0]: - game_report_result(gametype, gameid, GAME_BLUEWON, wins[1], wins[0]) + maps = [maps] + + # For tour mode, game_number represents which game (of a match) we're in; + # in regular mode, game_number only takes on a value of 0 and doesn't really mean much + # (since all the maps get played in the the same engine run) + for game_number in range (0, len(maps)): + maps_arg = maps[game_number] + # Execute game + result = util.monitor_command( + ['./gradlew', 'run', + '-PteamA={}'.format(teamname1), + '-PteamB={}'.format(teamname2), + '-PclassLocationA={}'.format(os.path.join(classdir, 'player1')), + '-PclassLocationB={}'.format(os.path.join(classdir, 'player2')), + '-PpackageNameA={}'.format(package1), + '-PpackageNameB={}'.format(package2), + '-Pmaps={}'.format(maps_arg), + '-Preplay=replay.bc21' + ], + cwd=rootdir, + timeout=TIMEOUT_GAME) + + if result[0] != 0: + game_log_error(gametype, gameid, 'Game execution had non-zero return code') + + # Upload replay file + # In tour mode, we create the replay link by appending the match number to the replay hex + replay_id = replay + if tourmode: + replay_id += '-' + str(game_number) + bucket = client.get_bucket(GCLOUD_BUCKET_REPLAY) + try: + with open(os.path.join(rootdir, 'replay.bc21'), 'rb') as file_obj: + bucket.blob(os.path.join('replays', '{}.bc21'.format(replay_id))).upload_from_file(file_obj) + except: + game_log_error(gametype, gameid, 'Could not send replay file to bucket') + + + # Interpret game result + server_output = result[1].split('\n') + + wins = [0, 0] + try: + # Read the winner of each game from the engine + for line in server_output: + if re.fullmatch(GAME_WINNER, line): + game_winner = line[line.rfind('wins')-3] + assert (game_winner == 'A' or game_winner == 'B') + if game_winner == 'A': + wins[0] += 1 + elif game_winner == 'B': + wins[1] += 1 + # We should have as many game wins as games played + assert (wins[0] + wins[1] == len(maps_arg.split(','))) + logging.info('Game ended. Result {}:{}'.format(wins[0], wins[1])) + except: + game_log_error(gametype, gameid, 'Could not determine winner') else: - game_log_error(gametype, gameid, 'Ended in draw, which should not happen') + + if wins[0] > wins[1]: + game_report_result(gametype, gameid, GAME_REDWON, wins[0], wins[1]) + elif wins[1] > wins[0]: + game_report_result(gametype, gameid, GAME_BLUEWON, wins[1], wins[0]) + else: + game_log_error(gametype, gameid, 'Ended in draw, which should not happen') + finally: # Clean up working directory try: shutil.rmtree(classdir) shutil.rmtree(builddir) - os.remove(os.path.join(rootdir, 'replay.bc20')) + os.remove(os.path.join(rootdir, 'replay.bc21')) except: logging.warning('Could not clean up game execution directory') diff --git a/infrastructure/worker/app/subscription.py b/infrastructure/worker/app/subscription.py index ad42493f..24c2f390 100644 --- a/infrastructure/worker/app/subscription.py +++ b/infrastructure/worker/app/subscription.py @@ -24,7 +24,9 @@ def renew_deadline(): if message != None: try: with lock: - client.modify_ack_deadline(subscription_path, [message.ack_id], ack_deadline_seconds=SUB_ACK_DEADLINE) + client.modify_ack_deadline(subscription=subscription_path, + ack_ids=[message.ack_id], + ack_deadline_seconds=SUB_ACK_DEADLINE) logging.debug('Reset ack deadline for {} for {}s'.format(message.message.data.decode(), SUB_ACK_DEADLINE)) time.sleep(SUB_SLEEP_TIME) except Exception as e: @@ -36,7 +38,7 @@ def renew_deadline(): logging.info('Listening for jobs') try: while not shutdown_requested: - response = client.pull(subscription_path, max_messages=1, return_immediately=True) + response = client.pull(request= {'subscription': subscription_path, 'max_messages':1}) if not response.received_messages: logging.info('Job queue is empty') @@ -57,14 +59,14 @@ def renew_deadline(): if process.exitcode == 0: # Success; acknowledge and return try: - client.acknowledge(subscription_path, [message.ack_id]) + client.acknowledge(subscription=subscription_path, ack_ids=[message.ack_id]) logging.info('Ending and acknowledged: {}'.format(message.message.data.decode())) except Exception as e: logging.error('Could not end and acknowledge: {}'.format(message.message.data.decode()), exc_info=e) - elif give_up and (int(time.time()) - message.message.publish_time.seconds) > 600: + elif give_up and (time.time() - message.message.publish_time.timestamp()) >600: # Failure; give up and acknowledge try: - client.acknowledge(subscription_path, [message.ack_id]) + client.acknowledge(subscription=subscription_path, ack_ids=[message.ack_id]) logging.error('Failed but acknowledged: {}'.format(message.message.data.decode())) except Exception as e: logging.error('Failed but could not acknowledge: {}'.format(message.message.data.decode()), exc_info=e) diff --git a/infrastructure/worker/box/build.gradle b/infrastructure/worker/box/build.gradle index 168fd1bf..58f9d4dd 100644 --- a/infrastructure/worker/box/build.gradle +++ b/infrastructure/worker/box/build.gradle @@ -16,10 +16,10 @@ repositories { mavenCentral() maven { - url "https://maven.pkg.github.com/battlecode/battlecode20" + url "https://maven.pkg.github.com/battlecode/battlecode21" credentials { username = project.findProperty("gpr.user") - password = new URL("https://2020.battlecode.org/access.txt").text.trim() + password = new URL("https://2021.battlecode.org/access.txt").text.trim() } } // Use the JCenter repo to resolve Scala dependencies. @@ -65,13 +65,13 @@ if (!project.hasProperty("classLocationB")) { ext.classLocationB = sourceSets.main.output.classesDirs.getAsPath() } if (!project.hasProperty("replay")) { - ext.replay = 'matches/' + project.property('teamA') + '-vs-' + project.property('teamB') + '-on-' + project.property('maps') + '.bc20' + ext.replay = 'matches/' + project.property('teamA') + '-vs-' + project.property('teamB') + '-on-' + project.property('maps') + '.bc21' } // The dependencies of this project. dependencies { // The Battlecode engine. - implementation group: 'org.battlecode', name: 'battlecode', version: versions.battlecode + implementation group: 'org.battlecode', name: 'battlecode21', version: versions.battlecode // Scala! implementation 'org.scala-lang:scala-library:2.11.7' @@ -92,7 +92,7 @@ task update { group 'battlecode' doLast { //overwrites stored version number - new File(projectDir, "version.txt").text = new URL("https://2020.battlecode.org/version.txt").text + new File(projectDir, "version.txt").text = new URL("https://2021.battlecode.org/version.txt").text //overwrites cached version number versions.battlecode = new File(projectDir, "version.txt").text @@ -125,6 +125,7 @@ task run(type: JavaExec, dependsOn: 'build') { '-Dbc.server.map-path=maps', '-Dbc.server.robot-player-to-system-out=false', '-Dbc.server.robot-player-replay-file-per-team-limit-bytes=1048576', + '-Dbc.engine.show-indicators=false', '-Dbc.game.team-a='+project.property('teamA'), '-Dbc.game.team-b='+project.property('teamB'), '-Dbc.game.team-a.url='+project.property('classLocationA'), @@ -163,13 +164,13 @@ task listMaps { doLast { sourceSets.main.compileClasspath.each { - if (it.toString().contains('battlecode-2020')) { + if (it.toString().contains('battlecode-2021')) { FileCollection fc = zipTree(it) fc += fileTree(new File(project.projectDir, 'maps')) fc.each { String fn = it.getName() - if (fn.endsWith('.map20')) { - println 'MAP: '+fn.substring(0, fn.indexOf('.map20')) + if (fn.endsWith('.map21')) { + println 'MAP: '+fn.substring(0, fn.indexOf('.map21')) } } } diff --git a/infrastructure/worker/box/gradle.properties b/infrastructure/worker/box/gradle.properties index 63416533..6baf6718 100644 --- a/infrastructure/worker/box/gradle.properties +++ b/infrastructure/worker/box/gradle.properties @@ -5,5 +5,5 @@ packageNameA=examplefuncsplayer packageNameB=examplefuncsplayer maps=maptestsmall source=src -version=2020.0.1.2 +version=2021.0.0.1 gpr.user=battlecodedownloadpackage diff --git a/infrastructure/worker/box/gradlew b/infrastructure/worker/box/gradlew old mode 100644 new mode 100755 diff --git a/infrastructure/worker/box/src/Helloworld.java b/infrastructure/worker/box/src/Helloworld.java deleted file mode 100644 index 01a75af8..00000000 --- a/infrastructure/worker/box/src/Helloworld.java +++ /dev/null @@ -1,12 +0,0 @@ -/** - * This hello world program is compiled when the docker image is built, to - * force gradle to download all of its dependencies; this way, users have a - * shorter wait time because they don't have to wait for this very slow - * download - */ - -public class Helloworld { - public static void main(String[] args) { - System.out.println("Hello world!"); - } -} diff --git a/infrastructure/worker/box/version.txt b/infrastructure/worker/box/version.txt index 2fe34749..b53028ea 100644 --- a/infrastructure/worker/box/version.txt +++ b/infrastructure/worker/box/version.txt @@ -1 +1 @@ -2020.1.0.0 \ No newline at end of file +2021.0.0.1 \ No newline at end of file diff --git a/release.py b/release_python.py similarity index 95% rename from release.py rename to release_python.py index 39594d5b..15ef7b24 100755 --- a/release.py +++ b/release_python.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +In general, this script is out of date. Make any changes as necessary. +(e.g. repo names, year numbers, new frontend deploy process, different specs) Here's what this script does: * Generates a comparison link to review the changes * Adds version number and changelog to specs/specs.md @@ -22,6 +24,7 @@ def main(version): specs(version) + # TODO should be adapted now that we use markdeep instead fancy_specs() deploy_frontend() diff --git a/schema/battlecode.fbs b/schema/battlecode.fbs index 2c43bdbc..57a2963c 100644 --- a/schema/battlecode.fbs +++ b/schema/battlecode.fbs @@ -80,7 +80,7 @@ table GameMap { /// the actions were performed. enum Action : byte { /// Politicians self-destruct and affect nearby bodies. - /// Target: none + /// Target: radius squared EMPOWER, /// Slanderers passively generate influence for the /// Enlightenment Center that created them. @@ -148,7 +148,7 @@ table TeamData { teamID: byte; } -/// Profiler tables +// Profiler tables /// These tables are set-up so that they match closely with speedscope's file format documented at /// https://github.com/jlfwong/speedscope/wiki/Importing-from-custom-sources. @@ -235,6 +235,8 @@ table MatchFooter { winner: byte; /// The number of rounds played. totalRounds: int; + /// Profiler data for team A and B if profiling is enabled. + profilerFiles: [ProfilerFile]; } /// A single time-step in a Game. @@ -313,6 +315,9 @@ table Round { bytecodeIDs: [int]; /// The bytecodes used by the player bodies. bytecodesUsed: [int]; + + /// Amount of influence contributing to the teams' buffs. Added at end for backwards compatability. + teamNumBuffs: [int]; } /// Necessary due to flatbuffers requiring unions to be wrapped in tables. diff --git a/schema/java/battlecode/schema/Action.java b/schema/java/battlecode/schema/Action.java index 6bf137da..a21287a3 100644 --- a/schema/java/battlecode/schema/Action.java +++ b/schema/java/battlecode/schema/Action.java @@ -14,7 +14,7 @@ public final class Action { private Action() { } /** * Politicians self-destruct and affect nearby bodies. - * Target: none + * Target: radius squared */ public static final byte EMPOWER = 0; /** diff --git a/schema/java/battlecode/schema/MatchFooter.java b/schema/java/battlecode/schema/MatchFooter.java index 76a56d36..8777c9ea 100644 --- a/schema/java/battlecode/schema/MatchFooter.java +++ b/schema/java/battlecode/schema/MatchFooter.java @@ -25,19 +25,30 @@ public final class MatchFooter extends Table { * The number of rounds played. */ public int totalRounds() { int o = __offset(6); return o != 0 ? bb.getInt(o + bb_pos) : 0; } + /** + * Profiler data for team A and B if profiling is enabled. + */ + public ProfilerFile profilerFiles(int j) { return profilerFiles(new ProfilerFile(), j); } + public ProfilerFile profilerFiles(ProfilerFile obj, int j) { int o = __offset(8); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; } + public int profilerFilesLength() { int o = __offset(8); return o != 0 ? __vector_len(o) : 0; } public static int createMatchFooter(FlatBufferBuilder builder, byte winner, - int totalRounds) { - builder.startObject(2); + int totalRounds, + int profilerFilesOffset) { + builder.startObject(3); + MatchFooter.addProfilerFiles(builder, profilerFilesOffset); MatchFooter.addTotalRounds(builder, totalRounds); MatchFooter.addWinner(builder, winner); return MatchFooter.endMatchFooter(builder); } - public static void startMatchFooter(FlatBufferBuilder builder) { builder.startObject(2); } + public static void startMatchFooter(FlatBufferBuilder builder) { builder.startObject(3); } public static void addWinner(FlatBufferBuilder builder, byte winner) { builder.addByte(0, winner, 0); } public static void addTotalRounds(FlatBufferBuilder builder, int totalRounds) { builder.addInt(1, totalRounds, 0); } + public static void addProfilerFiles(FlatBufferBuilder builder, int profilerFilesOffset) { builder.addOffset(2, profilerFilesOffset, 0); } + public static int createProfilerFilesVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); } + public static void startProfilerFilesVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); } public static int endMatchFooter(FlatBufferBuilder builder) { int o = builder.endObject(); return o; diff --git a/schema/java/battlecode/schema/ProfilerEvent.java b/schema/java/battlecode/schema/ProfilerEvent.java index d7c861d8..58c47f12 100644 --- a/schema/java/battlecode/schema/ProfilerEvent.java +++ b/schema/java/battlecode/schema/ProfilerEvent.java @@ -9,7 +9,6 @@ @SuppressWarnings("unused") /** - * Profiler tables * These tables are set-up so that they match closely with speedscope's file format documented at * https://github.com/jlfwong/speedscope/wiki/Importing-from-custom-sources. * The client uses speedscope to show the recorded data in an interactive interface. diff --git a/schema/java/battlecode/schema/Round.java b/schema/java/battlecode/schema/Round.java index fabd21ff..c36316ae 100644 --- a/schema/java/battlecode/schema/Round.java +++ b/schema/java/battlecode/schema/Round.java @@ -169,6 +169,13 @@ public final class Round extends Table { public int bytecodesUsedLength() { int o = __offset(44); return o != 0 ? __vector_len(o) : 0; } public ByteBuffer bytecodesUsedAsByteBuffer() { return __vector_as_bytebuffer(44, 4); } public ByteBuffer bytecodesUsedInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 44, 4); } + /** + * Amount of influence contributing to the teams' buffs. Added at end for backwards compatability. + */ + public int teamNumBuffs(int j) { int o = __offset(46); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; } + public int teamNumBuffsLength() { int o = __offset(46); return o != 0 ? __vector_len(o) : 0; } + public ByteBuffer teamNumBuffsAsByteBuffer() { return __vector_as_bytebuffer(46, 4); } + public ByteBuffer teamNumBuffsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 46, 4); } public static int createRound(FlatBufferBuilder builder, int teamIDsOffset, @@ -191,8 +198,10 @@ public static int createRound(FlatBufferBuilder builder, int logsOffset, int roundID, int bytecodeIDsOffset, - int bytecodesUsedOffset) { - builder.startObject(21); + int bytecodesUsedOffset, + int teamNumBuffsOffset) { + builder.startObject(22); + Round.addTeamNumBuffs(builder, teamNumBuffsOffset); Round.addBytecodesUsed(builder, bytecodesUsedOffset); Round.addBytecodeIDs(builder, bytecodeIDsOffset); Round.addRoundID(builder, roundID); @@ -217,7 +226,7 @@ public static int createRound(FlatBufferBuilder builder, return Round.endRound(builder); } - public static void startRound(FlatBufferBuilder builder) { builder.startObject(21); } + public static void startRound(FlatBufferBuilder builder) { builder.startObject(22); } public static void addTeamIDs(FlatBufferBuilder builder, int teamIDsOffset) { builder.addOffset(0, teamIDsOffset, 0); } public static int createTeamIDsVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); } public static void startTeamIDsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); } @@ -263,6 +272,9 @@ public static int createRound(FlatBufferBuilder builder, public static void addBytecodesUsed(FlatBufferBuilder builder, int bytecodesUsedOffset) { builder.addOffset(20, bytecodesUsedOffset, 0); } public static int createBytecodesUsedVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); } public static void startBytecodesUsedVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); } + public static void addTeamNumBuffs(FlatBufferBuilder builder, int teamNumBuffsOffset) { builder.addOffset(21, teamNumBuffsOffset, 0); } + public static int createTeamNumBuffsVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); } + public static void startTeamNumBuffsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); } public static int endRound(FlatBufferBuilder builder) { int o = builder.endObject(); return o; diff --git a/schema/ts/battlecode_generated.ts b/schema/ts/battlecode_generated.ts index 128a381c..9e0b103f 100644 --- a/schema/ts/battlecode_generated.ts +++ b/schema/ts/battlecode_generated.ts @@ -46,7 +46,7 @@ export namespace battlecode.schema{ export enum Action{ /** * Politicians self-destruct and affect nearby bodies. - * Target: none + * Target: radius squared */ EMPOWER= 0, @@ -1422,7 +1422,6 @@ static createTeamData(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset } } /** - * Profiler tables * These tables are set-up so that they match closely with speedscope's file format documented at * https://github.com/jlfwong/speedscope/wiki/Importing-from-custom-sources. * The client uses speedscope to show the recorded data in an interactive interface. @@ -2200,11 +2199,31 @@ totalRounds():number { return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0; }; +/** + * Profiler data for team A and B if profiling is enabled. + * + * @param number index + * @param battlecode.schema.ProfilerFile= obj + * @returns battlecode.schema.ProfilerFile + */ +profilerFiles(index: number, obj?:battlecode.schema.ProfilerFile):battlecode.schema.ProfilerFile|null { + var offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? (obj || new battlecode.schema.ProfilerFile).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null; +}; + +/** + * @returns number + */ +profilerFilesLength():number { + var offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; +}; + /** * @param flatbuffers.Builder builder */ static startMatchFooter(builder:flatbuffers.Builder) { - builder.startObject(2); + builder.startObject(3); }; /** @@ -2223,6 +2242,35 @@ static addTotalRounds(builder:flatbuffers.Builder, totalRounds:number) { builder.addFieldInt32(1, totalRounds, 0); }; +/** + * @param flatbuffers.Builder builder + * @param flatbuffers.Offset profilerFilesOffset + */ +static addProfilerFiles(builder:flatbuffers.Builder, profilerFilesOffset:flatbuffers.Offset) { + builder.addFieldOffset(2, profilerFilesOffset, 0); +}; + +/** + * @param flatbuffers.Builder builder + * @param Array. data + * @returns flatbuffers.Offset + */ +static createProfilerFilesVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset { + builder.startVector(4, data.length, 4); + for (var i = data.length - 1; i >= 0; i--) { + builder.addOffset(data[i]); + } + return builder.endVector(); +}; + +/** + * @param flatbuffers.Builder builder + * @param number numElems + */ +static startProfilerFilesVector(builder:flatbuffers.Builder, numElems:number) { + builder.startVector(4, numElems, 4); +}; + /** * @param flatbuffers.Builder builder * @returns flatbuffers.Offset @@ -2232,10 +2280,11 @@ static endMatchFooter(builder:flatbuffers.Builder):flatbuffers.Offset { return offset; }; -static createMatchFooter(builder:flatbuffers.Builder, winner:number, totalRounds:number):flatbuffers.Offset { +static createMatchFooter(builder:flatbuffers.Builder, winner:number, totalRounds:number, profilerFilesOffset:flatbuffers.Offset):flatbuffers.Offset { MatchFooter.startMatchFooter(builder); MatchFooter.addWinner(builder, winner); MatchFooter.addTotalRounds(builder, totalRounds); + MatchFooter.addProfilerFiles(builder, profilerFilesOffset); return MatchFooter.endMatchFooter(builder); } } @@ -2717,11 +2766,38 @@ bytecodesUsedArray():Int32Array|null { return offset ? new Int32Array(this.bb!.bytes().buffer, this.bb!.bytes().byteOffset + this.bb!.__vector(this.bb_pos + offset), this.bb!.__vector_len(this.bb_pos + offset)) : null; }; +/** + * Amount of influence contributing to the teams' buffs. Added at end for backwards compatability. + * + * @param number index + * @returns number + */ +teamNumBuffs(index: number):number|null { + var offset = this.bb!.__offset(this.bb_pos, 46); + return offset ? this.bb!.readInt32(this.bb!.__vector(this.bb_pos + offset) + index * 4) : 0; +}; + +/** + * @returns number + */ +teamNumBuffsLength():number { + var offset = this.bb!.__offset(this.bb_pos, 46); + return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; +}; + +/** + * @returns Int32Array + */ +teamNumBuffsArray():Int32Array|null { + var offset = this.bb!.__offset(this.bb_pos, 46); + return offset ? new Int32Array(this.bb!.bytes().buffer, this.bb!.bytes().byteOffset + this.bb!.__vector(this.bb_pos + offset), this.bb!.__vector_len(this.bb_pos + offset)) : null; +}; + /** * @param flatbuffers.Builder builder */ static startRound(builder:flatbuffers.Builder) { - builder.startObject(21); + builder.startObject(22); }; /** @@ -3144,6 +3220,35 @@ static startBytecodesUsedVector(builder:flatbuffers.Builder, numElems:number) { builder.startVector(4, numElems, 4); }; +/** + * @param flatbuffers.Builder builder + * @param flatbuffers.Offset teamNumBuffsOffset + */ +static addTeamNumBuffs(builder:flatbuffers.Builder, teamNumBuffsOffset:flatbuffers.Offset) { + builder.addFieldOffset(21, teamNumBuffsOffset, 0); +}; + +/** + * @param flatbuffers.Builder builder + * @param Array. data + * @returns flatbuffers.Offset + */ +static createTeamNumBuffsVector(builder:flatbuffers.Builder, data:number[] | Uint8Array):flatbuffers.Offset { + builder.startVector(4, data.length, 4); + for (var i = data.length - 1; i >= 0; i--) { + builder.addInt32(data[i]); + } + return builder.endVector(); +}; + +/** + * @param flatbuffers.Builder builder + * @param number numElems + */ +static startTeamNumBuffsVector(builder:flatbuffers.Builder, numElems:number) { + builder.startVector(4, numElems, 4); +}; + /** * @param flatbuffers.Builder builder * @returns flatbuffers.Offset @@ -3153,7 +3258,7 @@ static endRound(builder:flatbuffers.Builder):flatbuffers.Offset { return offset; }; -static createRound(builder:flatbuffers.Builder, teamIDsOffset:flatbuffers.Offset, teamVotesOffset:flatbuffers.Offset, teamBidderIDsOffset:flatbuffers.Offset, movedIDsOffset:flatbuffers.Offset, movedLocsOffset:flatbuffers.Offset, spawnedBodiesOffset:flatbuffers.Offset, diedIDsOffset:flatbuffers.Offset, actionIDsOffset:flatbuffers.Offset, actionsOffset:flatbuffers.Offset, actionTargetsOffset:flatbuffers.Offset, indicatorDotIDsOffset:flatbuffers.Offset, indicatorDotLocsOffset:flatbuffers.Offset, indicatorDotRGBsOffset:flatbuffers.Offset, indicatorLineIDsOffset:flatbuffers.Offset, indicatorLineStartLocsOffset:flatbuffers.Offset, indicatorLineEndLocsOffset:flatbuffers.Offset, indicatorLineRGBsOffset:flatbuffers.Offset, logsOffset:flatbuffers.Offset, roundID:number, bytecodeIDsOffset:flatbuffers.Offset, bytecodesUsedOffset:flatbuffers.Offset):flatbuffers.Offset { +static createRound(builder:flatbuffers.Builder, teamIDsOffset:flatbuffers.Offset, teamVotesOffset:flatbuffers.Offset, teamBidderIDsOffset:flatbuffers.Offset, movedIDsOffset:flatbuffers.Offset, movedLocsOffset:flatbuffers.Offset, spawnedBodiesOffset:flatbuffers.Offset, diedIDsOffset:flatbuffers.Offset, actionIDsOffset:flatbuffers.Offset, actionsOffset:flatbuffers.Offset, actionTargetsOffset:flatbuffers.Offset, indicatorDotIDsOffset:flatbuffers.Offset, indicatorDotLocsOffset:flatbuffers.Offset, indicatorDotRGBsOffset:flatbuffers.Offset, indicatorLineIDsOffset:flatbuffers.Offset, indicatorLineStartLocsOffset:flatbuffers.Offset, indicatorLineEndLocsOffset:flatbuffers.Offset, indicatorLineRGBsOffset:flatbuffers.Offset, logsOffset:flatbuffers.Offset, roundID:number, bytecodeIDsOffset:flatbuffers.Offset, bytecodesUsedOffset:flatbuffers.Offset, teamNumBuffsOffset:flatbuffers.Offset):flatbuffers.Offset { Round.startRound(builder); Round.addTeamIDs(builder, teamIDsOffset); Round.addTeamVotes(builder, teamVotesOffset); @@ -3176,6 +3281,7 @@ static createRound(builder:flatbuffers.Builder, teamIDsOffset:flatbuffers.Offset Round.addRoundID(builder, roundID); Round.addBytecodeIDs(builder, bytecodeIDsOffset); Round.addBytecodesUsed(builder, bytecodesUsedOffset); + Round.addTeamNumBuffs(builder, teamNumBuffsOffset); return Round.endRound(builder); } } diff --git a/specs/specs.md.html b/specs/specs.md.html index 301304fc..8fe2ed50 100644 --- a/specs/specs.md.html +++ b/specs/specs.md.html @@ -16,7 +16,7 @@ **Battlecode: Campaign** *The formal specification of the Battlecode 2021 game.* - Current version: 2021.1.0.0 + Current version: 2021.3.0.5 Welcome to Battlecode 2021: Campaign. This is a high-level overview of this year's game. @@ -55,7 +55,7 @@ and lead your party to victory. In each Campaign battle, your robots will face an opponent party of robots on the game map. -The game is turn-based, taking place over 3000 rounds: +The game is turn-based, taking place over 1500 rounds: in every round, each robot takes one turn, in order of creation. Each robot receives limited computation time per turn, as described in the [Bytecode Limits](#bytecodelimits) section. @@ -96,7 +96,7 @@ The map also defines the locations of the starting units. At the beginning of a match, each team will own between 1 and 3 Enlightenment Centers. These buildings serve as the foundation of your army, and are where your new politicians will be educated. -There may also be a number of neutral Enlightenment Centers scattered on the map, +There may also be up to 6 neutral Enlightenment Centers scattered on the map, which your team may wish to acquire. In order to prevent maps from favoring one player over another, @@ -109,6 +109,8 @@ Influence is not a global resource: your team's influence is distributed among your Enlightenment Centers, and is generated passively both by the Enlightenment Centers and by specific robot types. +Each robot has a hard limit of $10^8$ influence: any influence in excessive of this will be permanently lost. + ## Overview: Votes The objective of **Battlecode: Campaign** is to win the most votes. @@ -143,6 +145,7 @@ You are able to create units by transferring part of your influence to the new unit. The influence you spend is an integer parameter $C$, which you may choose for each new unit you create. +Newly built politicians and muckrakers will have a cooldown of 10 rounds. The **conviction** of a unit describes how loyal it is to your party; by transferring more influence, @@ -157,17 +160,18 @@ | **Influence** | Cannot be created | $C$ (variable) | $C$ (variable) | $C$ (variable) | | **Minimum influence** | N/A | 1 | 1 | 1 | | **Initial conviction**[^1] | = current influence | $\lceil1.0C\rceil$ | $\lceil1.0C\rceil$ | $\lceil0.7C\rceil$ | +| **Initial cooldown** | 0 | 10 | 0 | 10 | | **Base action cooldown** | 2.0 | 1.0 | 2.0 | 1.5 | | **Action radius squared** | 2 | 9 | 0 | 12 | | **Sensor radius squared** | 40 | 25 | 20 | 30 | | **Detection radius squared** | 40 | 25 | 20 | 40 | | **True sense** | Yes | No | No | Yes | -| **Ability** * | Bid | Empower | Embezzle, Camouflage | Expose | -| **Bytecode limit** | 12,000 | 6,000 | 3,000 | 9,000 | -* an advanced mechanic; see below +| **Ability**[^2] | Bid | Empower | Embezzle, Camouflage | Expose | +| **Bytecode limit** | 20,000 | 15,000 | 7,500 | 15,000 | [^1] The $\lceil\cdot\rceil$ denotes the *ceiling* function, which you may read more about [here](https://en.wikipedia.org/wiki/Floor_and_ceiling_functions). +[^2] an advanced mechanic; see below ## Note: radius squared @@ -223,11 +227,10 @@ if the politician has less than 10 conviction, the speech will not affect other robots at all. - If there are $n$ nearby robots, then the remaining conviction will be divided into $n$ equal parts. - If it cannot be equally divided, - the extra conviction will be distributed with priority given first to later creation, - with ties broken by smaller robot ID. - Each friendly unit will gain conviction, capped at the unit's initial conviction. + Any buffs from Muckrakers **will** be applied here. - Each friendly building will gain conviction. + Friendly buildings **do not receive** buffs from Muckrakers. - Each non-friendly (enemy or neutral) robot will lose conviction. If its conviction becomes negative, then: - Politicians will be **converted** to your team, @@ -235,26 +238,32 @@ capped at the robot's initial conviction. - Slanderers and muckrakers will be destroyed. - Buildings will be **converted** to your team, - with conviction equal to the absolute value of the difference. + although the excess conviction **does not receive** any buffs from Muckrakers. - Unused conviction (i.e. conviction lost due to conviction caps) will be lost forever, with echoes of the speech carried away by the Martian wind. +Robot flags may be reset if the robot changes team. +If the robot rejoins your team later, you are **not guaranteed** that it will have the same ID, +and it **will be possible** that a new copy of your code will be created to control the robot +(so your old controller is erased). + ### Slanderers Slanderers advance the party's political agenda by spreading falsehoods, generating influence for the Enlightenment Center that had created it. - **Sensing**: will see other slanderers to be politicians. -- **Embezzle (passive ability)**: For 50 turns after being created, +- **Embezzle (passive ability)**: Suppose the slanderer has $x$ influence. + For 50 turns after being created, as long as both it and the Enlightenment Center that created it are friendly, that Enlightenment Center passively receives - $\lfloor 0.05\times\text{slanderer influence}\rfloor$ influence per round [^2]. + $\left\lfloor\left(\frac{1}{50}+0.03e^{-0.001x}\right)\cdot x\right\rfloor$ influence per round [^3]. - **Camouflage (passive ability)**: 300 rounds after being created, the slanderer's claims fade from citizens' memory, and it transforms into a politician of equal conviction. You have no control over the Embezzle and Camouflage abilities of Slanderers, as these occur passively. -[^2] The $\lfloor\cdot\rfloor$ denotes the *floor* function, which you may read more about +[^3] The $\lfloor\cdot\rfloor$ denotes the *floor* function, which you may read more about [here](https://en.wikipedia.org/wiki/Floor_and_ceiling_functions). ### Muckrakers @@ -265,9 +274,9 @@ - **Expose (active ability)**: Targets an enemy slanderer, exposing its lies and destroying it. For the next 50 turns, all speeches made by the muckraker's team will have a multiplicative factor of - $1.01^\text{slanderer's influence}$ applied to the total conviction of the speech, - before the 10 units of conviction are deducted. - If multiple slanderers are exposed, these factors are combined multiplicatively. + $1+0.001\cdot(\text{slanderer's influence})$ applied to the total conviction of the speech, + after the 10 units of conviction are deducted. + If multiple slanderers are exposed, the total slanderer influence accumulates. ### Enlightenment Centers @@ -278,10 +287,11 @@ - Either neutral or owned by a team. - Enlightenment Centers initially belonging to a team will have 150 influence. - Neutral enlightenment centers have a predetermined amount of influence between 50 and 500. -- **Build robot**: Spawn a unit of a specified type on an adjacent square, +- **Build robot (active ability)**: Spawn a unit of a specified type on an adjacent square, transferring part of the Enlightenment Center's influence to the newly spawned unit. -- **Bid (active ability)**: Bid influence for a vote. +- **Bid**: Bid influence for a vote. This is described in more detail in the [Victory](#victory) section below. + This action does not affect or depend on the robot's cooldown. - When affiliated with a team, passively generate influence per turn, where the amount generated is a function $f(t)$ of the current round number $t$. $$ f(t)=\left\lceil0.2\sqrt{t}\right\rceil $$ @@ -358,10 +368,10 @@ The per-turn bytecode limits for various robots are as follows: -- Politician: 6,000 -- Slanderer: 3,000 -- Muckraker: 9,000 -- Enlightenment Center: 12,000 +- Politician: 15,000 +- Slanderer: 7,500 +- Muckraker: 15,000 +- Enlightenment Center: 20,000 Some standard functions such as the math library and sensing functions have fixed bytecode costs, available [here](https://github.com/battlecode/battlecode21/blob/master/engine/src/main/battlecode/instrumenter/bytecode/resources/MethodCosts.txt). @@ -401,7 +411,7 @@ ## Complete documentation -Every function you could possibly use to interact with the game can be found in our [javadocs](about:blank). +Every function you could possibly use to interact with the game can be found in our [javadocs](/javadoc/). # Other restrictions @@ -458,11 +468,124 @@ # Lingering questions and clarifications -If something is unclear, direct your questions to our [Discord](https://discord.gg/N86mxkH) where other people may have the same question. We'll update this spec as the competition progresses. +If something is unclear, direct your questions to our [Discord](https://discord.gg/N86mxkH) where other people may have the same question. +We'll update this spec as the competition progresses. # Changelog -- Version 2021.1.0.0 (1/4/20) +- Version 2021.3.0.5 (2/2/21) + - Release all maps + +- Version 2021.3.0.4 (1/28/21) + - Update client stats display + +- Version 2021.3.0.3 (1/28/21) + - Client (thanks to a PR from Team California Roll) + - Display team income + - Represent conviction with unit size + +- Version 2021.3.0.2 (1/23/21) + - Fix client buff display + +- Version 2021.3.0.1 (1/22/21) + - No changes: redeploy was needed for technical reasons + +- Version 2021.3.0.0 (1/22/21) + - Muckraker buff changed from exponential to linear (1st order Taylor expansion) + - Buff has no effect on friendly enlightnment centers + - Empowering now gets taxed before buff is applied + +- Version 2021.2.4.3 (1/20/21) + - Make new maps visible in Client + - Fix bid tracking + +- Version 2021.2.4.2 (1/20/21) + - Release Sprint 2 maps + - Client + - Allow upload of .map21 files in map editor + - Track max bids (losing bids are only tracked for new replays) + +- Version 2021.2.4.1 (1/16/21) + - Client + - Track more info (buffs will only work for new replays) + - Add profiler (thanks to a PR from Team camel_case) + +- Version 2021.2.4.0 (1/16/21) + - Interface change: + - `senseNearbyRobots` and `detectNearbyRobots` no longer give any guarantees on return order. + - Map update: + - All map neutral ECs are now legal. + - Exclude map Cow from scrimmages as it is too large (you can keep it for testing if you want). + - Bug fix: + - Fix issue where EC conviction and influence become unequal after big flip. + +- Version 2021.2.3.0 (1/13/21) + - Reduce number of rounds per game (3000 -> 1500) + - Upload maps from Sprint Tournament 1 + +- Version 2021.2.2.0 (1/12/21) + - Limit maximum robot influence to $10^8$, and supply `GameConstants.ROBOT_INFLUENCE_LIMIT`. + +- Version 2021.2.1.2 (1/11/21) + - Client + - Track more information + - Small visual changes + +- Version 2021.2.1.1 (1/11/21) + - Properly include the engine sources with the deployment + +- Version 2021.2.1.0 (1/11/21) + - Add command-line option to disable all indicator dots. + Can be achieved by adding `-Dbc.engine.show-indicators=false` to the `build.gradle` file. + +- Version 2021.2.0.3 (1/10/21) + - Client + - Revamp map editor (thanks to a PR from Team Double J) + - Performance improvements + - Terraformed Mars + +- Version 2021.2.0.2 (1/8/21) + - Fix incorrect muckraker initial cooldown (15 -> 10) + +- Version 2021.2.0.1 (1/8/21) + - Finish incomplete release + +- Version 2021.2.0.0 (1/8/21) + - Breaking changes: + - Change slanderer influence function from linear to exponential decay, preventing exponential growth + - Remove `GameConstants.MAX_ROBOT_ID` + (cannot guarantee reasonable upper bound as new IDs are generated when robots change teams) + - Major game spec changes: + - Newly built politicians and muckrakers have an action cooldown of 10 rounds + - Guarantee no more than 6 neutral Enlightenment Centers + - Other adjusted constants + - Muckraker buff exponentiation base decreased from 1.01 to 1.001 + - Significantly increased bytecode limits + - New features: + - Add `RobotController::expose(int id)` + - New maps for experimentation + - Add option to disable logs processing in client + - Bug fixes + - Fix reset of non-fatal damage to units + - Minor client bug fixes + +- Version 2021.1.0.3 (1/5/21) + - Spec clarifications: + - Clarify cooldowns for Enlightenment Center abilities + - Clarify what happens on robot team conversion + - Client bugfixes + - Corrected influence and conviction tracking + - Handles neutral enlightenment centers + - Fixed animations and various crashes during game playback + +- Version 2021.1.0.2 (1/4/21) + - Registered maptestsmall in client + +- Version 2021.1.0.1 (1/4/21) + - Fixed map extensions in client, scaffold + - Implemented enlightenment centers can get flags of all bots + +- Version 2021.1.0.0 (1/4/21) - Initial release