Commit 7002fbe7 authored by talljosh's avatar talljosh
Browse files

Merging to include changes from default

--HG--
branch : unstable
### Summary
A quick summary of the bug.
### Steps to reproduce
Steps someone can follow to reproduce the issue.
### What is the current *bug* behavior?
Describe what actually happens.
### What is the expected *correct* behavior?
Describe what you should see instead.
### Relevant logs and/or screenshots
```
Paste any relevant logs or output here.
```
### Possible fixes
If you have any idea which part of the code is responsible for the problem, mention it here.
/label ~bug
Use this template to start a question for developers, or a discussion about Trosnoth.
/label ~discussion
### Quick summary
Summarise your proposal.
### Why it would be valuable
Explain why this would be a good addition to the game.
Convince us that your idea isn't overpowered. It shouldn't confer too much advantage to one player or team, leaving enemies with no way to counter it.
### Proposal
Explain the details of your proposal.
/label ~feature
......@@ -43,4 +43,6 @@ b8ec4e7d08cf78b4cc5e2525af33e329c90ebdd2 release-1.15.0
b2227749d81939e5b2661145f4d80954371b1514 release-1.15.0
b2227749d81939e5b2661145f4d80954371b1514 release-1.15.0
47aed55d78f5bc50d3cd43a038d5c721167a1bee release-1.15.0
33c18b7117f874979c32b8c69bfc06b992bddbd1 release-1.15.1
fb8e756f33d8d399be936cacdd9c0d21157e8dc6 release-1.15.2
5777855d496c7f3862fcb639dfe27ea1358e941e release-1.16.0a1
......@@ -19,9 +19,10 @@
<inspection_tool class="PyPackageRequirementsInspection" enabled="false" level="WARNING" enabled_by_default="false">
<option name="ignoredPackages">
<value>
<list size="2">
<list size="3">
<item index="0" class="java.lang.String" itemvalue="PySide2" />
<item index="1" class="java.lang.String" itemvalue="contextvars" />
<item index="2" class="java.lang.String" itemvalue="asyncqt" />
</list>
</value>
</option>
......@@ -66,6 +67,7 @@
<option value="trosnoth.model.universe.UIOptions.frontLine" />
<option value="trosnoth.model.universe.UIOptions.winning_player_ids" />
<option value="trosnoth.model.universe.UIOptions.winning_team_ids" />
<option value="dataclasses" />
</list>
</option>
</inspection_tool>
......
......@@ -10,7 +10,7 @@ import sys
from PyInstaller.building.api import PYZ, EXE
from PyInstaller.building.build_main import Analysis
from PyInstaller.utils.hooks import (
collect_submodules, get_module_file_attribute,
collect_submodules, get_module_file_attribute, collect_dynamic_libs,
)
......@@ -160,11 +160,11 @@ def get_django_bits(spec_path):
return result
def build_exe(script_name, datas=(), hiddenimports=(), excludes=(), exe_name=None, console=True):
def build_exe(script_name, datas=(), hiddenimports=(), excludes=(), exe_name=None, console=True, binaries=()):
a = Analysis(
['scripts/' + script_name],
pathex=[base_dir],
binaries=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
......@@ -244,10 +244,15 @@ def get_support_bits(spec_path):
def get_trosnoth_app_bits(spec_path):
if os.name == 'nt':
binaries = [(b, '.') for b, _ in collect_dynamic_libs('pygame')]
else:
binaries = ()
return build_exe(
'trosnoth',
# Avoid name collision with trosnoth folder
exe_name='trosnoth' if os.name == 'nt' else 'trosnothapp',
binaries=binaries,
datas=[
('trosnoth/data/achievements/*.png', 'trosnoth/data/achievements'),
('trosnoth/data/config/*.cfg', 'trosnoth/data/config'),
......
v1.15.2
* Fixed bug where changes to the keyboard mapping would not apply until the player left and rejoined the game.
* Updated codebase to work with Pygame 2, and fixed associated glitches.
* Enable auto-scaling when running on ultra-high-resolution monitors.
v1.15.1
* Added a server option for requiring all players to select ‘Ready’ before starting a game instead of 70% of players. This is useful for tournament matches.
* Added a server option to select what kind of bot is used for balance bot and in Humans vs. Machines games.
* If no bot type is selected for a Humans vs. Machines game, SilverBot is now used for Trosnoth matches played on a Regulation 1v1 map.
* The list of available games now only allows 2 seconds to resolve DNS names for trusted servers. This means discovered LAN servers often appear sooner.
* Moved ‘Regulation Standard’ map type to the top of the selection list whenever it’s available.
* Fixed server bug where the location of replays was not correctly stored and displayed.
* Fixed bug where missing stationary points in the pathfinding database could result in server and clients getting out of sync.
* Fixed concurrency issue when displaying messages like ‘Joining game…’ which sometimes resulted in the wrong message being briefly shown.
v1.15.0
* Added new welcome screens developed using Qt so that they can be edited in Qt Designer.
* Added an easy way to host a LAN game, and select the game options.
......
pygame
twisted
twisted>=15.0
simplejson
msgpack
PySide2
......
......@@ -44,6 +44,9 @@ if __name__ == '__main__':
'twisted>=15.0',
'simplejson',
'msgpack',
'PySide2',
'asyncqt',
'cryptography',
],
classifiers = [
......
......@@ -70,7 +70,7 @@ class ServerSideBotProcessManager:
self.joiningAgents.append((agentHasJoined, fromLevel))
await self.amp.callRemote(
StartBot, name=aiName, fromLevel=fromLevel, nick=nick,
StartBot, name=aiName, fromLevel=fromLevel, nick=nick or '',
teamId=team.id if team else NEUTRAL_TEAM_ID,
authTag=authTag, difficulty=difficulty,
)
......
......@@ -41,7 +41,7 @@ LOOK_AHEAD_TICKS = 20
MIN_EVASION_TICKS = 5
def listAIs(showAll=False):
def get_available_bot_names(show_all=False):
'''
Returns a list of strings of the available AI classes.
'''
......@@ -60,14 +60,19 @@ def listAIs(showAll=False):
except Exception as e:
log.warning('Error loading AI %r: %s', aiName, e)
continue
if hasattr(c, 'BotClass') and (c.BotClass.generic or showAll):
if hasattr(c, 'BotClass') and (c.BotClass.generic or show_all):
results.append(aiName)
return results
def makeAIAgent(game, aiName, fromLevel=False, nick=None, difficulty=DEFAULT_BOT_DIFFICULTY):
c = __import__('trosnoth.bots.%s' % (aiName,), fromlist=['BotClass'])
return AIAgent(game, c.BotClass, fromLevel=fromLevel, nick=nick, difficulty=difficulty)
def get_bot_class(bot_name):
c = __import__(f'trosnoth.bots.{bot_name}', fromlist=['BotClass'])
return c.BotClass
def makeAIAgent(game, bot_name, fromLevel=False, nick=None, difficulty=DEFAULT_BOT_DIFFICULTY):
bot_class = get_bot_class(bot_name)
return AIAgent(game, bot_class, fromLevel=fromLevel, nick=nick, difficulty=difficulty)
class AIAgent(ConcreteAgent):
......
......@@ -8,9 +8,8 @@ from trosnoth.bots.goalsetter import (
from trosnoth.bots.orders import FollowPlayer
from trosnoth.const import (
BOT_GOAL_HUNT_RABBITS, BOT_GOAL_SCORE_TROSBALL_POINT, BOT_GOAL_CAPTURE_MAP,
BOT_GOAL_KILL_THINGS, TROSBALL_TARGET_ROOM,
BOT_GOAL_KILL_THINGS, TROSBALL_TARGET_ROOM, BOT_GOAL_COWARDLY_CAPTURE,
)
from trosnoth.messages import BuyUpgradeMsg
from trosnoth.model import upgrades
from trosnoth.model.map import consequence_of_room_capture
from trosnoth.model.team import Team
......@@ -50,6 +49,8 @@ class ReachCurrentObjective(Goal):
self.setSubGoal(HuntTheRabbits(self.bot, self))
elif botGoal == BOT_GOAL_CAPTURE_MAP:
self.setSubGoal(WinStandardTrosnothGame(self.bot, self))
elif botGoal == BOT_GOAL_COWARDLY_CAPTURE:
self.setSubGoal(CaptureZonesButDoNotDie(self.bot, self))
elif botGoal == BOT_GOAL_KILL_THINGS:
self.setSubGoal(HuntEnemies(self.bot, self))
else:
......@@ -539,6 +540,109 @@ class WinStandardTrosnothGame(Goal):
return utility
class CaptureZonesButDoNotDie(Goal):
'''
This bot goal is used for villagers in the space vampire game mode.
Bots with this goal are cautious, but do capture zones if it's safe.
'''
def __init__(self, bot, parent):
super().__init__(bot, parent)
self.next_check = None
def start(self):
self.bot.set_dodges_bullets(True)
self.bot.onOrderFinished.addListener(self.order_finished)
super().start()
self.schedule_next_check()
def stop(self):
self.cancel_next_check()
self.bot.onOrderFinished.removeListener(self.order_finished)
super().stop()
def schedule_next_check(self):
self.cancel_next_check()
delay = 3 + random.random()
self.next_check = self.bot.world.callLater(delay, self.reevaluate)
def cancel_next_check(self):
if self.next_check:
self.next_check.cancel()
self.next_check = None
def order_finished(self):
self.reevaluate()
def reevaluate(self):
self.cancel_next_check()
my_zone = self.bot.player.getZone()
if self.bot.player.dead:
respawn_room = my_zone
if not respawn_room:
respawn_room = random.choice(list(self.bot.world.rooms))
self.setSubGoal(RespawnNearZone(self.bot, self, respawn_room))
return
if not my_zone:
from trosnoth.bots.silver import MoveIntoMap
self.setSubGoal(MoveIntoMap(self.bot, self))
elif self.in_scary_zone():
if random.random() > .3:
self.leave_zone(nearest=True)
else:
self.leave_zone()
elif self.enemies_are_in_zone():
self.setSubGoal(KillEnemyInZone(self.bot, self))
elif my_zone.owner == self.bot.player.team:
self.leave_zone()
elif my_zone.isCapturableBy(self.bot.player.team):
self.setSubGoal(None)
self.bot.moveToOrb(my_zone)
else:
self.setSubGoal(None)
self.bot.standStill()
self.schedule_next_check()
def in_scary_zone(self):
me = self.bot.player
here = me.getZone()
friendly_hitpoints = sum(
p.health for p in here.players if me.isFriendsWith(p) and not p.dead)
enemy_hitpoints = sum(
p.health for p in here.players if not me.isFriendsWith(p) and not p.dead)
return 1.5 * enemy_hitpoints >= friendly_hitpoints
def leave_zone(self, nearest=False):
me = self.bot.player
here = me.getZone()
neighbours = list(here.getUnblockedNeighbours())
if not neighbours:
target = here
elif nearest:
target = min(neighbours, key=lambda room: distance(room.centre, me.pos))
else:
choices = [z for z in neighbours if z.owner != me.team]
if not choices or random.random() < .3:
choices = neighbours
target = random.choice(choices)
self.setSubGoal(None)
self.bot.moveToOrb(target)
def enemies_are_in_zone(self):
me = self.bot.player
here = me.getZone()
return any(not me.isFriendsWith(p) and not p.dead for p in here.players)
class Explode(Goal):
def start(self):
self.bot.onTick.addListener(self.tick)
......
......@@ -116,6 +116,7 @@ BONUS_COINS_PER_5_SECONDS = 10
BONUS_COINS_FOR_TROSBALL_SCORE = 300
BONUS_COINS_FOR_RABBIT_SURVIVAL = 500
DEATH_TAX_RATE = 0.35
DEATH_TAX_FREE_THRESHOLD = 250
DEATH_MAX_COIN_COUNT = 5
......@@ -165,6 +166,7 @@ BOT_GOAL_CAPTURE_MAP = 'cap'
BOT_GOAL_HUNT_RABBITS = 'hunt'
BOT_GOAL_SCORE_TROSBALL_POINT = 'ball'
BOT_GOAL_KILL_THINGS = 'kill'
BOT_GOAL_COWARDLY_CAPTURE = 'sneak'
ACHIEVEMENT_TACTICAL = 'tactical'
......
This diff is collapsed.
import functools
from django import forms
from trosnoth.bots.base import get_available_bot_names, get_bot_class
from trosnoth.const import BOT_DIFFICULTY_EASY, BOT_DIFFICULTY_MEDIUM, BOT_DIFFICULTY_HARD
from trosnoth.djangoapp.models import (
TrosnothServerSettings, TrosnothUser, TrosnothArena,
......@@ -9,7 +12,7 @@ from trosnoth.levels.maps import (
StandardMap, SmallMap, WideMap, CorridorMap, LargeMap,
SmallStackMap, SmallRingMap, LargeRingsMap, LabyrinthMap,
)
from trosnoth.server.arena import LEVELS
from trosnoth.levels.registry import scenario_class_by_code, available_scenario_classes
class ServerSettingsForm(forms.ModelForm):
......@@ -74,6 +77,15 @@ class ManagePlayerForm(forms.ModelForm):
}
@functools.lru_cache()
def get_bot_kind_choices():
result = [('', '(Automatic)')]
for bot_name in get_available_bot_names():
bot_class = get_bot_class(bot_name)
result.append((bot_name, bot_class.nick))
return tuple(result)
class ArenaModelForm(forms.ModelForm):
balance_bot_difficulty = forms.TypedChoiceField(
empty_value=None,
......@@ -98,14 +110,24 @@ class ArenaModelForm(forms.ModelForm):
label='Humans vs. Machines bot difficulty',
)
balance_bot_kind = forms.ChoiceField(choices=get_bot_kind_choices(), required=False)
machines_bot_kind = forms.ChoiceField(
choices=get_bot_kind_choices(),
required=False,
label='Humans vs. Machines bot kind',
)
class Meta:
model = TrosnothArena
fields = (
'name',
'enabled',
'autoStartCountDown',
'require_everyone_ready',
'balance_bot_difficulty',
'balance_bot_kind',
'machines_difficulty',
'machines_bot_kind',
'force_half_width',
'force_height',
'force_duration',
......@@ -161,20 +183,10 @@ class SelectLevelForm(forms.Form):
map_object.code: 1
for map_object in level_class.map_selection
},
} for code, level_class in LEVELS.items()
} for code, level_class in scenario_class_by_code.items()
}
scenario = forms.ChoiceField(
choices=(
('standard', 'Trosnoth match'),
('trosball', 'Trosball'),
('catpigeon', 'Cat among pigeons'),
('freeforall', 'Free for all'),
('hunted', 'Hunted'),
('orbchase', 'Orb chase'),
('elephantking', 'Elephant king'),
('defencedrill', 'Defence drill (red team cannot cap)'),
('positioningdrill', 'Positioning drill (no shooting)'),
),
choices=((c.level_code, c.levelName) for c in available_scenario_classes),
widget=forms.Select(attrs={
'onchange': "update_fields();",
}),
......
# Generated by Django 2.2.12 on 2021-01-26 11:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('trosnoth', '0029_auto_20200821_0142'),
]
operations = [
migrations.AddField(
model_name='trosnotharena',
name='require_everyone_ready',
field=models.BooleanField(default=False, help_text='If enabled, a new game will only start when the countdown runs out, or every player selects ready. If disabled, only 70% of players need to select ready.'),
),
]
# Generated by Django 2.2.12 on 2021-01-27 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('trosnoth', '0030_trosnotharena_require_everyone_ready'),
]
operations = [
migrations.AddField(
model_name='trosnotharena',
name='balance_bot_kind',
field=models.TextField(default=''),
),
migrations.AddField(
model_name='trosnotharena',
name='machines_bot_kind',
field=models.TextField(default=''),
),
]
......@@ -89,11 +89,20 @@ class TrosnothArena(models.Model):
'to disable)',
default=90,
)
require_everyone_ready = models.BooleanField(
default=False,
help_text='If enabled, a new game will only start when the '
'countdown runs out, or every player selects ready. '
'If disabled, only 70% of players need to select '
'ready.',
)
balance_bot_difficulty = models.IntegerField(
null=True, blank=True, verbose_name='Difficulty for BalanceBot',
default=BOT_DIFFICULTY_EASY)
machines_difficulty = models.IntegerField(
default=DEFAULT_BOT_DIFFICULTY, verbose_name='Bot difficulty for Humans vs. Machines')
balance_bot_kind = models.TextField(default='')
machines_bot_kind = models.TextField(default='')
force_half_width = models.IntegerField(
null=True, blank=True, verbose_name='Forced map half width (blank disables)')
force_height = models.IntegerField(
......
......@@ -111,7 +111,7 @@ class Game(object):
player.agent = agent
agent.setPlayer(player)
player.joinComplete = True
player.join_complete = True
player.onJoinComplete()
def playerRemoved(self, player, playerId):
......@@ -282,7 +282,12 @@ class LocalGame(Game):
# that all the messages arrive at the clients in the same order
# as the server, we queue them and release them as soon as the
# first message is completely sent.
msg.tracePoint(self, '(sendServerCommand: deferred)')
# NOTE: if the deffering of messages causes issues, you can
# decorate functions/methods with trosnoth.utils.aio.…
# delay_so_messages_will_apply_immediately.
msg.tracePoint(
self, f'(sendServerCommand: deferred from {self._serverCommandStack[0]})')
return
while self._serverCommandStack:
......
......@@ -131,7 +131,7 @@ class AchievementManager(object):
@defer.inlineCallbacks
def playerAdded(self, player):
if self.world.isServer and not player.joinComplete:
if not player.join_complete:
# We want to identify by user rather than nickname if the user
# is authenticated.
yield player.onJoinComplete.wait()
......
......@@ -302,7 +302,7 @@ class StatKeeper(object):
@defer.inlineCallbacks
def playerAdded(self, player):
if self.world.isServer and not player.joinComplete:
if not player.join_complete:
# We want to identify by user rather than nickname if the user
# is authenticated.
yield player.onJoinComplete.wait()
......
......@@ -188,6 +188,8 @@ class PygameRunnerAPI(Protocol):
coroutine: Callable[[], Awaitable[T]],
size: Optional[Tuple[int, int]] = None,
full_screen: Optional[bool] = None,
scaled: Optional[bool] = None,
*,
target_frame_rate: float = 60,
hide_qt: bool = True,
) -> T:
......@@ -208,6 +210,9 @@ class PygameRunnerAPI(Protocol):
will be shown in full-screen mode. Note that if full_screen
is True and size is not a supported full-screen size, a
suitable size will be automatically chosen.
:param scaled: If provided, controls whether the window will be
scaled to be visible on higher resolution monitors. May have
no effect if full_screen is True.
:param target_frame_rate: This value controls the length of
delay between each frame of the event loop. Expressed in
units of frames per second.
......@@ -222,7 +227,12 @@ class PygameRunnerAPI(Protocol):
raise NotImplementedError
def resize_window(
self, window_size: Tuple[int, int], full_screen: Optional[bool] = None) -> None:
self,
window_size: Tuple[int, int],
full_screen: Optional[bool] = None,
scaled: Optional[bool] = None,
from_event=False,
) -> None:
'''
Resizes the currently showing Pygame window.
......@@ -230,6 +240,10 @@ class PygameRunnerAPI(Protocol):
:param full_screen: If provided, controls whether the window
will be shown in full-screen mode. If not provided, the
current value will be used.
:param scaled: If provided, controls whether the window will be
scaled to be visible on higher resolution monutors.
:param from_event: Should be True iff this call is in response
to a VIDEORESIZE event.
:raises:
PygameWindowNotOpen: if the Pygame window is not open
'''
......@@ -238,9 +252,10 @@ class PygameRunnerAPI(Protocol):
def launch_application(
self,
coroutine: Callable[[], Awaitable[T]],
*,
size: Tuple[int, int],
full_screen: bool,
scaled: bool,
*,
target_frame_rate: float = 60,