Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d02589ed1 | |||
| 49156bcda4 | |||
| 1e3c2b176d | |||
| f004205069 | |||
| fa42cec01c | |||
| 39e943b25e | |||
| f8454d8950 | |||
| 284a45ece1 | |||
| e0ee552012 | |||
| e5a4b6a1c9 | |||
|
|
16ff0a9497 |
3
DDApp/.gitignore
vendored
Normal file
3
DDApp/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.idea
|
||||||
|
bin
|
||||||
|
obj
|
||||||
16
DDApp/DDApp.sln
Normal file
16
DDApp/DDApp.sln
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDApplication", "DDApplication\DDApplication.csproj", "{6519A0C8-BC8F-490D-B429-C2460DCB272C}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{6519A0C8-BC8F-490D-B429-C2460DCB272C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6519A0C8-BC8F-490D-B429-C2460DCB272C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6519A0C8-BC8F-490D-B429-C2460DCB272C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6519A0C8-BC8F-490D-B429-C2460DCB272C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
10
DDApp/DDApplication/App.axaml
Normal file
10
DDApp/DDApplication/App.axaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="DDApplication.App"
|
||||||
|
RequestedThemeVariant="Default">
|
||||||
|
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||||
|
|
||||||
|
<Application.Styles>
|
||||||
|
<FluentTheme />
|
||||||
|
</Application.Styles>
|
||||||
|
</Application>
|
||||||
23
DDApp/DDApplication/App.axaml.cs
Normal file
23
DDApp/DDApplication/App.axaml.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace DDApplication;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted()
|
||||||
|
{
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
desktop.MainWindow = new MainWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
250
DDApp/DDApplication/BotSessionLogger.cs
Normal file
250
DDApp/DDApplication/BotSessionLogger.cs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace DDApplication;
|
||||||
|
|
||||||
|
internal sealed class BotSessionLogger : IDisposable
|
||||||
|
{
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private readonly DateTimeOffset _startedAt = DateTimeOffset.UtcNow;
|
||||||
|
private readonly Dictionary<string, int> _actionAttempts = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly Dictionary<string, int> _actionSuccess = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private StreamWriter? _eventsWriter;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public string SessionId { get; } = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||||
|
public string DirectoryPath { get; }
|
||||||
|
public string EventsPath { get; }
|
||||||
|
public string SummaryCsvPath { get; }
|
||||||
|
public string LatestSymlinkPath { get; }
|
||||||
|
public string LatestSummarySymlinkPath { get; }
|
||||||
|
|
||||||
|
public BotSessionLogger()
|
||||||
|
{
|
||||||
|
var baseDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
DirectoryPath = Path.Combine(baseDir, "DDApplication", "logs");
|
||||||
|
Directory.CreateDirectory(DirectoryPath);
|
||||||
|
|
||||||
|
var stamp = DateTimeOffset.UtcNow.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
|
||||||
|
EventsPath = Path.Combine(DirectoryPath, $"bot_{stamp}_{SessionId}.events.csv");
|
||||||
|
SummaryCsvPath = Path.Combine(DirectoryPath, $"bot_{stamp}_{SessionId}.summary.csv");
|
||||||
|
LatestSymlinkPath = Path.Combine(DirectoryPath, "latest.log.csv");
|
||||||
|
LatestSummarySymlinkPath = Path.Combine(DirectoryPath, "latest-summary.log.csv");
|
||||||
|
|
||||||
|
_eventsWriter = new StreamWriter(EventsPath, append: false, Encoding.UTF8) { AutoFlush = true };
|
||||||
|
_eventsWriter.WriteLine("timestamp_utc,event,label,executed,hp,max_hp,kills,deaths,pos_x,pos_y,pressure,details");
|
||||||
|
UpdateLatestSymlink(LatestSymlinkPath, EventsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogDecision(
|
||||||
|
string label,
|
||||||
|
bool executed,
|
||||||
|
double hp,
|
||||||
|
double maxHp,
|
||||||
|
int kills,
|
||||||
|
int deaths,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
bool pressure,
|
||||||
|
string details)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed || _eventsWriter is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Increment(_actionAttempts, label);
|
||||||
|
if (executed)
|
||||||
|
{
|
||||||
|
Increment(_actionSuccess, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventsWriter.WriteLine(string.Join(",",
|
||||||
|
Csv(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)),
|
||||||
|
Csv("decision"),
|
||||||
|
Csv(label),
|
||||||
|
Csv(executed ? "1" : "0"),
|
||||||
|
Csv(hp.ToString("0.00", CultureInfo.InvariantCulture)),
|
||||||
|
Csv(maxHp.ToString("0.00", CultureInfo.InvariantCulture)),
|
||||||
|
Csv(kills.ToString(CultureInfo.InvariantCulture)),
|
||||||
|
Csv(deaths.ToString(CultureInfo.InvariantCulture)),
|
||||||
|
Csv(x.ToString(CultureInfo.InvariantCulture)),
|
||||||
|
Csv(y.ToString(CultureInfo.InvariantCulture)),
|
||||||
|
Csv(pressure ? "1" : "0"),
|
||||||
|
Csv(details)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogTelemetry(
|
||||||
|
double hp,
|
||||||
|
double maxHp,
|
||||||
|
int kills,
|
||||||
|
int deaths,
|
||||||
|
double progress,
|
||||||
|
double remaining,
|
||||||
|
string level,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
string details)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed || _eventsWriter is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventsWriter.WriteLine(string.Join(",",
|
||||||
|
Csv(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)),
|
||||||
|
Csv("telemetry"),
|
||||||
|
Csv(level),
|
||||||
|
Csv(""),
|
||||||
|
Csv(hp.ToString("0.00", CultureInfo.InvariantCulture)),
|
||||||
|
Csv(maxHp.ToString("0.00", CultureInfo.InvariantCulture)),
|
||||||
|
Csv(kills.ToString(CultureInfo.InvariantCulture)),
|
||||||
|
Csv(deaths.ToString(CultureInfo.InvariantCulture)),
|
||||||
|
Csv(x.ToString(CultureInfo.InvariantCulture)),
|
||||||
|
Csv(y.ToString(CultureInfo.InvariantCulture)),
|
||||||
|
Csv(""),
|
||||||
|
Csv($"progress={progress:0.00};remaining={remaining:0.00};{details}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogEvent(string label, string details)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed || _eventsWriter is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventsWriter.WriteLine(string.Join(",",
|
||||||
|
Csv(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)),
|
||||||
|
Csv("event"),
|
||||||
|
Csv(label),
|
||||||
|
Csv(""),
|
||||||
|
Csv(""),
|
||||||
|
Csv(""),
|
||||||
|
Csv(""),
|
||||||
|
Csv(""),
|
||||||
|
Csv(""),
|
||||||
|
Csv(""),
|
||||||
|
Csv(""),
|
||||||
|
Csv(details)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteSummaryAndDispose(double finalHp, double finalMaxHp, int finalKills, int finalDeaths)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsed = DateTimeOffset.UtcNow - _startedAt;
|
||||||
|
var minutes = Math.Max(elapsed.TotalMinutes, 0.01);
|
||||||
|
var kd = finalDeaths == 0 ? finalKills : (double)finalKills / finalDeaths;
|
||||||
|
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
$"session={SessionId}",
|
||||||
|
$"started_utc={_startedAt:O}",
|
||||||
|
$"ended_utc={DateTimeOffset.UtcNow:O}",
|
||||||
|
$"duration_minutes={minutes:0.00}",
|
||||||
|
$"kills={finalKills}",
|
||||||
|
$"deaths={finalDeaths}",
|
||||||
|
$"kd={kd:0.000}",
|
||||||
|
$"kills_per_min={(finalKills / minutes):0.000}",
|
||||||
|
$"deaths_per_min={(finalDeaths / minutes):0.000}",
|
||||||
|
$"final_hp={finalHp:0.00}/{finalMaxHp:0.00}",
|
||||||
|
"actions="
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var attempt in _actionAttempts.OrderBy(x => x.Key))
|
||||||
|
{
|
||||||
|
_actionSuccess.TryGetValue(attempt.Key, out var success);
|
||||||
|
var rate = attempt.Value == 0 ? 0 : (double)success / attempt.Value;
|
||||||
|
lines.Add($" {attempt.Key}: attempts={attempt.Value}, success={success}, rate={rate:0.000}");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var csvWriter = new StreamWriter(SummaryCsvPath, append: false, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
csvWriter.WriteLine("type,key,value");
|
||||||
|
csvWriter.WriteLine($"{Csv("metric")},{Csv("session")},{Csv(SessionId)}");
|
||||||
|
csvWriter.WriteLine($"{Csv("metric")},{Csv("started_utc")},{Csv(_startedAt.ToString("O", CultureInfo.InvariantCulture))}");
|
||||||
|
csvWriter.WriteLine($"{Csv("metric")},{Csv("ended_utc")},{Csv(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture))}");
|
||||||
|
csvWriter.WriteLine($"{Csv("metric")},{Csv("duration_minutes")},{Csv(minutes.ToString("0.00", CultureInfo.InvariantCulture))}");
|
||||||
|
csvWriter.WriteLine($"{Csv("metric")},{Csv("kills")},{Csv(finalKills.ToString(CultureInfo.InvariantCulture))}");
|
||||||
|
csvWriter.WriteLine($"{Csv("metric")},{Csv("deaths")},{Csv(finalDeaths.ToString(CultureInfo.InvariantCulture))}");
|
||||||
|
csvWriter.WriteLine($"{Csv("metric")},{Csv("kd")},{Csv(kd.ToString("0.000", CultureInfo.InvariantCulture))}");
|
||||||
|
csvWriter.WriteLine($"{Csv("metric")},{Csv("kills_per_min")},{Csv((finalKills / minutes).ToString("0.000", CultureInfo.InvariantCulture))}");
|
||||||
|
csvWriter.WriteLine($"{Csv("metric")},{Csv("deaths_per_min")},{Csv((finalDeaths / minutes).ToString("0.000", CultureInfo.InvariantCulture))}");
|
||||||
|
csvWriter.WriteLine($"{Csv("metric")},{Csv("final_hp")},{Csv($"{finalHp:0.00}/{finalMaxHp:0.00}")}");
|
||||||
|
|
||||||
|
foreach (var attempt in _actionAttempts.OrderBy(x => x.Key))
|
||||||
|
{
|
||||||
|
_actionSuccess.TryGetValue(attempt.Key, out var success);
|
||||||
|
var rate = attempt.Value == 0 ? 0 : (double)success / attempt.Value;
|
||||||
|
csvWriter.WriteLine($"{Csv("action")},{Csv(attempt.Key + ".attempts")},{Csv(attempt.Value.ToString(CultureInfo.InvariantCulture))}");
|
||||||
|
csvWriter.WriteLine($"{Csv("action")},{Csv(attempt.Key + ".success")},{Csv(success.ToString(CultureInfo.InvariantCulture))}");
|
||||||
|
csvWriter.WriteLine($"{Csv("action")},{Csv(attempt.Key + ".rate")},{Csv(rate.ToString("0.000", CultureInfo.InvariantCulture))}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateLatestSymlink(LatestSummarySymlinkPath, SummaryCsvPath);
|
||||||
|
|
||||||
|
_eventsWriter?.Dispose();
|
||||||
|
_eventsWriter = null;
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
WriteSummaryAndDispose(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Increment(Dictionary<string, int> dict, string key)
|
||||||
|
{
|
||||||
|
dict.TryGetValue(key, out var current);
|
||||||
|
dict[key] = current + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Csv(string value)
|
||||||
|
{
|
||||||
|
var safe = value.Replace("\"", "\"\"");
|
||||||
|
return $"\"{safe}\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateLatestSymlink(string symlinkPath, string targetPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(symlinkPath) || Directory.Exists(symlinkPath))
|
||||||
|
{
|
||||||
|
File.Delete(symlinkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.CreateSymbolicLink(symlinkPath, targetPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best-effort only; logging should continue even if symlink creation fails.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
30
DDApp/DDApplication/DDApplication.csproj
Normal file
30
DDApp/DDApplication/DDApplication.csproj
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
<UserSecretsId>c93b84d8-b4e9-4725-84e3-00a10f74073f</UserSecretsId>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
|
||||||
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12" />
|
||||||
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.12">
|
||||||
|
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||||
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
|
<PackageReference Include="NSwag.ApiDescription.Client" Version="14.6.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<OpenApiReference Include="DigitalDojoApi.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
1253
DDApp/DDApplication/DigitalDojoApi.json
Normal file
1253
DDApp/DDApplication/DigitalDojoApi.json
Normal file
File diff suppressed because it is too large
Load Diff
62
DDApp/DDApplication/MainWindow.axaml
Normal file
62
DDApp/DDApplication/MainWindow.axaml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="1320" d:DesignHeight="860"
|
||||||
|
Width="1320" Height="860"
|
||||||
|
x:Class="DDApplication.MainWindow"
|
||||||
|
Title="Digital Dojo Bot Controller">
|
||||||
|
<Grid RowDefinitions="Auto,Auto,*,Auto" ColumnDefinitions="2*,2*,3*" Margin="12">
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button x:Name="StartButton" Content="Start Bot" Width="120"/>
|
||||||
|
<Button x:Name="StopButton" Content="Stop Bot" Width="120" IsEnabled="False"/>
|
||||||
|
<CheckBox x:Name="MultiplayerCheckBox" Content="Multiplayer" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Aggro:" VerticalAlignment="Center"/>
|
||||||
|
<ComboBox x:Name="AggressivenessProfileBox" Width="150" SelectedIndex="2">
|
||||||
|
<ComboBoxItem Content="Conservative"/>
|
||||||
|
<ComboBoxItem Content="Balanced"/>
|
||||||
|
<ComboBoxItem Content="Aggressive"/>
|
||||||
|
<ComboBoxItem Content="Extreme"/>
|
||||||
|
</ComboBox>
|
||||||
|
<TextBlock Text="Mode Profile:" VerticalAlignment="Center"/>
|
||||||
|
<ComboBox x:Name="ModeOptimizationProfileBox" Width="200" SelectedIndex="0">
|
||||||
|
<ComboBoxItem Content="Auto"/>
|
||||||
|
<ComboBoxItem Content="Singleplayer Optimized"/>
|
||||||
|
<ComboBoxItem Content="Multiplayer Optimized"/>
|
||||||
|
</ComboBox>
|
||||||
|
<TextBlock Text="API Key:" VerticalAlignment="Center"/>
|
||||||
|
<TextBox x:Name="ApiKeyBox" Width="360" Watermark="GUID player token"/>
|
||||||
|
<TextBlock x:Name="StatusText" VerticalAlignment="Center" Text="Idle"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Margin="0,8,0,8" Padding="8" BorderThickness="1" CornerRadius="6">
|
||||||
|
<SelectableTextBlock x:Name="StatsText" TextWrapping="Wrap" Text="Waiting for game state..." Focusable="True"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Row="2" Grid.Column="0" Margin="0,0,8,0" Padding="8" BorderThickness="1" CornerRadius="6">
|
||||||
|
<Grid RowDefinitions="Auto,*" RowSpacing="6">
|
||||||
|
<TextBlock Text="Bot Knowledge" FontWeight="Bold"/>
|
||||||
|
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
|
||||||
|
<UniformGrid x:Name="KnowledgeGrid" Rows="21" Columns="21"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Row="2" Grid.Column="1" Margin="0,0,8,0" Padding="8" BorderThickness="1" CornerRadius="6">
|
||||||
|
<Grid RowDefinitions="Auto,Auto,*" RowSpacing="8">
|
||||||
|
<TextBlock Text="Decision + Cooldowns" FontWeight="Bold"/>
|
||||||
|
<TextBlock x:Name="DecisionText" Grid.Row="1" TextWrapping="Wrap" Text="No decision yet."/>
|
||||||
|
<TextBlock x:Name="CooldownText" Grid.Row="2" TextWrapping="Wrap"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Row="2" Grid.Column="2" Padding="8" BorderThickness="1" CornerRadius="6">
|
||||||
|
<Grid RowDefinitions="Auto,*" RowSpacing="6">
|
||||||
|
<TextBlock Text="Action Log" FontWeight="Bold"/>
|
||||||
|
<ListBox x:Name="LogList" Grid.Row="1"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" Margin="0,8,0,0" Text="Legend: P=bot, N=scan-nearest, *=high threat, +=medium threat, .=low threat" />
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
1645
DDApp/DDApplication/MainWindow.axaml.cs
Normal file
1645
DDApp/DDApplication/MainWindow.axaml.cs
Normal file
File diff suppressed because it is too large
Load Diff
30
DDApp/DDApplication/Program.cs
Normal file
30
DDApp/DDApplication/Program.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace DDApplication;
|
||||||
|
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
public static IConfiguration Configuration { get; private set; } = new ConfigurationBuilder()
|
||||||
|
.AddJsonFile("appsettings.json", optional: true)
|
||||||
|
.AddUserSecrets<Program>(optional: true)
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||||
|
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||||
|
// yet and stuff might break.
|
||||||
|
[STAThread]
|
||||||
|
public static int Main(string[] args)
|
||||||
|
{
|
||||||
|
return BuildAvaloniaApp()
|
||||||
|
.StartWithClassicDesktopLifetime(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
|
private static AppBuilder BuildAvaloniaApp()
|
||||||
|
=> AppBuilder.Configure<App>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
.WithInterFont()
|
||||||
|
.LogToTrace();
|
||||||
|
}
|
||||||
48
DDApp/DDApplication/README.md
Normal file
48
DDApp/DDApplication/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Digital Dojo Bot Controller
|
||||||
|
|
||||||
|
This Avalonia app runs an autonomous bot for the Digital Dojo singleplayer API and visualizes both current game state and inferred enemy knowledge.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
- Starts/uses your current game session (`/api/game/{key}/create`, `/status`)
|
||||||
|
- Runs a cooldown-aware combat loop (`move`, `hit`, `shoot`, `specialattack`, `radar`, `scan`, `peek`, `dash`)
|
||||||
|
- Tries to maximize score by prioritizing fast damage actions when enemies are nearby
|
||||||
|
- Shows live stats (kills/deaths, HP, level progress, remaining time)
|
||||||
|
- Shows a local knowledge map:
|
||||||
|
- `P` = estimated player position
|
||||||
|
- `N` = nearest enemy from latest scan
|
||||||
|
- `* + .` = inferred enemy threat intensity
|
||||||
|
- Shows cooldown timers and action reasoning/logs
|
||||||
|
- Writes per-session analytics logs for post-run analysis
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Set your API key (GUID) in one of these ways:
|
||||||
|
|
||||||
|
1. Environment variable: `APIKEY`
|
||||||
|
2. User secrets (`Program` already loads user secrets)
|
||||||
|
3. Paste it directly into the UI text box before clicking **Start Bot**
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project /home/tikaiz/RiderProjects/digitalDojo/DDApp/DDApplication/DDApplication.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Position and map are inferred from your own movement + sensor calls (not an authoritative server map).
|
||||||
|
- The strategy is level-aware:
|
||||||
|
- `Default Level`: balanced combat with short escape commitments
|
||||||
|
- `Bigger Map Level`: more scouting and stronger anti-loop exploration
|
||||||
|
- `More Bots Level`: safer special-attack usage and tighter targeting
|
||||||
|
- `New Bots Level`: more ranged pressure and less chasing of teleporting targets
|
||||||
|
- The strategy is heuristic and tunable; it is designed for high kill throughput while respecting API cooldowns.
|
||||||
|
- Session logs are written to `~/.local/share/DDApplication/logs` on Linux:
|
||||||
|
- `*.events.csv` contains timestamped telemetry, decisions, and events
|
||||||
|
- `*.summary.csv` contains machine-readable summary metrics and action rates
|
||||||
|
- `latest-summary.log.csv` is a symlink that auto-points to the newest `*.summary.csv`
|
||||||
|
- `latest.log` is a symlink that auto-points to the newest `*.events.csv` session log
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
18
DDApp/DDApplication/app.manifest
Normal file
18
DDApp/DDApplication/app.manifest
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<!-- This manifest is used on Windows only.
|
||||||
|
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||||
|
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="DDApplication.Desktop"/>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- A list of the Windows versions that this application has been tested on
|
||||||
|
and is designed to work with. Uncomment the appropriate elements
|
||||||
|
and Windows will automatically select the most compatible environment. -->
|
||||||
|
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
338
DDApp/DDApplication/levelübersicht.txt
Normal file
338
DDApp/DDApplication/levelübersicht.txt
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
Digital Dojo <https://dd.countit.at/> Menü //
|
||||||
|
|
||||||
|
* //Coden lernen<https://dd.countit.at/programmieren-lernen>
|
||||||
|
o //Programmier-Roadmap <https://dd.countit.at/programmieren-lernen>
|
||||||
|
o //Programmierschule <https://dd.countit.at/programmierschule>
|
||||||
|
o //Übungsräume <https://dd.countit.at/dojos>
|
||||||
|
o //Katas <https://dd.countit.at/katas>
|
||||||
|
o //Tutorials <https://dd.countit.at/tutorials>
|
||||||
|
* //Dojo Game<https://dd.countit.at/dojo-game/multiplayer-modus>
|
||||||
|
o //Besiege unseren Bot <https://dd.countit.at/dojo-game>
|
||||||
|
o //Spieleinstieg <https://dd.countit.at/dojo-game/spieleinstieg>
|
||||||
|
o //Levels & Bots <https://dd.countit.at/dojo-game/levels>
|
||||||
|
o //Leaderboard <https://dd.countit.at/dojo-game/leaderboard>
|
||||||
|
o //Multiplayer Modus <https://dd.countit.at/dojo-game/
|
||||||
|
multiplayer-modus>
|
||||||
|
o //Tipps <https://dd.countit.at/dojo-game/tipps>
|
||||||
|
* //Programmier-Challenge <https://dd.countit.at/programmierwettbewerb>
|
||||||
|
* //Bewerben <https://dd.countit.at/bewerben>
|
||||||
|
* //Feedback <https://dd.countit.at/feedback>
|
||||||
|
* // <#>
|
||||||
|
Ausloggen
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
//Teilen //Facebook <https://www.facebook.com/sharer/sharer.php?
|
||||||
|
u=https%3A%2F%2Fdd.countit.at%2Fdojo-game%2Flevels> //Instagram
|
||||||
|
<https://www.instagram.com/count_it_group/> //LinkedIn <https://
|
||||||
|
www.linkedin.com/shareArticle?
|
||||||
|
mini=true&url=https%3A%2F%2Fdd.countit.at%2Fdojo-
|
||||||
|
game%2Flevels&title=&summary=&source=> //E-Mail
|
||||||
|
<mailto:m.mustermann@countit.at?subject=Lese-
|
||||||
|
Tipp%3A%20Dojo%20Game%20%7C%20%C3%9Cbersicht%20der%20Levels%20%26%20Bots&body=Hey%2C%0D%0A%0D%0Aich%20habe%20auf%20https%3A%2F%2Fkarriere.countit.at%20einen%20Beitrag%20gefunden%2C%20der%20dich%20interessieren%20k%C3%B6nnte%3A%0D%0A%22Dojo%20Game%20%7C%20%C3%9Cbersicht%20der%20Levels%20%26%20Bots%22%0D%0ASchau%20ihn%20dir%20mal%20an%3A%20https%3A%2F%2Fkarriere.countit.at%2Fdojo-game%2Flevels%3Fkaid%3Dsharemail%0D%0A%0D%0ABeste%20Gr%C3%BC%C3%9Fe>
|
||||||
|
|
||||||
|
|
||||||
|
Übersicht der Levels & Bots
|
||||||
|
|
||||||
|
|
||||||
|
Dojo Game
|
||||||
|
|
||||||
|
|
||||||
|
//Dojo Game | Levelübersicht
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Im Folgenden findest du wichtige Infos und Eckdaten zu den einzelnen
|
||||||
|
Levels, welche im Spiel implementiert sind. Bestimmte Informationen
|
||||||
|
können sich unter den einzelnen Levels unterscheiden, daher ist es
|
||||||
|
wichtig, in deinem Algorithmus auf diese Attribute einzugehen.
|
||||||
|
Einen Einblick in die verschiedenen Bot-Typen findest du hier. <https://
|
||||||
|
dd.countit.at/dojo-game/levels#bots>
|
||||||
|
|
||||||
|
|
||||||
|
//1. Default Level
|
||||||
|
|
||||||
|
Das Default Level ist das erste Level und gleichzeitig auch das
|
||||||
|
Einsteiger-Level im Dojo Game.
|
||||||
|
|
||||||
|
Während der Laufzeit des Spiels wird jeder Gegner und auch dein Ritter,
|
||||||
|
sobald dieser getötet wird, sofort an einer anderen Stelle des
|
||||||
|
Spielfelds neu geboren. Nach Ablauf der Zeit wird das Spiel automatisch
|
||||||
|
beendet und die Kills und Deaths dieses Spiels dem Leaderboard
|
||||||
|
hinzugefügt. Danach kann direkt ein neues Spiel erstellt werden.
|
||||||
|
Generell ist es möglich, mehrere Gegner auf einmal zu schlagen. Stehen 2
|
||||||
|
Spieler auf demselben Block oder ineinander, so wird beiden gleich viel
|
||||||
|
Schaden zugefügt.
|
||||||
|
|
||||||
|
*Spielfeldgröße:* 15x15
|
||||||
|
|
||||||
|
*Bots:* (4) Default Bot x2, Tank Bot x2
|
||||||
|
|
||||||
|
*Dauer des Spiels:* erfolgreicher Abschluss des Levels oder 15 Minuten
|
||||||
|
|
||||||
|
*Erfolgreicher Abschluss:* 20 Kills
|
||||||
|
|
||||||
|
*Cooldowns:* Move = 250, Hit = 250, Radar = 250, SpecialAttack = 5000,
|
||||||
|
Peek = 1000, Scan = 2000, Dash = 3000, Teleport = 20000, Shoot = 1000
|
||||||
|
|
||||||
|
|
||||||
|
//2. Bigger Map Level
|
||||||
|
|
||||||
|
Im 2. Level des Games vergrößert sich die Spielfläche und die Default
|
||||||
|
Bots werden mehr!
|
||||||
|
|
||||||
|
Dieses Level wird nach 15 Minuten beendet. Wenn du nach Ablauf der Zeit
|
||||||
|
100% Progress hast, wirst du ins nächste Level weitergeleitet.
|
||||||
|
|
||||||
|
Während der Laufzeit des Spiels wird jeder Gegner und auch dein Ritter,
|
||||||
|
sobald dieser getötet wird, sofort an einer anderen Stelle des
|
||||||
|
Spielfelds neu geboren. Nach Ablauf der Zeit wird das Spiel automatisch
|
||||||
|
beendet und die Kills und Deaths dieses Spiels dem Leaderboard
|
||||||
|
hinzugefügt. Danach kann direkt ein neues Spiel erstellt werden.
|
||||||
|
Generell ist es möglich, mehrere Gegner auf einmal zu schlagen. Stehen 2
|
||||||
|
Spieler auf demselben Block oder ineinander, so wird beiden gleich viel
|
||||||
|
Schaden zugefügt.
|
||||||
|
|
||||||
|
*Spielfeldgröße:* 25x25
|
||||||
|
|
||||||
|
*Bots:* (5) Default Bot x5
|
||||||
|
|
||||||
|
*Dauer des Spiels:* 15 Minuten
|
||||||
|
|
||||||
|
*Erfolgreicher Abschluss:* 5 Kills
|
||||||
|
|
||||||
|
Cooldowns: Move = 250, Hit = 250, Radar = 250, SpecialAttack = 5000,
|
||||||
|
Peek = 1000, Scan = 2000, Dash = 3000, Teleport = 20000, Shoot = 1000
|
||||||
|
|
||||||
|
|
||||||
|
//3. More Bots Level
|
||||||
|
|
||||||
|
In diesem Level kämpfst du gegen 6 Tank Bots - pass auf, der Radius der
|
||||||
|
Specialattack ist in diesem Level von 3 Blöcken auf 1 Block geschrumpft!
|
||||||
|
|
||||||
|
Während der Laufzeit des Spiels wird jeder Gegner und auch dein Ritter,
|
||||||
|
sobald dieser getötet wird, sofort an einer anderen Stelle des
|
||||||
|
Spielfelds neu geboren. Nach Ablauf der Zeit wird das Spiel automatisch
|
||||||
|
beendet und die Kills und Deaths dieses Spiels dem Leaderboard
|
||||||
|
hinzugefügt. Danach kann direkt ein neues Spiel erstellt werden.
|
||||||
|
Generell ist es möglich, mehrere Gegner auf einmal zu schlagen. Stehen 2
|
||||||
|
Spieler auf demselben Block oder ineinander, so wird beiden gleich viel
|
||||||
|
Schaden zugefügt.
|
||||||
|
|
||||||
|
*Spielfeldgröße:* 25x25
|
||||||
|
|
||||||
|
*Bots:* (6) Tank Bot x6
|
||||||
|
|
||||||
|
*Dauer des Spiels: *15 Minuten oder erfolgreicher Abschluss des Levels
|
||||||
|
|
||||||
|
*Erfolgreicher Abschluss:* 10 Kills
|
||||||
|
|
||||||
|
Cooldowns: Move = 250, Hit = 250, Radar = 250, SpecialAttack = 5000,
|
||||||
|
Peek = 1000, Scan = 2000, Dash = 3000, Teleport = 20000, Shoot = 1000
|
||||||
|
|
||||||
|
|
||||||
|
//4. New Bots Level
|
||||||
|
|
||||||
|
Hier kämpfst du gegen 5 Psy Bots, welche mit einer neuen Fähigkeit
|
||||||
|
ausgestattet wurden.
|
||||||
|
|
||||||
|
Während der Laufzeit des Spiels wird jeder Gegner und auch dein Ritter,
|
||||||
|
sobald dieser getötet wird, sofort an einer anderen Stelle des
|
||||||
|
Spielfelds neu geboren. Nach Ablauf der Zeit wird das Spiel automatisch
|
||||||
|
beendet und die Kills und Deaths dieses Spiels dem Leaderboard
|
||||||
|
hinzugefügt. Danach kann direkt ein neues Spiel erstellt werden.
|
||||||
|
Generell ist es möglich, mehrere Gegner auf einmal zu schlagen. Stehen 2
|
||||||
|
Spieler auf demselben Block oder ineinander, so wird beiden gleich viel
|
||||||
|
Schaden zugefügt.
|
||||||
|
|
||||||
|
*Spielfeldgröße:* 15x15
|
||||||
|
|
||||||
|
*Bots:* (5) Psy Bot x5
|
||||||
|
|
||||||
|
*Dauer des Spiels: *15 Minuten
|
||||||
|
|
||||||
|
*Erfolgreicher Abschluss:* 10 Kills
|
||||||
|
|
||||||
|
Cooldowns: Move = 250, Hit = 250, Radar = 250, SpecialAttack = 5000,
|
||||||
|
Peek = 1000, Scan = 2000, Dash = 3000, Teleport = 20000, Shoot = 1000
|
||||||
|
|
||||||
|
|
||||||
|
//Übersicht der Bots
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
In den Dojo Game Levels sind verschiedene Bot-Typen vorhanden. Jeder Bot
|
||||||
|
hat eine individuelle Fähigkeit und eigene Attribute, welche ihn
|
||||||
|
einzigartig machen. Hier findest du eine Liste der Bots und ihren Eckdaten.
|
||||||
|
|
||||||
|
|
||||||
|
//The Default Bot
|
||||||
|
|
||||||
|
Der Default Bot hat die selben Rahmenbedingungen wie ein echter Spieler.
|
||||||
|
Dies macht ihn, die Werte betreffend, gleich stark wie einen Spieler.
|
||||||
|
Sie unterscheiden sich nur in deren Implementierung.
|
||||||
|
|
||||||
|
*Lebenspunkte beim Start:* 10.0
|
||||||
|
|
||||||
|
*Maximale Lebenspunkte:* 10.0
|
||||||
|
|
||||||
|
*Einfacher Schaden:* 3
|
||||||
|
|
||||||
|
*Special-Attack:* 3 Blöcke Rundumschlag mit einfachem Schaden
|
||||||
|
|
||||||
|
|
||||||
|
//The Tank Bot
|
||||||
|
|
||||||
|
Der Tank Bot ist ein auf den ersten Blick schwächer erscheinender Bot,
|
||||||
|
welcher aber mit ansteigender Spieldauer immer stärker wird.
|
||||||
|
|
||||||
|
*Lebenspunkte beim Start:* 5.0
|
||||||
|
|
||||||
|
*Maximale Lebenspunkte:* 10.0
|
||||||
|
|
||||||
|
*Einfacher Schaden:* 3
|
||||||
|
|
||||||
|
*Special-Attack:* +1 Lebenspunkt zusätzlich und 3 Blöcke Rundumschlag
|
||||||
|
mit einfachem Schaden
|
||||||
|
|
||||||
|
|
||||||
|
//The Psy Bot
|
||||||
|
|
||||||
|
Der Psy Bot verhält sich gleich wie der Default Bot, jedoch
|
||||||
|
unterscheidet er sich deutlich von diesem durch seinen Special-Attack.
|
||||||
|
Dieser hat nämlich die Fähigkeit, sich beim Erhalten von Schaden oder
|
||||||
|
alle 5 Sekunden seinen Standort zu ändern und sich an eine zufällige
|
||||||
|
Position zu teleportieren.
|
||||||
|
|
||||||
|
*Lebenspunkte beim Start:* 10.0
|
||||||
|
|
||||||
|
*Maximale Lebenspunkte: *10.0
|
||||||
|
|
||||||
|
*Einfacher Schaden:* 3
|
||||||
|
|
||||||
|
*Special-Attack:* Standort ändern an einen zufälligen Ort am Spielfeld
|
||||||
|
|
||||||
|
|
||||||
|
Los geht's mit dem Dojo-Game!
|
||||||
|
|
||||||
|
Erster Platz am Leaderboard? Klingt genial!
|
||||||
|
|
||||||
|
Dojo-Game //
|
||||||
|
<https://dd.countit.at/dojo-game>
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
|
||||||
|
Programmier-Challenge
|
||||||
|
|
||||||
|
Stell dich der Programmier-Challenge und miss dich mit deinen
|
||||||
|
Klassenkolleg*innen.
|
||||||
|
|
||||||
|
Jetzt teilnehmen! // <https://dd.countit.at/programmierwettbewerb>
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
|
||||||
|
Ultimative Coding-Roadmap
|
||||||
|
|
||||||
|
Unsere Roadmap weist dir den Weg zum Coding-Profi!
|
||||||
|
|
||||||
|
Mehr erfahren // <https://dd.countit.at/programmieren-lernen>
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
|
||||||
|
Bewerbung bei COUNT IT
|
||||||
|
|
||||||
|
Starte deine Karriere als Softwareentwickler*in bei COUNT IT.
|
||||||
|
|
||||||
|
Zum Formular! // <https://dd.countit.at/bewerben>
|
||||||
|
|
||||||
|
|
||||||
|
Über Digital Dojo
|
||||||
|
|
||||||
|
Das Digital Dojo ist der virtuelle Übungsraum von COUNT IT.
|
||||||
|
|
||||||
|
Angehende Programmierer*innen, Code-Neulinge, Wiedereinsteiger*innen und
|
||||||
|
Fortgeschrittene finden hier das nötige Rüstzeug für ihre Karriere.
|
||||||
|
|
||||||
|
Du möchtest deine Lehre bei COUNT IT starten? Dann bist du hier richtig
|
||||||
|
- besiege deine Gegner im Dojo Game und sichere dir deine Lehrstelle!
|
||||||
|
|
||||||
|
Inspire your career.
|
||||||
|
|
||||||
|
|
||||||
|
Navigation
|
||||||
|
|
||||||
|
Programmieren lernen <https://dd.countit.at/programmieren-lernen> News
|
||||||
|
<https://dd.countit.at/news>
|
||||||
|
Programmierschule <https://dd.countit.at/programmierschule> Partner
|
||||||
|
<https://dd.countit.at/partner>
|
||||||
|
Programmier-Challenge <https://dd.countit.at/programmierwettbewerb>
|
||||||
|
Dojo Game <https://dd.countit.at/dojo-game>
|
||||||
|
|
||||||
|
|
||||||
|
Datenschutz <https://www.countit.at/datenschutz> Karriere @ COUNT IT
|
||||||
|
<https://karriere.countit.at/>
|
||||||
|
Impressum <https://www.countit.at/impressum> COUNT IT
|
||||||
|
Softwarehouse <https://it.countit.at/>
|
||||||
|
|
||||||
|
|
||||||
|
Newsletter abonnieren
|
||||||
|
|
||||||
|
Der COUNT IT Newsletter liefert viermal jährlich interessante
|
||||||
|
Neuigkeiten über das Unternehmen. Gleich anfordern!
|
||||||
|
|
||||||
|
E-Mail-Adresse
|
||||||
|
Senden
|
||||||
|
|
||||||
|
// <https://de-de.facebook.com/COUNTITGroup/> // <https://
|
||||||
|
www.instagram.com/count_it_group/> // <https://de.linkedin.com/company/
|
||||||
|
count-it-group>
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
COUNT IT Group verwendet Cookies, um die Website bestmöglich an die
|
||||||
|
Bedürfnisse der User anpassen zu können.
|
||||||
|
|
||||||
|
* Google
|
||||||
|
* Microsoft
|
||||||
|
* Ahrefs
|
||||||
|
* Statistik
|
||||||
|
|
||||||
|
Akzeptieren
|
||||||
|
Individuelle EinstellungenIndividuelle Einstellungen ausblenden |
|
||||||
|
Datenschutzerklärung <https://www.countit.at/datenschutz>
|
||||||
|
|
||||||
|
Google
|
||||||
|
|
||||||
|
Google Analytics und Optimize aggregiert Daten über unsere Besucher und
|
||||||
|
ihr Verhalten auf unserer Website. Wir nutzen diese Informationen zur
|
||||||
|
Verbesserung unserer Seite.
|
||||||
|
|
||||||
|
Microsoft
|
||||||
|
|
||||||
|
Microsoft aggregiert Daten über unsere Besucher und ihr Verhalten auf
|
||||||
|
unserer Website. Wir nutzen diese Informationen zur Verbesserung unserer
|
||||||
|
Seite.
|
||||||
|
|
||||||
|
Ahrefs
|
||||||
|
|
||||||
|
Ahrefs aggregiert Daten über unsere Besucher und ihr Verhalten auf
|
||||||
|
unserer Website. Wir nutzen diese Informationen zur Verbesserung unserer
|
||||||
|
Seite.
|
||||||
|
|
||||||
|
Statistik
|
||||||
|
|
||||||
|
Es werden grundlegend notwendige, anonymisierte Userdaten aufgezeichnet,
|
||||||
|
welche nur von uns für Auswertungen verwendet werden!
|
||||||
|
|
||||||
|
Cookie Entscheidung speichern
|
||||||
288
DDApp/DDApplication/multiplayer.txt
Normal file
288
DDApp/DDApplication/multiplayer.txt
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
Digital Dojo <https://dd.countit.at/> Menü //
|
||||||
|
|
||||||
|
* //Coden lernen<https://dd.countit.at/programmieren-lernen>
|
||||||
|
o //Programmier-Roadmap <https://dd.countit.at/programmieren-lernen>
|
||||||
|
o //Programmierschule <https://dd.countit.at/programmierschule>
|
||||||
|
o //Übungsräume <https://dd.countit.at/dojos>
|
||||||
|
o //Katas <https://dd.countit.at/katas>
|
||||||
|
o //Tutorials <https://dd.countit.at/tutorials>
|
||||||
|
* //Dojo Game<https://dd.countit.at/dojo-game/multiplayer-modus>
|
||||||
|
o //Besiege unseren Bot <https://dd.countit.at/dojo-game>
|
||||||
|
o //Spieleinstieg <https://dd.countit.at/dojo-game/spieleinstieg>
|
||||||
|
o //Levels & Bots <https://dd.countit.at/dojo-game/levels>
|
||||||
|
o //Leaderboard <https://dd.countit.at/dojo-game/leaderboard>
|
||||||
|
o //Multiplayer Modus <https://dd.countit.at/dojo-game/
|
||||||
|
multiplayer-modus>
|
||||||
|
o //Tipps <https://dd.countit.at/dojo-game/tipps>
|
||||||
|
* //Programmier-Challenge <https://dd.countit.at/programmierwettbewerb>
|
||||||
|
* //Bewerben <https://dd.countit.at/bewerben>
|
||||||
|
* //Feedback <https://dd.countit.at/feedback>
|
||||||
|
* // <#>
|
||||||
|
Ausloggen
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
//Teilen //Facebook <https://www.facebook.com/sharer/sharer.php?
|
||||||
|
u=https%3A%2F%2Fdd.countit.at%2Fdojo-game%2Fmultiplayer-modus> //
|
||||||
|
Instagram <https://www.instagram.com/count_it_group/> //LinkedIn
|
||||||
|
<https://www.linkedin.com/shareArticle?
|
||||||
|
mini=true&url=https%3A%2F%2Fdd.countit.at%2Fdojo-game%2Fmultiplayer-
|
||||||
|
modus&title=&summary=&source=> //E-Mail <mailto:m.mustermann@countit.at?
|
||||||
|
subject=Lese-
|
||||||
|
Tipp%3A%20Dojo%20Game%20%7C%20Multiplayer%20Modus&body=Hey%2C%0D%0A%0D%0Aich%20habe%20auf%20https%3A%2F%2Fkarriere.countit.at%20einen%20Beitrag%20gefunden%2C%20der%20dich%20interessieren%20k%C3%B6nnte%3A%0D%0A%22Dojo%20Game%20%7C%20Multiplayer%20Modus%22%0D%0ASchau%20ihn%20dir%20mal%20an%3A%20https%3A%2F%2Fkarriere.countit.at%2Fdojo-game%2Fmultiplayer-modus%3Fkaid%3Dsharemail%0D%0A%0D%0ABeste%20Gr%C3%BC%C3%9Fe>
|
||||||
|
|
||||||
|
|
||||||
|
Der Multiplayer Modus
|
||||||
|
|
||||||
|
|
||||||
|
Dojo Game
|
||||||
|
|
||||||
|
|
||||||
|
//Multiplayer Modus
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Der Mutiplayer Modus basiert auf einem *Lobby System*. Erstelle eine
|
||||||
|
Lobby, lade Freunde ein oder spiele mit anderen Usern! Alle
|
||||||
|
Möglichkeiten stehen dir offen.
|
||||||
|
|
||||||
|
Desto mehr Spieler in einer Lobby sind, mit welchen du zuvor noch nicht
|
||||||
|
viel gespielt hast, desto mehr Punkte werden dir für eine gute
|
||||||
|
Platzierung gutgeschrieben. Der Mehrspieler Modus funktioniert mit den
|
||||||
|
selben API-Aufrufen <https://dd.countit.at/dojo-game/spieleinstieg#api>
|
||||||
|
wie der Singleplayer - es werden lediglich Bots durch Spieler ersetzt.
|
||||||
|
Du kannst daher deinen Einzelspieler Code auch für Mehrspieler Matches
|
||||||
|
verwenden.
|
||||||
|
|
||||||
|
|
||||||
|
*Viel Spaß!*
|
||||||
|
|
||||||
|
|
||||||
|
// Lobby Manager
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
Lobby-Key
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
//
|
||||||
|
|
||||||
|
//Starten <https://dd.countit.at/lobby/start>
|
||||||
|
//Verlassen <https://dd.countit.at/lobby/leave>
|
||||||
|
//Schließen <https://dd.countit.at/lobby/close>
|
||||||
|
// tikaiz1 ADMIN
|
||||||
|
// Bot
|
||||||
|
// Bot
|
||||||
|
// Bot
|
||||||
|
// Bot
|
||||||
|
// Bot
|
||||||
|
|
||||||
|
|
||||||
|
// Dein derzeitiger Spielstand
|
||||||
|
|
||||||
|
|
||||||
|
Deine Tötungen: *21*
|
||||||
|
|
||||||
|
Deine Tode: *43*
|
||||||
|
|
||||||
|
Dein Leben: *(7/10)*
|
||||||
|
|
||||||
|
|
||||||
|
// Server Status
|
||||||
|
|
||||||
|
Server - Adresse: game-dd.countit.at
|
||||||
|
|
||||||
|
Port: 80 (optional)
|
||||||
|
|
||||||
|
Status: Online
|
||||||
|
|
||||||
|
|
||||||
|
//Wichtige Informationen
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
//Der Start
|
||||||
|
|
||||||
|
Um eine Lobby zu starten, müssen mindestens die minimale Anzahl an
|
||||||
|
Spielern in einer Lobby sein - standardmäßig sind dies 2. Wir das Spiel
|
||||||
|
dann gestartet, wird die Lobby aus der Liste entfernt, da diese dann
|
||||||
|
nicht mehr benötigt wird. Spielern wird die eigene Lobby nach dem Start
|
||||||
|
nicht mehr angezeigt.
|
||||||
|
|
||||||
|
|
||||||
|
//Dein Algorithmus
|
||||||
|
|
||||||
|
Dein Algorithmus kann schon laufen bevor deine Lobby gestartet wurde.
|
||||||
|
Dein Code wird, sobald eine Verbindung zu einem Spiel hergestellt wurde,
|
||||||
|
automatisch Befehle ausführen.
|
||||||
|
*Vorsicht:* Du darfst deinen Singleplayer Code nicht 1:1 übernehmen.
|
||||||
|
Wenn du ein Lobby Game spielst, darfst du zuvor keine Singleplayer Game
|
||||||
|
Instanz erstellen.
|
||||||
|
|
||||||
|
|
||||||
|
//Dein Spieler
|
||||||
|
|
||||||
|
Dein Spieler ist im Multiplayer mit den selben Stärken und Fähigkeiten
|
||||||
|
ausgestattet wie im Singleplayer. In diesem Modus sind alle Spieler
|
||||||
|
gleich stark - es gibt keine unterschiedlichen Klassen, denn es geht
|
||||||
|
darum, wer den besten Code hat.
|
||||||
|
|
||||||
|
|
||||||
|
//Rangliste
|
||||||
|
|
||||||
|
Unsere Rangliste wird durch Kills und an der Vielzahl der verschiedenen
|
||||||
|
Gegner in einem Match berechnet. Spielst du gegen neue Gegner oder
|
||||||
|
andere Spieler, mit welchen du noch nicht all zu viel gespielt hast,
|
||||||
|
bekommst du mehr Punkte als wenn du beispielsweise immer mit denselben 3
|
||||||
|
Freunden spielst.
|
||||||
|
|
||||||
|
|
||||||
|
//Public/Private Lobby
|
||||||
|
|
||||||
|
Eine öffentliche Lobby ist für jeden Nutzer zugänglich und kann mit
|
||||||
|
jedem Spieler gespielt werden. Eine geschlossene und private Lobby ist
|
||||||
|
nur für jene Spieler zugänglich, welche von einem bereits in der Lobby
|
||||||
|
vorhandenen Spieler den Lobby-Key o.a. Lobby-ID erhalten haben. Beim
|
||||||
|
Eintreten in eine private Lobby wird nach diesem Key verlangt.
|
||||||
|
|
||||||
|
|
||||||
|
//Lobby Admin
|
||||||
|
|
||||||
|
Standardmäßig ist der Ersteller einer Lobby der Admin für die derzeitige
|
||||||
|
Sitzung. Verlässt dieser Nutzer die Lobby, wird durch ein Zufallsprinzip
|
||||||
|
ein neuer Admin ausgewählt. Das Spiel kann nur vom Lobby-Admin
|
||||||
|
geschlossen oder gestartet werden.
|
||||||
|
|
||||||
|
|
||||||
|
//Update Changelog
|
||||||
|
|
||||||
|
Es sind seit dem neuesten Update nur 2 Spieler notwendig, um eine Lobby
|
||||||
|
zu starten. Zusätzlich wurde die Interkation mit dem User durch
|
||||||
|
verbesserte Nachrichten und Übersetzungen optimiert.
|
||||||
|
|
||||||
|
|
||||||
|
Übung macht den Meister!
|
||||||
|
|
||||||
|
Du möchtest deine Skills noch ein wenig trainieren? Dann führe ein paar
|
||||||
|
Katas aus!
|
||||||
|
|
||||||
|
Katas //
|
||||||
|
<https://dd.countit.at/katas>
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
|
||||||
|
Dojo - virtueller Übungsraum
|
||||||
|
|
||||||
|
Löse die Dojo-Aufgaben und werde Programmier-Profi!
|
||||||
|
|
||||||
|
Zu den Dojos // <https://dd.countit.at/dojos>
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
|
||||||
|
Ultimative Coding-Roadmap
|
||||||
|
|
||||||
|
Unsere Roadmap weist dir den Weg zum Coding-Profi!
|
||||||
|
|
||||||
|
Mehr erfahren // <https://dd.countit.at/programmieren-lernen>
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
|
||||||
|
Bewerbung bei COUNT IT
|
||||||
|
|
||||||
|
Starte deine Karriere als Softwareentwickler*in bei COUNT IT.
|
||||||
|
|
||||||
|
Zum Formular! // <https://dd.countit.at/bewerben>
|
||||||
|
|
||||||
|
|
||||||
|
Über Digital Dojo
|
||||||
|
|
||||||
|
Das Digital Dojo ist der virtuelle Übungsraum von COUNT IT.
|
||||||
|
|
||||||
|
Angehende Programmierer*innen, Code-Neulinge, Wiedereinsteiger*innen und
|
||||||
|
Fortgeschrittene finden hier das nötige Rüstzeug für ihre Karriere.
|
||||||
|
|
||||||
|
Du möchtest deine Lehre bei COUNT IT starten? Dann bist du hier richtig
|
||||||
|
- besiege deine Gegner im Dojo Game und sichere dir deine Lehrstelle!
|
||||||
|
|
||||||
|
Inspire your career.
|
||||||
|
|
||||||
|
|
||||||
|
Navigation
|
||||||
|
|
||||||
|
Programmieren lernen <https://dd.countit.at/programmieren-lernen> News
|
||||||
|
<https://dd.countit.at/news>
|
||||||
|
Programmierschule <https://dd.countit.at/programmierschule> Partner
|
||||||
|
<https://dd.countit.at/partner>
|
||||||
|
Programmier-Challenge <https://dd.countit.at/programmierwettbewerb>
|
||||||
|
Dojo Game <https://dd.countit.at/dojo-game>
|
||||||
|
|
||||||
|
|
||||||
|
Datenschutz <https://www.countit.at/datenschutz> Karriere @ COUNT IT
|
||||||
|
<https://karriere.countit.at/>
|
||||||
|
Impressum <https://www.countit.at/impressum> COUNT IT
|
||||||
|
Softwarehouse <https://it.countit.at/>
|
||||||
|
|
||||||
|
|
||||||
|
Newsletter abonnieren
|
||||||
|
|
||||||
|
Der COUNT IT Newsletter liefert viermal jährlich interessante
|
||||||
|
Neuigkeiten über das Unternehmen. Gleich anfordern!
|
||||||
|
|
||||||
|
E-Mail-Adresse
|
||||||
|
Senden
|
||||||
|
|
||||||
|
// <https://de-de.facebook.com/COUNTITGroup/> // <https://
|
||||||
|
www.instagram.com/count_it_group/> // <https://de.linkedin.com/company/
|
||||||
|
count-it-group>
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
COUNT IT Group verwendet Cookies, um die Website bestmöglich an die
|
||||||
|
Bedürfnisse der User anpassen zu können.
|
||||||
|
|
||||||
|
* Google
|
||||||
|
* Microsoft
|
||||||
|
* Ahrefs
|
||||||
|
* Statistik
|
||||||
|
|
||||||
|
Akzeptieren
|
||||||
|
Individuelle EinstellungenIndividuelle Einstellungen ausblenden |
|
||||||
|
Datenschutzerklärung <https://www.countit.at/datenschutz>
|
||||||
|
|
||||||
|
Google
|
||||||
|
|
||||||
|
Google Analytics und Optimize aggregiert Daten über unsere Besucher und
|
||||||
|
ihr Verhalten auf unserer Website. Wir nutzen diese Informationen zur
|
||||||
|
Verbesserung unserer Seite.
|
||||||
|
|
||||||
|
Microsoft
|
||||||
|
|
||||||
|
Microsoft aggregiert Daten über unsere Besucher und ihr Verhalten auf
|
||||||
|
unserer Website. Wir nutzen diese Informationen zur Verbesserung unserer
|
||||||
|
Seite.
|
||||||
|
|
||||||
|
Ahrefs
|
||||||
|
|
||||||
|
Ahrefs aggregiert Daten über unsere Besucher und ihr Verhalten auf
|
||||||
|
unserer Website. Wir nutzen diese Informationen zur Verbesserung unserer
|
||||||
|
Seite.
|
||||||
|
|
||||||
|
Statistik
|
||||||
|
|
||||||
|
Es werden grundlegend notwendige, anonymisierte Userdaten aufgezeichnet,
|
||||||
|
welche nur von uns für Auswertungen verwendet werden!
|
||||||
|
|
||||||
|
Cookie Entscheidung speichern
|
||||||
633
DDApp/DDApplication/spieleinstieg.txt
Normal file
633
DDApp/DDApplication/spieleinstieg.txt
Normal file
@@ -0,0 +1,633 @@
|
|||||||
|
Digital Dojo <https://dd.countit.at/> Menü //
|
||||||
|
|
||||||
|
* //Coden lernen<https://dd.countit.at/programmieren-lernen>
|
||||||
|
o //Programmier-Roadmap <https://dd.countit.at/programmieren-lernen>
|
||||||
|
o //Programmierschule <https://dd.countit.at/programmierschule>
|
||||||
|
o //Übungsräume <https://dd.countit.at/dojos>
|
||||||
|
o //Katas <https://dd.countit.at/katas>
|
||||||
|
o //Tutorials <https://dd.countit.at/tutorials>
|
||||||
|
* //Dojo Game<https://dd.countit.at/dojo-game/multiplayer-modus>
|
||||||
|
o //Besiege unseren Bot <https://dd.countit.at/dojo-game>
|
||||||
|
o //Spieleinstieg <https://dd.countit.at/dojo-game/spieleinstieg>
|
||||||
|
o //Levels & Bots <https://dd.countit.at/dojo-game/levels>
|
||||||
|
o //Leaderboard <https://dd.countit.at/dojo-game/leaderboard>
|
||||||
|
o //Multiplayer Modus <https://dd.countit.at/dojo-game/
|
||||||
|
multiplayer-modus>
|
||||||
|
o //Tipps <https://dd.countit.at/dojo-game/tipps>
|
||||||
|
* //Programmier-Challenge <https://dd.countit.at/programmierwettbewerb>
|
||||||
|
* //Bewerben <https://dd.countit.at/bewerben>
|
||||||
|
* //Feedback <https://dd.countit.at/feedback>
|
||||||
|
* // <#>
|
||||||
|
Ausloggen
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
//Teilen //Facebook <https://www.facebook.com/sharer/sharer.php?
|
||||||
|
u=https%3A%2F%2Fdd.countit.at%2Fdojo-game%2Fspieleinstieg> //Instagram
|
||||||
|
<https://www.instagram.com/count_it_group/> //LinkedIn <https://
|
||||||
|
www.linkedin.com/shareArticle?
|
||||||
|
mini=true&url=https%3A%2F%2Fdd.countit.at%2Fdojo-
|
||||||
|
game%2Fspieleinstieg&title=&summary=&source=> //E-Mail
|
||||||
|
<mailto:m.mustermann@countit.at?subject=Lese-
|
||||||
|
Tipp%3A%20Dojo%20Game%20%7C%20Wichtige%20Infos%20zum%20Spieleinstieg&body=Hey%2C%0D%0A%0D%0Aich%20habe%20auf%20https%3A%2F%2Fkarriere.countit.at%20einen%20Beitrag%20gefunden%2C%20der%20dich%20interessieren%20k%C3%B6nnte%3A%0D%0A%22Dojo%20Game%20%7C%20Wichtige%20Infos%20zum%20Spieleinstieg%22%0D%0ASchau%20ihn%20dir%20mal%20an%3A%20https%3A%2F%2Fkarriere.countit.at%2Fdojo-game%2Fspieleinstieg%3Fkaid%3Dsharemail%0D%0A%0D%0ABeste%20Gr%C3%BC%C3%9Fe>
|
||||||
|
|
||||||
|
|
||||||
|
Spieleinstieg
|
||||||
|
|
||||||
|
|
||||||
|
Dojo Game
|
||||||
|
|
||||||
|
|
||||||
|
//Dojo Game | Dein Einstieg ins Spiel
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
In diesem Abschnitt findest du alle notwendigen Infos, um das Dojo Game
|
||||||
|
zu starten und erste Aktionen durchzuführen.
|
||||||
|
|
||||||
|
*Hinweis: *Hol dir deinen persönlichen Zugang <https://dd.countit.at/
|
||||||
|
register> bevor du startest bzw. logge dich ein <https://dd.countit.at/
|
||||||
|
login>. So erscheinst du später auch am Leaderboard!
|
||||||
|
|
||||||
|
|
||||||
|
Inhaltsverzeichnis
|
||||||
|
|
||||||
|
1. Das Spiel <#spiel>
|
||||||
|
2. Vorbereitung <#vorbereitung>
|
||||||
|
3. API-Schlüssel <#api>
|
||||||
|
4. Spiel starten & beenden <#starten>
|
||||||
|
5. Ritter steuern <#ritter>
|
||||||
|
|
||||||
|
|
||||||
|
//Das Spiel
|
||||||
|
|
||||||
|
Im Dojo Game geht es, einfach erklärt, um einen *Server*, auf welchem
|
||||||
|
ein Spiel für dich bereitgestellt wird. In diesem Spiel gibt es im
|
||||||
|
Singleplayer-Modus *einen Gegner, den sogenannten Dojo-Bot,* und deinen
|
||||||
|
Charakter. *Dein Ritter* hat dieselben Eigenschaften wie ein vom
|
||||||
|
Computer gesteuerter Ritter. Über bestimmte Befehle (diese werden über
|
||||||
|
URLs aufgerufen) kannst du deinen Spieler steuern und so programmieren,
|
||||||
|
dass dieser mit deinen Gegnern kämpft, vor ihnen wegläuft oder andere
|
||||||
|
strategische Aktionen ausführt. Ziel ist es, so viele Gegner wie möglich
|
||||||
|
zu eliminieren.
|
||||||
|
*Ein Spiel dauert maximal 15 Minuten* - danach wird deine Sitzung
|
||||||
|
geschlossen. Es gibt innerhalb dieser 15 Minuten unendlich viele Gegner.
|
||||||
|
Wird ein Bot getötet, wird dieser auf einer zufälligen anderen Stelle
|
||||||
|
der Map wiedergeboren. Bei Start eines neuen Spiels beginnst du immer
|
||||||
|
automatisch neu in Level 1.
|
||||||
|
|
||||||
|
|
||||||
|
//Vorbereitung
|
||||||
|
|
||||||
|
Um am Dojo Game teilzunehmen, benötigst du einen an das Internet
|
||||||
|
angeschlossenen Computer, einen *Digital Dojo Zugang* <https://
|
||||||
|
dd.countit.at/register>. Sobald du eingeloggt <https://dd.countit.at/
|
||||||
|
login> bist, findest du im Folgenden deinen *API-Schlüssel*, den du
|
||||||
|
schon bald brauchen wirst.
|
||||||
|
|
||||||
|
Die Logik deines Spielers kannst du in jeder beliebigen
|
||||||
|
Programmiersprache implementieren, da Web-Requests an eine API von fast
|
||||||
|
jeder verfügbaren Programmier- und Skriptsprache unterstützt werden.
|
||||||
|
|
||||||
|
Falls du Starthilfe benötigst und auf bereits erstellte Algorithmen
|
||||||
|
aufbauen möchtest, kannst du dich hier <https://dd.countit.at/dojo-game/
|
||||||
|
tipps> umsehen.
|
||||||
|
|
||||||
|
Die REST-API Befehle können auch mithilfe vom Fiddler <https://
|
||||||
|
www.telerik.com/fiddler> oder dem Postman <https://www.postman.com/
|
||||||
|
> gesendet werden. Diese Programme werden meistens zum Testen oder für
|
||||||
|
Prototypen verwendet und benötigen keinen Programm-Code zum Funktionieren.
|
||||||
|
|
||||||
|
|
||||||
|
//API-Schlüssel
|
||||||
|
|
||||||
|
Mithilfe deines Keys kannst du dich mit unseren Servern verbinden um
|
||||||
|
dein Spiel zu starten. Zudem werden damit deine gesendeten Befehle
|
||||||
|
identifiziert, damit Aktionen, welche du ausführst, auch wirklich nur
|
||||||
|
für dich gelten. Somit wird verhindert, dass du die Spiele anderer User
|
||||||
|
stören oder manipulieren kannst.
|
||||||
|
|
||||||
|
*Halte deinen Schlüssel geheim* - er könnte es anderen Nutzern
|
||||||
|
ermöglichen, Befehle auszuführen welche nicht in deinem Sinne sind!
|
||||||
|
|
||||||
|
|
||||||
|
//Los geht's
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
In diesem Abschnitt erfährst du, wie du ein Spiel startest und beendest,
|
||||||
|
den Status des laufenden Spiels abfragen kannst, und wie sich dein
|
||||||
|
Ritter steuern lässt.
|
||||||
|
|
||||||
|
|
||||||
|
Wie starte ich ein Spiel? [POST]
|
||||||
|
|
||||||
|
|
||||||
|
Um ein Spiel zu starten, benötigst du deinen API-Schlüssel <#api>. Wurde
|
||||||
|
dieser Befehl erfolgreich ausgeführt, wird das Spiel automatisch
|
||||||
|
gestartet und du kannst direkt Aktionen zum Bewegen des Ritters ausführen.
|
||||||
|
|
||||||
|
Falls bereits ein Spiel mit deinem Charakter läuft, musst du dieses
|
||||||
|
zuerst beenden, bevor du ein neues erstellen kannst. Spiele werden
|
||||||
|
mithilfe eines POST-Requests erzeugt.
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/game/{key}/create
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/game/{key}/create
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#spielstart>
|
||||||
|
|
||||||
|
|
||||||
|
<http://countit-azure-test.westeurope.cloudapp.azure.com:1337/dojo-
|
||||||
|
game/postman#spieleinstieg>
|
||||||
|
|
||||||
|
|
||||||
|
Wie beende ich ein Spiel? [POST]
|
||||||
|
|
||||||
|
|
||||||
|
Ein Spiel wird beendet, wenn du mit deinem API-Key folgenden Befehl als
|
||||||
|
POST-Request ausführst:
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/game/{key}/close
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/game/{key}/close
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#spielende>
|
||||||
|
|
||||||
|
|
||||||
|
Status des laufenden Spiels abfragen [GET]
|
||||||
|
|
||||||
|
|
||||||
|
Um abfragen zu können, ob mit deinem API-Schlüssel bereits ein Spiel
|
||||||
|
läuft, kannst du den folgenden Befehl verwenden. Du erhältst als
|
||||||
|
Rückgabewert dasselbe wie beim Erstellen und Schließen eines Spiels
|
||||||
|
sowie eine Fehlermeldung, falls kein Spiel vorhanden ist.
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/game/{key}/status
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/game/{key}/status
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#status>
|
||||||
|
|
||||||
|
Beim Erstellen und Beenden eines Spiels werden jeweils dieselben Daten
|
||||||
|
zurückgeliefert:
|
||||||
|
|
||||||
|
→ Game-ID: Diese macht dein Spiel identifizierbar.
|
||||||
|
→ Running: Damit werden dein Spielstatus und der Name des geladenen
|
||||||
|
Levels sichtbar.
|
||||||
|
|
||||||
|
Der zurückgegebene String ist im JSON Format.
|
||||||
|
|
||||||
|
{"gameid":"d2144a67-733a-4e06-8a6b-9ce877b99753","running":true,"level":"The Default Level"}
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{"gameid":"d2144a67-733a-4e06-8a6b-9ce877b99753","running":true,"level":"The Default Level"}
|
||||||
|
|
||||||
|
➯ Was ist JSON? <https://dd.countit.at/dojo-game/tipps#technologie>
|
||||||
|
|
||||||
|
|
||||||
|
//Steuere deinen Ritter
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Ein Ritter hat die Möglichkeit auf einem zweidimensionalen Spielfeld
|
||||||
|
verschiedenste Aktionen auszuführen. Aktionen sowie ihre Wirkungen und
|
||||||
|
REST Aufrufe sind hier gelistet. Versuche deine Aktionen nicht zu
|
||||||
|
schnell hintereinander auszuführen, da du nach jeder Aktion eine gewisse
|
||||||
|
Zeit abwarten musst, bis du diese wieder einsetzen kannst. Dein Ritter
|
||||||
|
führt nur Befehle aus, welche du ihm gibst. Sendest du deinem Spieler
|
||||||
|
keine Aktionen, welche dieser ausführen soll, steht dieser durchgehend
|
||||||
|
auf derselben Position und ist verletzbar.
|
||||||
|
|
||||||
|
|
||||||
|
Move [POST]
|
||||||
|
|
||||||
|
|
||||||
|
Du kannst deinen Ritter alle 250 Millisekunden um ein Feld in eine der
|
||||||
|
vier Himmelsrichtungen bewegen.
|
||||||
|
Das Feld "Direction" sollte eine Zahl zwischen 0 und 3 enthalten:
|
||||||
|
|
||||||
|
→ 0 für Norden
|
||||||
|
→ 1 für Osten
|
||||||
|
→ 2 für Süden
|
||||||
|
→ 3 für Westen
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/move/{direction}
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/move/{direction}
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#move>
|
||||||
|
|
||||||
|
|
||||||
|
Dash [POST]
|
||||||
|
|
||||||
|
|
||||||
|
Dash lässt dich 5 Blöcke in eine von dir bestimmte Richtung bewegen.
|
||||||
|
Es hat außerdem einen besonderen Cooldown: Du kannst Dash schon 3
|
||||||
|
Sekunden nach der Aktivierung verwenden, aber dein Spieler wird zu
|
||||||
|
erschöpft sein und sich nur 3 Blöcke bewegen. Doch aufgepasst, wenn du
|
||||||
|
gegen eine Wand dasht nimmst du Schaden in Höhe der verbleibenden Dash-
|
||||||
|
Distanz. Nachdem du diese Aktion verwendet hast, musst du 5000
|
||||||
|
Millisekunden warten.
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/dash/{direction}
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/dash/{direction}
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#dash>
|
||||||
|
|
||||||
|
|
||||||
|
Hit [POST]
|
||||||
|
|
||||||
|
|
||||||
|
Diese Aktion wird benutzt, um mit deinem Ritter zu schlagen. Es kann
|
||||||
|
jeweils nur in eine Himmelsrichtung (0 für Norden, 1 für Osten, 2 für
|
||||||
|
Süden und 3 für Westen) geschlagen werden und dies nur einen Block weit.
|
||||||
|
Danach musst du 250 Millisekunden warten, bis du wieder angreifen
|
||||||
|
kannst. Ein normaler Schlag, welcher mit einem Abstand von einem Block
|
||||||
|
erzielt wurde, fügt dem Gegner weniger Schaden zu als wenn du auf
|
||||||
|
demselben Block wie ein Gegner stehst. Hierbei ist es egal, wie viele
|
||||||
|
Gegner auf einem Fleck stehen. Jeder bekommt gleich viel Schaden.
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/hit/{direction}
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/hit/{direction}
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#hit>
|
||||||
|
|
||||||
|
|
||||||
|
Shoot [POST]
|
||||||
|
|
||||||
|
|
||||||
|
Shoot benötigt, wie Peek, eine Direction (0 für Norden, 1 für Osten, 2
|
||||||
|
für Süden und 3 für Westen), also eine Richtung. Shoot verwendet diese
|
||||||
|
Richtung, um einen Schuss in diese Richtung abzugeben. Als Antwort kommt
|
||||||
|
zurück, ob diese Aktion ausgeführt wurde und ob ein Gegner getroffen
|
||||||
|
wurde. Die Kugel stoppt beim ersten Spieler. Nachdem du diese Aktion
|
||||||
|
verwendet hast, musst du 1000 Millisekunden warten.
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/shoot/{direction}
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/shoot/{direction}
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#shoot>
|
||||||
|
|
||||||
|
|
||||||
|
Radar [GET]
|
||||||
|
|
||||||
|
|
||||||
|
Mit dem Befehl "Radar" kannst du die Umgebung um deinen Spieler im
|
||||||
|
Umkreis von 5 Blöcken auf Feinde überprüfen. Das Feld wird aufgeteilt
|
||||||
|
und durchsucht. Danach erhältst du Informationen über die Anzahl der
|
||||||
|
Spieler in den 4 Himmelsrichtungen und den Block, auf dem du stehst.
|
||||||
|
Nachdem du diese Aktion verwendet hast, musst du 250 Millisekunden warten.
|
||||||
|
|
||||||
|
Radar <https://dd.countit.at/media/dojo-game/dojo-game_radar-
|
||||||
|
himmelsrichtungen.png>
|
||||||
|
https://game-dd.countit.at/api/player/{key}/radar
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/radar
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#radar>
|
||||||
|
|
||||||
|
|
||||||
|
Scan [GET]
|
||||||
|
|
||||||
|
|
||||||
|
Mit Scan kannst du den am nächsten stehenden Spieler im Umkreis von 3
|
||||||
|
Blöcken finden. Durch den Rückgabewert weißt du dann auch, was seine
|
||||||
|
relative Position zu dir ist. Nachdem du diese Aktion verwendet hast,
|
||||||
|
musst du 2000 Millisekunden warten.
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/scan
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/scan
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request? <https://dd.countit.at/dojo-game/postman#scan>
|
||||||
|
|
||||||
|
|
||||||
|
Peek [GET]
|
||||||
|
|
||||||
|
|
||||||
|
Peek benötigt, wie Move und Hit, eine Direction (0 für Norden, 1 für
|
||||||
|
Osten, 2 für Süden und 3 für Westen). Peek verwendet diese Richtung, um
|
||||||
|
zu überprüfen, ob sich in der angegebenen Richtung ein Spieler in deinem
|
||||||
|
Sichtfeld befindet. Peek gibt dir dann die Anzahl der Gegner in der
|
||||||
|
Sichtlinie und die Distanz zum nächsten stehenden Spieler in dieser
|
||||||
|
Sichtlinie zurück. Dies kannst du alle 1000 Millisekunden verwenden.
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/peek/{direction}
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/peek/{direction}
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#peek>
|
||||||
|
|
||||||
|
|
||||||
|
Stats [GET]
|
||||||
|
|
||||||
|
|
||||||
|
Mit Stats hast du die Möglichkeit dir deine Kills und Deaths des aktiven
|
||||||
|
Spiels anzusehen. Der Rückgabewert weicht vom Rückgabewert der anderen
|
||||||
|
Funktionen ab - enthalten sind Informationen zum derzeitigen Level,
|
||||||
|
allgemeine Stats sowie Informationen zu den Lebenspunkten eines Spielers.
|
||||||
|
|
||||||
|
*progress* - gibt an, wie weit du ein Level bereits abgeschlossen hast (%)
|
||||||
|
*remainingtime* - gibt an, wieviele Minuten noch übrig sind bevor das
|
||||||
|
Spiel gestoppt wird
|
||||||
|
*deathsleft* - gibt an, wie oft du noch sterben darfst, bevor das Spiel
|
||||||
|
gestoppt wird
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/stats
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/stats
|
||||||
|
|
||||||
|
Der Rückgabewert dieser Funktion wurde mit dem letzten Update verändert.
|
||||||
|
Hier ein Beispiel für den neuen Rückgabewert:
|
||||||
|
|
||||||
|
{"action":"stats","executed":true,"stats":
|
||||||
|
{"kills":1,"deaths":1},"level":
|
||||||
|
{"progress":6.6,"remainingtime":14.44,"deathsleft":999,"levelid":2,"name":"Bigger Map Level"},"health":{"currenthealth":10.0,"maxhealth":10.0}}
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{"action":"stats","executed":true,"stats":{"kills":1,"deaths":1},"level":{"progress":6.6,"remainingtime":14.44,"deathsleft":999,"levelid":2,"name":"Bigger Map Level"},"health":{"currenthealth":10.0,"maxhealth":10.0}}
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#stats>
|
||||||
|
|
||||||
|
|
||||||
|
Specialattack [POST]
|
||||||
|
|
||||||
|
|
||||||
|
Specialattack ist eine Aktion, welche du viel seltener als einen
|
||||||
|
normalen Schlag ("Hit") einsetzen kannst. Diese hat allerdings eine
|
||||||
|
bestimmte Fähigkeit und verursacht dadurch mehr Schaden an deinem
|
||||||
|
Gegner. Specialattack nimmt keine Direction als Parameter entgegen,
|
||||||
|
sondern führt den Angriff in allen Richtungen mit einem Wirkungskreis
|
||||||
|
von 3 Blöcken gleichzeitig aus. Du kannst die Specialattack alle 5000
|
||||||
|
Millisekunden ausführen.
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/specialattack
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/specialattack
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#specialattack>
|
||||||
|
|
||||||
|
|
||||||
|
Teleport [GET]
|
||||||
|
|
||||||
|
|
||||||
|
Mit Teleport kannst du dich auf ein beliebiges Feld teleportieren. Der
|
||||||
|
Trick dabei: Du musst das Feld relativ zu deiner Position mithilfe von x
|
||||||
|
und y Koordinaten angeben.
|
||||||
|
Cooldown: 20000 Millisekunden (20 Sekunden)
|
||||||
|
|
||||||
|
Achtung: Falls du in einer Wand landest bist du sofort tot. Wenn du
|
||||||
|
allerdings einen Gegner erwischst, wird dieser sofort getötet.
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/teleport/{x}/{y}
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://game-dd.countit.at/api/player/{key}/teleport/{x}/{y}
|
||||||
|
|
||||||
|
➯ Problem? <https://dd.countit.at/dojo-game/tipps#fehler>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#teleport>
|
||||||
|
|
||||||
|
|
||||||
|
Rückgabewert
|
||||||
|
|
||||||
|
|
||||||
|
Jede Aktion gibt im Prinzip immer einen gleich strukturierten JSON-
|
||||||
|
String zurück. Jedoch können sich die Datentypen der einzelnen Felder je
|
||||||
|
nach Aktion unterscheiden.
|
||||||
|
Hier zwei Beispiele, um dies zu verdeutlichen:
|
||||||
|
|
||||||
|
Die Move-Aktion, ohne speziellen Rückgabewert:
|
||||||
|
|
||||||
|
{"action":"move","move":"true","executed":"true"}
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{"action":"move","move":"true","executed":"true"}
|
||||||
|
|
||||||
|
Die Radar-Aktion, mit einem Integer als Rückgabewert:
|
||||||
|
|
||||||
|
{"action":"radar","radar":2,"executed":"true"}
|
||||||
|
text/x-csharp
|
||||||
|
|
||||||
|
xxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{"action":"radar","radar":2,"executed":"true"}
|
||||||
|
|
||||||
|
➯ Was ist JSON? <https://dd.countit.at/dojo-game/tipps#technologie>
|
||||||
|
➯ Postman-Request <https://dd.countit.at/dojo-game/postman#rueckgabewerte>
|
||||||
|
|
||||||
|
|
||||||
|
Bereit für die ultimative Programmier-Challenge an deiner Schule?
|
||||||
|
|
||||||
|
Alle Infos dazu findest du hier //
|
||||||
|
<https://dd.countit.at/programmierwettbewerb>
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
|
||||||
|
Ultimative Coding-Roadmap
|
||||||
|
|
||||||
|
Unsere Roadmap weist dir den Weg zum Coding-Profi!
|
||||||
|
|
||||||
|
Mehr erfahren // <https://dd.countit.at/programmieren-lernen>
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
|
||||||
|
Besiege unseren Bot
|
||||||
|
|
||||||
|
Egal ob als Einzelspieler oder im Mulitplayer-Modus: Stelle dich dem
|
||||||
|
Dojo-Bot und beweise, dass du auf das Leaderboard gehörst!
|
||||||
|
|
||||||
|
Ich bin bereit! // <https://dd.countit.at/dojo-game>
|
||||||
|
/
|
||||||
|
|
||||||
|
/
|
||||||
|
|
||||||
|
|
||||||
|
Bewerbung bei COUNT IT
|
||||||
|
|
||||||
|
Starte deine Karriere als Softwareentwickler*in bei COUNT IT.
|
||||||
|
|
||||||
|
Zum Formular! // <https://dd.countit.at/bewerben>
|
||||||
|
|
||||||
|
|
||||||
|
Über Digital Dojo
|
||||||
|
|
||||||
|
Das Digital Dojo ist der virtuelle Übungsraum von COUNT IT.
|
||||||
|
|
||||||
|
Angehende Programmierer*innen, Code-Neulinge, Wiedereinsteiger*innen und
|
||||||
|
Fortgeschrittene finden hier das nötige Rüstzeug für ihre Karriere.
|
||||||
|
|
||||||
|
Du möchtest deine Lehre bei COUNT IT starten? Dann bist du hier richtig
|
||||||
|
- besiege deine Gegner im Dojo Game und sichere dir deine Lehrstelle!
|
||||||
|
|
||||||
|
Inspire your career.
|
||||||
|
|
||||||
|
|
||||||
|
Navigation
|
||||||
|
|
||||||
|
Programmieren lernen <https://dd.countit.at/programmieren-lernen> News
|
||||||
|
<https://dd.countit.at/news>
|
||||||
|
Programmierschule <https://dd.countit.at/programmierschule> Partner
|
||||||
|
<https://dd.countit.at/partner>
|
||||||
|
Programmier-Challenge <https://dd.countit.at/programmierwettbewerb>
|
||||||
|
Dojo Game <https://dd.countit.at/dojo-game>
|
||||||
|
|
||||||
|
|
||||||
|
Datenschutz <https://www.countit.at/datenschutz> Karriere @ COUNT IT
|
||||||
|
<https://karriere.countit.at/>
|
||||||
|
Impressum <https://www.countit.at/impressum> COUNT IT
|
||||||
|
Softwarehouse <https://it.countit.at/>
|
||||||
|
|
||||||
|
|
||||||
|
Newsletter abonnieren
|
||||||
|
|
||||||
|
Der COUNT IT Newsletter liefert viermal jährlich interessante
|
||||||
|
Neuigkeiten über das Unternehmen. Gleich anfordern!
|
||||||
|
|
||||||
|
E-Mail-Adresse
|
||||||
|
Senden
|
||||||
|
|
||||||
|
// <https://de-de.facebook.com/COUNTITGroup/> // <https://
|
||||||
|
www.instagram.com/count_it_group/> // <https://de.linkedin.com/company/
|
||||||
|
count-it-group>
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
COUNT IT Group verwendet Cookies, um die Website bestmöglich an die
|
||||||
|
Bedürfnisse der User anpassen zu können.
|
||||||
|
|
||||||
|
* Google
|
||||||
|
* Microsoft
|
||||||
|
* Ahrefs
|
||||||
|
* Statistik
|
||||||
|
|
||||||
|
Akzeptieren
|
||||||
|
Individuelle EinstellungenIndividuelle Einstellungen ausblenden |
|
||||||
|
Datenschutzerklärung <https://www.countit.at/datenschutz>
|
||||||
|
|
||||||
|
Google
|
||||||
|
|
||||||
|
Google Analytics und Optimize aggregiert Daten über unsere Besucher und
|
||||||
|
ihr Verhalten auf unserer Website. Wir nutzen diese Informationen zur
|
||||||
|
Verbesserung unserer Seite.
|
||||||
|
|
||||||
|
Microsoft
|
||||||
|
|
||||||
|
Microsoft aggregiert Daten über unsere Besucher und ihr Verhalten auf
|
||||||
|
unserer Website. Wir nutzen diese Informationen zur Verbesserung unserer
|
||||||
|
Seite.
|
||||||
|
|
||||||
|
Ahrefs
|
||||||
|
|
||||||
|
Ahrefs aggregiert Daten über unsere Besucher und ihr Verhalten auf
|
||||||
|
unserer Website. Wir nutzen diese Informationen zur Verbesserung unserer
|
||||||
|
Seite.
|
||||||
|
|
||||||
|
Statistik
|
||||||
|
|
||||||
|
Es werden grundlegend notwendige, anonymisierte Userdaten aufgezeichnet,
|
||||||
|
welche nur von uns für Auswertungen verwendet werden!
|
||||||
|
|
||||||
|
Cookie Entscheidung speichern
|
||||||
453
main-aggro.py
Normal file
453
main-aggro.py
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
"""
|
||||||
|
Digital Dojo Bot v3 - Radar-based detection
|
||||||
|
============================================
|
||||||
|
Problem: scan returns (0,0) when no enemy nearby = useless for navigation.
|
||||||
|
Fix: Use RADAR (0.25s cooldown) to get actual enemy positions, log the raw
|
||||||
|
structure so we can understand the API, and chase accordingly.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
pip install httpx
|
||||||
|
python dojo_bot.py --token YOUR_API_KEY
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio, argparse, logging, random, sys, time
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S")
|
||||||
|
log = logging.getLogger("DojoBot")
|
||||||
|
|
||||||
|
BASE = "https://game-dd.countit.at"
|
||||||
|
|
||||||
|
# direction: 0=NORTH(-Y), 1=EAST(+X), 2=SOUTH(+Y), 3=WEST(-X)
|
||||||
|
NAMES = {0:"N", 1:"E", 2:"S", 3:"W"}
|
||||||
|
|
||||||
|
CD = {
|
||||||
|
"move": 0.27,
|
||||||
|
"hit": 0.27,
|
||||||
|
"radar": 0.27,
|
||||||
|
"special": 5.1,
|
||||||
|
"scan": 2.1,
|
||||||
|
"dash": 3.1,
|
||||||
|
"shoot": 1.1,
|
||||||
|
"peek": 1.1,
|
||||||
|
}
|
||||||
|
|
||||||
|
def dir_toward(dx, dy):
|
||||||
|
if abs(dx) >= abs(dy): return 1 if dx > 0 else 3
|
||||||
|
return 2 if dy > 0 else 0
|
||||||
|
|
||||||
|
|
||||||
|
class Bot:
|
||||||
|
def __init__(self, token, multiplayer=False):
|
||||||
|
self.token = token
|
||||||
|
self.multiplayer = multiplayer
|
||||||
|
self.http = httpx.AsyncClient(timeout=8.0)
|
||||||
|
self._last = {k: 0.0 for k in CD}
|
||||||
|
self.health = 10.0
|
||||||
|
self.kills = 0
|
||||||
|
self.deaths = 0
|
||||||
|
self.target_dx = None
|
||||||
|
self.target_dy = None
|
||||||
|
self.target_last_seen = 0.0
|
||||||
|
self.target_ttl = 1.8
|
||||||
|
self.last_known_dir = random.randint(0, 3)
|
||||||
|
self.radar_seen = False # have we printed raw radar yet?
|
||||||
|
self.explore_dir = random.randint(0, 3)
|
||||||
|
self.explore_steps = 0
|
||||||
|
|
||||||
|
def ready(self, a): return (time.monotonic() - self._last[a]) >= CD[a]
|
||||||
|
def mark(self, a): self._last[a] = time.monotonic()
|
||||||
|
|
||||||
|
def _set_target(self, dx, dy):
|
||||||
|
self.target_dx, self.target_dy = dx, dy
|
||||||
|
self.target_last_seen = time.monotonic()
|
||||||
|
self.last_known_dir = dir_toward(dx, dy)
|
||||||
|
|
||||||
|
def _expire_target(self):
|
||||||
|
if self.has_target and (time.monotonic() - self.target_last_seen) > self.target_ttl:
|
||||||
|
self.target_dx, self.target_dy = None, None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_target(self): return self.target_dx is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dist(self):
|
||||||
|
if not self.has_target: return 999
|
||||||
|
return abs(self.target_dx) + abs(self.target_dy)
|
||||||
|
|
||||||
|
def _ci_get(self, data, *keys, default=None):
|
||||||
|
"""Case-insensitive dict getter for unstable API field casing."""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return default
|
||||||
|
for key in keys:
|
||||||
|
if key in data:
|
||||||
|
return data[key]
|
||||||
|
lowered = {str(k).lower(): v for k, v in data.items()}
|
||||||
|
for key in keys:
|
||||||
|
hit = lowered.get(str(key).lower())
|
||||||
|
if hit is not None:
|
||||||
|
return hit
|
||||||
|
return default
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _as_int(value, default=0):
|
||||||
|
try:
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _as_bool(value):
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return value != 0
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.strip().lower() in {"true", "1", "yes"}
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def GET(self, path):
|
||||||
|
for attempt in range(2):
|
||||||
|
try:
|
||||||
|
r = await self.http.get(f"{BASE}{path}")
|
||||||
|
if r.status_code == 200:
|
||||||
|
try:
|
||||||
|
return r.json()
|
||||||
|
except ValueError:
|
||||||
|
log.warning(f"GET {path}: invalid JSON body")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if r.status_code >= 500 and attempt == 0:
|
||||||
|
await asyncio.sleep(0.15)
|
||||||
|
continue
|
||||||
|
|
||||||
|
hint = (r.text or "")[:180].replace("\n", " ")
|
||||||
|
log.warning(f"GET {path}: status={r.status_code} body={hint}")
|
||||||
|
return None
|
||||||
|
except (httpx.TimeoutException, httpx.NetworkError, httpx.RemoteProtocolError) as e:
|
||||||
|
if attempt == 0:
|
||||||
|
await asyncio.sleep(0.15)
|
||||||
|
continue
|
||||||
|
log.warning(f"GET {path}: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"GET {path}: {e}")
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def POST(self, path):
|
||||||
|
for attempt in range(2):
|
||||||
|
try:
|
||||||
|
r = await self.http.post(f"{BASE}{path}")
|
||||||
|
if r.status_code == 200:
|
||||||
|
try:
|
||||||
|
return r.json()
|
||||||
|
except ValueError:
|
||||||
|
log.warning(f"POST {path}: invalid JSON body")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if r.status_code >= 500 and attempt == 0:
|
||||||
|
await asyncio.sleep(0.15)
|
||||||
|
continue
|
||||||
|
|
||||||
|
hint = (r.text or "")[:180].replace("\n", " ")
|
||||||
|
log.warning(f"POST {path}: status={r.status_code} body={hint}")
|
||||||
|
return None
|
||||||
|
except (httpx.TimeoutException, httpx.NetworkError, httpx.RemoteProtocolError) as e:
|
||||||
|
if attempt == 0:
|
||||||
|
await asyncio.sleep(0.15)
|
||||||
|
continue
|
||||||
|
log.warning(f"POST {path}: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"POST {path}: {e}")
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ── Game ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
if not self.multiplayer:
|
||||||
|
r = await self.POST(f"/api/game/{self.token}/create")
|
||||||
|
log.info(f"Game: {r}")
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
if not self.multiplayer: await self.POST(f"/api/game/{self.token}/close")
|
||||||
|
await self.http.aclose()
|
||||||
|
log.info("Done.")
|
||||||
|
|
||||||
|
# ── Sense ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def radar(self):
|
||||||
|
if not self.ready("radar"): return
|
||||||
|
res = await self.GET(f"/api/player/{self.token}/radar")
|
||||||
|
if not res: return
|
||||||
|
self.mark("radar")
|
||||||
|
|
||||||
|
raw = self._ci_get(res, "RadarResults", "radarResults", default={}) or {}
|
||||||
|
|
||||||
|
# Log full radar structure once to understand server payload shape
|
||||||
|
if not self.radar_seen:
|
||||||
|
log.info(f"=== RADAR STRUCTURE ===\n{res}\n=======================")
|
||||||
|
self.radar_seen = True
|
||||||
|
|
||||||
|
nearest = self._parse_radar(raw)
|
||||||
|
if nearest is not None:
|
||||||
|
self._set_target(*nearest)
|
||||||
|
|
||||||
|
def _parse_radar(self, raw):
|
||||||
|
"""Extract nearest enemy hint from RadarResults."""
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Documented format: directional counts in radar sectors.
|
||||||
|
if isinstance(raw, dict) and all(isinstance(v, (int, float, str)) for v in raw.values()):
|
||||||
|
buckets = {
|
||||||
|
0: ["0", "n", "north", "up"],
|
||||||
|
1: ["1", "e", "east", "right"],
|
||||||
|
2: ["2", "s", "south", "down"],
|
||||||
|
3: ["3", "w", "west", "left"],
|
||||||
|
}
|
||||||
|
counts = {}
|
||||||
|
lowered = {str(k).lower(): self._as_int(v, 0) for k, v in raw.items()}
|
||||||
|
for direction, names in buckets.items():
|
||||||
|
counts[direction] = max((lowered.get(n, 0) for n in names), default=0)
|
||||||
|
|
||||||
|
best_dir = max(counts, key=counts.get)
|
||||||
|
if counts.get(best_dir, 0) > 0:
|
||||||
|
return [(0, -2), (2, 0), (0, 2), (-2, 0)][best_dir]
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Fallback: try coordinate-like structures if API payload differs.
|
||||||
|
candidates = []
|
||||||
|
if isinstance(raw, dict):
|
||||||
|
values = list(raw.values())
|
||||||
|
elif isinstance(raw, list):
|
||||||
|
values = raw
|
||||||
|
else:
|
||||||
|
values = []
|
||||||
|
|
||||||
|
for v in values:
|
||||||
|
if not isinstance(v, dict):
|
||||||
|
continue
|
||||||
|
x = self._ci_get(v, "x", "posX", "X", "PosX")
|
||||||
|
y = self._ci_get(v, "y", "posY", "Y", "PosY")
|
||||||
|
if x is None or y is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
xi, yi = int(x), int(y)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
candidates.append((xi, yi))
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
return min(candidates, key=lambda c: abs(c[0]) + abs(c[1]))
|
||||||
|
|
||||||
|
async def scan(self):
|
||||||
|
"""Backup: scan returns relative delta to nearest player."""
|
||||||
|
if not self.ready("scan"): return
|
||||||
|
res = await self.GET(f"/api/player/{self.token}/scan")
|
||||||
|
if not res: return
|
||||||
|
self.mark("scan")
|
||||||
|
|
||||||
|
diff = self._ci_get(res, "DifferenceToNearestPlayer", default={}) or {}
|
||||||
|
x = self._as_int(self._ci_get(diff, "X", "x"), 0)
|
||||||
|
y = self._as_int(self._ci_get(diff, "Y", "y"), 0)
|
||||||
|
if x != 0 or y != 0:
|
||||||
|
self._set_target(x, y)
|
||||||
|
log.info(f"Scan: enemy at delta=({x:+d},{y:+d})")
|
||||||
|
|
||||||
|
async def peek_all(self):
|
||||||
|
"""Peek all 4 directions to spot enemies."""
|
||||||
|
for d in range(4):
|
||||||
|
if not self.ready("peek"): break
|
||||||
|
res = await self.GET(f"/api/player/{self.token}/peek/{d}")
|
||||||
|
if not res:
|
||||||
|
continue
|
||||||
|
self.mark("peek")
|
||||||
|
|
||||||
|
seen = self._as_int(self._ci_get(res, "PlayersInSight", default=0), 0)
|
||||||
|
if seen > 0:
|
||||||
|
dist = max(1, self._as_int(self._ci_get(res, "SightedPlayerDistance", default=1), 1))
|
||||||
|
dx, dy = [(0, -dist), (dist, 0), (0, dist), (-dist, 0)][d]
|
||||||
|
self._set_target(dx, dy)
|
||||||
|
log.info(f"Peek {NAMES[d]}: enemy dist={dist}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def stats(self):
|
||||||
|
res = await self.GET(f"/api/player/{self.token}/stats")
|
||||||
|
if not res: return
|
||||||
|
health = self._ci_get(res, "Health", default={}) or {}
|
||||||
|
self.health = float(self._ci_get(health, "Currenthealth", "CurrentHealth", "currenthealth", default=self.health))
|
||||||
|
|
||||||
|
s = self._ci_get(res, "Stats", default={}) or {}
|
||||||
|
k = self._as_int(self._ci_get(s, "Kills", "kills", default=self.kills), self.kills)
|
||||||
|
if k > self.kills: log.info(f"KILL! Total: {k}")
|
||||||
|
self.kills = k
|
||||||
|
self.deaths = self._as_int(self._ci_get(s, "Deaths", "deaths", default=self.deaths), self.deaths)
|
||||||
|
|
||||||
|
# ── Act ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def move(self, d):
|
||||||
|
if not self.ready("move"): return False
|
||||||
|
res = await self.POST(f"/api/player/{self.token}/move/{d}")
|
||||||
|
if not res: return False
|
||||||
|
self.mark("move")
|
||||||
|
|
||||||
|
moved = self._as_bool(self._ci_get(res, "Move", default=False))
|
||||||
|
if moved and self.has_target:
|
||||||
|
# Update relative target position as we move
|
||||||
|
deltas = [(0,1),(-1,0),(0,-1),(1,0)]
|
||||||
|
self.target_dx = (self.target_dx or 0) + deltas[d][0]
|
||||||
|
self.target_dy = (self.target_dy or 0) + deltas[d][1]
|
||||||
|
return moved
|
||||||
|
|
||||||
|
async def hit(self, d):
|
||||||
|
if not self.ready("hit"): return False
|
||||||
|
res = await self.POST(f"/api/player/{self.token}/hit/{d}")
|
||||||
|
if not res: return False
|
||||||
|
self.mark("hit")
|
||||||
|
|
||||||
|
dmg = self._as_int(self._ci_get(res, "Hit", default=0), 0)
|
||||||
|
crit = self._as_int(self._ci_get(res, "Critical", default=0), 0)
|
||||||
|
if dmg or crit:
|
||||||
|
log.info(f"Hit {NAMES[d]}! dmg={dmg} crit={crit}")
|
||||||
|
return self._as_bool(self._ci_get(res, "Executed", default=False))
|
||||||
|
|
||||||
|
async def special(self):
|
||||||
|
if not self.ready("special"): return False
|
||||||
|
res = await self.POST(f"/api/player/{self.token}/specialattack")
|
||||||
|
if not res: return False
|
||||||
|
self.mark("special")
|
||||||
|
|
||||||
|
h = self._as_int(self._ci_get(res, "Hitcount", "HitCount", default=0), 0)
|
||||||
|
if h: log.info(f"Special hit {h}!")
|
||||||
|
return self._as_bool(self._ci_get(res, "Executed", default=False))
|
||||||
|
|
||||||
|
async def dash(self, d):
|
||||||
|
if not self.ready("dash"): return False
|
||||||
|
res = await self.POST(f"/api/player/{self.token}/dash/{d}")
|
||||||
|
if not res: return False
|
||||||
|
self.mark("dash")
|
||||||
|
|
||||||
|
executed = self._as_bool(self._ci_get(res, "Executed", default=False))
|
||||||
|
if executed and self.has_target:
|
||||||
|
blocks = max(0, self._as_int(self._ci_get(res, "BlocksDashed", default=0), 0))
|
||||||
|
deltas = [(0, blocks), (-blocks, 0), (0, -blocks), (blocks, 0)]
|
||||||
|
self.target_dx = (self.target_dx or 0) + deltas[d][0]
|
||||||
|
self.target_dy = (self.target_dy or 0) + deltas[d][1]
|
||||||
|
return executed
|
||||||
|
|
||||||
|
async def shoot(self, d):
|
||||||
|
if not self.ready("shoot"): return False
|
||||||
|
res = await self.POST(f"/api/player/{self.token}/shoot/{d}")
|
||||||
|
if not res: return False
|
||||||
|
self.mark("shoot")
|
||||||
|
|
||||||
|
if self._as_bool(self._ci_get(res, "HitSomeone", default=False)):
|
||||||
|
log.info("Shoot HIT!")
|
||||||
|
return self._as_bool(self._ci_get(res, "Executed", default=False))
|
||||||
|
|
||||||
|
# ── Strategy ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def engage(self):
|
||||||
|
d = dir_toward(self.target_dx, self.target_dy)
|
||||||
|
|
||||||
|
dist = self.dist
|
||||||
|
if dist <= 1:
|
||||||
|
await self.special()
|
||||||
|
await self.hit(d)
|
||||||
|
await self.hit((d + 1) % 4)
|
||||||
|
await self.hit((d + 3) % 4)
|
||||||
|
await self.move(d)
|
||||||
|
return
|
||||||
|
|
||||||
|
if dist <= 3:
|
||||||
|
await self.special()
|
||||||
|
await self.shoot(d)
|
||||||
|
await self.hit(d)
|
||||||
|
if self.ready("dash"):
|
||||||
|
await self.dash(d)
|
||||||
|
else:
|
||||||
|
await self.move(d)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.shoot(d)
|
||||||
|
if self.ready("dash"):
|
||||||
|
await self.dash(d)
|
||||||
|
await self.move(d)
|
||||||
|
|
||||||
|
async def explore(self):
|
||||||
|
"""Aggressive sweep while probing and pushing forward."""
|
||||||
|
self.explore_steps += 1
|
||||||
|
if self.explore_steps >= 3:
|
||||||
|
self.explore_dir = (self.explore_dir + 1) % 4
|
||||||
|
self.explore_steps = 0
|
||||||
|
|
||||||
|
# Bias exploration toward the most recent enemy direction.
|
||||||
|
preferred = self.last_known_dir if random.random() < 0.7 else self.explore_dir
|
||||||
|
|
||||||
|
moved = await self.move(preferred)
|
||||||
|
if not moved:
|
||||||
|
self.explore_dir = (self.explore_dir + 1) % 4
|
||||||
|
self.explore_steps = 0
|
||||||
|
await self.move(self.explore_dir)
|
||||||
|
else:
|
||||||
|
self.explore_dir = preferred
|
||||||
|
await self.move(preferred)
|
||||||
|
|
||||||
|
await self.shoot(preferred)
|
||||||
|
await self.special()
|
||||||
|
|
||||||
|
# ── Main ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
await self.start()
|
||||||
|
log.info("Bot v3 running (aggressive mode). Ctrl+C to stop.")
|
||||||
|
|
||||||
|
tick = 0
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
tick += 1
|
||||||
|
|
||||||
|
await self.radar() # primary detection (0.27s cooldown)
|
||||||
|
await self.scan() # backup detection (2s cooldown)
|
||||||
|
self._expire_target()
|
||||||
|
|
||||||
|
# Reacquire quickly when no target is known.
|
||||||
|
if not self.has_target and tick % 6 == 0:
|
||||||
|
await self.peek_all()
|
||||||
|
|
||||||
|
if tick % 8 == 0:
|
||||||
|
await self.stats()
|
||||||
|
t = f"delta=({self.target_dx:+d},{self.target_dy:+d}) d={self.dist}" if self.has_target else "searching"
|
||||||
|
log.info(f"HP={self.health:.0f} K={self.kills} D={self.deaths} {t}")
|
||||||
|
|
||||||
|
if self.has_target:
|
||||||
|
await self.engage()
|
||||||
|
else:
|
||||||
|
await self.explore()
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
except (KeyboardInterrupt, asyncio.CancelledError):
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
await self.stop()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument("--token", required=True)
|
||||||
|
p.add_argument("--multiplayer", action="store_true")
|
||||||
|
args = p.parse_args()
|
||||||
|
await Bot(args.token, args.multiplayer).run()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try: asyncio.run(main())
|
||||||
|
except KeyboardInterrupt: sys.exit(0)
|
||||||
453
main-less-aggro.py
Normal file
453
main-less-aggro.py
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
"""
|
||||||
|
Digital Dojo Bot v3 - Radar-based detection
|
||||||
|
============================================
|
||||||
|
Problem: scan returns (0,0) when no enemy nearby = useless for navigation.
|
||||||
|
Fix: Use RADAR (0.25s cooldown) to get actual enemy positions, log the raw
|
||||||
|
structure so we can understand the API, and chase accordingly.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
pip install httpx
|
||||||
|
python dojo_bot.py --token YOUR_API_KEY
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio, argparse, logging, random, sys, time
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S")
|
||||||
|
log = logging.getLogger("DojoBot")
|
||||||
|
|
||||||
|
BASE = "https://game-dd.countit.at"
|
||||||
|
|
||||||
|
# direction: 0=NORTH(-Y), 1=EAST(+X), 2=SOUTH(+Y), 3=WEST(-X)
|
||||||
|
NAMES = {0:"N", 1:"E", 2:"S", 3:"W"}
|
||||||
|
|
||||||
|
CD = {
|
||||||
|
"move": 0.27,
|
||||||
|
"hit": 0.27,
|
||||||
|
"radar": 0.27,
|
||||||
|
"special": 5.1,
|
||||||
|
"scan": 2.1,
|
||||||
|
"dash": 3.1,
|
||||||
|
"shoot": 1.1,
|
||||||
|
"peek": 1.1,
|
||||||
|
}
|
||||||
|
|
||||||
|
def dir_toward(dx, dy):
|
||||||
|
if abs(dx) >= abs(dy): return 1 if dx > 0 else 3
|
||||||
|
return 2 if dy > 0 else 0
|
||||||
|
|
||||||
|
|
||||||
|
class Bot:
|
||||||
|
def __init__(self, token, multiplayer=False):
|
||||||
|
self.token = token
|
||||||
|
self.multiplayer = multiplayer
|
||||||
|
self.http = httpx.AsyncClient(timeout=8.0)
|
||||||
|
self._last = {k: 0.0 for k in CD}
|
||||||
|
self.health = 10.0
|
||||||
|
self.kills = 0
|
||||||
|
self.deaths = 0
|
||||||
|
self.target_dx = None
|
||||||
|
self.target_dy = None
|
||||||
|
self.target_last_seen = 0.0
|
||||||
|
self.target_ttl = 1.8
|
||||||
|
self.last_known_dir = random.randint(0, 3)
|
||||||
|
self.radar_seen = False # have we printed raw radar yet?
|
||||||
|
self.explore_dir = random.randint(0, 3)
|
||||||
|
self.explore_steps = 0
|
||||||
|
|
||||||
|
def ready(self, a): return (time.monotonic() - self._last[a]) >= CD[a]
|
||||||
|
def mark(self, a): self._last[a] = time.monotonic()
|
||||||
|
|
||||||
|
def _set_target(self, dx, dy):
|
||||||
|
self.target_dx, self.target_dy = dx, dy
|
||||||
|
self.target_last_seen = time.monotonic()
|
||||||
|
self.last_known_dir = dir_toward(dx, dy)
|
||||||
|
|
||||||
|
def _expire_target(self):
|
||||||
|
if self.has_target and (time.monotonic() - self.target_last_seen) > self.target_ttl:
|
||||||
|
self.target_dx, self.target_dy = None, None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_target(self): return self.target_dx is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dist(self):
|
||||||
|
if not self.has_target: return 999
|
||||||
|
return abs(self.target_dx) + abs(self.target_dy)
|
||||||
|
|
||||||
|
def _ci_get(self, data, *keys, default=None):
|
||||||
|
"""Case-insensitive dict getter for unstable API field casing."""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return default
|
||||||
|
for key in keys:
|
||||||
|
if key in data:
|
||||||
|
return data[key]
|
||||||
|
lowered = {str(k).lower(): v for k, v in data.items()}
|
||||||
|
for key in keys:
|
||||||
|
hit = lowered.get(str(key).lower())
|
||||||
|
if hit is not None:
|
||||||
|
return hit
|
||||||
|
return default
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _as_int(value, default=0):
|
||||||
|
try:
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _as_bool(value):
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return value != 0
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.strip().lower() in {"true", "1", "yes"}
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def GET(self, path):
|
||||||
|
for attempt in range(2):
|
||||||
|
try:
|
||||||
|
r = await self.http.get(f"{BASE}{path}")
|
||||||
|
if r.status_code == 200:
|
||||||
|
try:
|
||||||
|
return r.json()
|
||||||
|
except ValueError:
|
||||||
|
log.warning(f"GET {path}: invalid JSON body")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if r.status_code >= 500 and attempt == 0:
|
||||||
|
await asyncio.sleep(0.15)
|
||||||
|
continue
|
||||||
|
|
||||||
|
hint = (r.text or "")[:180].replace("\n", " ")
|
||||||
|
log.warning(f"GET {path}: status={r.status_code} body={hint}")
|
||||||
|
return None
|
||||||
|
except (httpx.TimeoutException, httpx.NetworkError, httpx.RemoteProtocolError) as e:
|
||||||
|
if attempt == 0:
|
||||||
|
await asyncio.sleep(0.15)
|
||||||
|
continue
|
||||||
|
log.warning(f"GET {path}: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"GET {path}: {e}")
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def POST(self, path):
|
||||||
|
for attempt in range(2):
|
||||||
|
try:
|
||||||
|
r = await self.http.post(f"{BASE}{path}")
|
||||||
|
if r.status_code == 200:
|
||||||
|
try:
|
||||||
|
return r.json()
|
||||||
|
except ValueError:
|
||||||
|
log.warning(f"POST {path}: invalid JSON body")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if r.status_code >= 500 and attempt == 0:
|
||||||
|
await asyncio.sleep(0.15)
|
||||||
|
continue
|
||||||
|
|
||||||
|
hint = (r.text or "")[:180].replace("\n", " ")
|
||||||
|
log.warning(f"POST {path}: status={r.status_code} body={hint}")
|
||||||
|
return None
|
||||||
|
except (httpx.TimeoutException, httpx.NetworkError, httpx.RemoteProtocolError) as e:
|
||||||
|
if attempt == 0:
|
||||||
|
await asyncio.sleep(0.15)
|
||||||
|
continue
|
||||||
|
log.warning(f"POST {path}: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"POST {path}: {e}")
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ── Game ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
if not self.multiplayer:
|
||||||
|
r = await self.POST(f"/api/game/{self.token}/create")
|
||||||
|
log.info(f"Game: {r}")
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
if not self.multiplayer: await self.POST(f"/api/game/{self.token}/close")
|
||||||
|
await self.http.aclose()
|
||||||
|
log.info("Done.")
|
||||||
|
|
||||||
|
# ── Sense ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def radar(self):
|
||||||
|
if not self.ready("radar"): return
|
||||||
|
res = await self.GET(f"/api/player/{self.token}/radar")
|
||||||
|
if not res: return
|
||||||
|
self.mark("radar")
|
||||||
|
|
||||||
|
raw = self._ci_get(res, "RadarResults", "radarResults", default={}) or {}
|
||||||
|
|
||||||
|
# Log full radar structure once to understand server payload shape
|
||||||
|
if not self.radar_seen:
|
||||||
|
log.info(f"=== RADAR STRUCTURE ===\n{res}\n=======================")
|
||||||
|
self.radar_seen = True
|
||||||
|
|
||||||
|
nearest = self._parse_radar(raw)
|
||||||
|
if nearest is not None:
|
||||||
|
self._set_target(*nearest)
|
||||||
|
|
||||||
|
def _parse_radar(self, raw):
|
||||||
|
"""Extract nearest enemy hint from RadarResults."""
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Documented format: directional counts in radar sectors.
|
||||||
|
if isinstance(raw, dict) and all(isinstance(v, (int, float, str)) for v in raw.values()):
|
||||||
|
buckets = {
|
||||||
|
0: ["0", "n", "north", "up"],
|
||||||
|
1: ["1", "e", "east", "right"],
|
||||||
|
2: ["2", "s", "south", "down"],
|
||||||
|
3: ["3", "w", "west", "left"],
|
||||||
|
}
|
||||||
|
counts = {}
|
||||||
|
lowered = {str(k).lower(): self._as_int(v, 0) for k, v in raw.items()}
|
||||||
|
for direction, names in buckets.items():
|
||||||
|
counts[direction] = max((lowered.get(n, 0) for n in names), default=0)
|
||||||
|
|
||||||
|
best_dir = max(counts, key=counts.get)
|
||||||
|
if counts.get(best_dir, 0) > 0:
|
||||||
|
return [(0, -2), (2, 0), (0, 2), (-2, 0)][best_dir]
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Fallback: try coordinate-like structures if API payload differs.
|
||||||
|
candidates = []
|
||||||
|
if isinstance(raw, dict):
|
||||||
|
values = list(raw.values())
|
||||||
|
elif isinstance(raw, list):
|
||||||
|
values = raw
|
||||||
|
else:
|
||||||
|
values = []
|
||||||
|
|
||||||
|
for v in values:
|
||||||
|
if not isinstance(v, dict):
|
||||||
|
continue
|
||||||
|
x = self._ci_get(v, "x", "posX", "X", "PosX")
|
||||||
|
y = self._ci_get(v, "y", "posY", "Y", "PosY")
|
||||||
|
if x is None or y is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
xi, yi = int(x), int(y)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
candidates.append((xi, yi))
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
return min(candidates, key=lambda c: abs(c[0]) + abs(c[1]))
|
||||||
|
|
||||||
|
async def scan(self):
|
||||||
|
"""Backup: scan returns relative delta to nearest player."""
|
||||||
|
if not self.ready("scan"): return
|
||||||
|
res = await self.GET(f"/api/player/{self.token}/scan")
|
||||||
|
if not res: return
|
||||||
|
self.mark("scan")
|
||||||
|
|
||||||
|
diff = self._ci_get(res, "DifferenceToNearestPlayer", default={}) or {}
|
||||||
|
x = self._as_int(self._ci_get(diff, "X", "x"), 0)
|
||||||
|
y = self._as_int(self._ci_get(diff, "Y", "y"), 0)
|
||||||
|
if x != 0 or y != 0:
|
||||||
|
self._set_target(x, y)
|
||||||
|
log.info(f"Scan: enemy at delta=({x:+d},{y:+d})")
|
||||||
|
|
||||||
|
async def peek_all(self):
|
||||||
|
"""Peek all 4 directions to spot enemies."""
|
||||||
|
for d in range(4):
|
||||||
|
if not self.ready("peek"): break
|
||||||
|
res = await self.GET(f"/api/player/{self.token}/peek/{d}")
|
||||||
|
if not res:
|
||||||
|
continue
|
||||||
|
self.mark("peek")
|
||||||
|
|
||||||
|
seen = self._as_int(self._ci_get(res, "PlayersInSight", default=0), 0)
|
||||||
|
if seen > 0:
|
||||||
|
dist = max(1, self._as_int(self._ci_get(res, "SightedPlayerDistance", default=1), 1))
|
||||||
|
dx, dy = [(0, -dist), (dist, 0), (0, dist), (-dist, 0)][d]
|
||||||
|
self._set_target(dx, dy)
|
||||||
|
log.info(f"Peek {NAMES[d]}: enemy dist={dist}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def stats(self):
|
||||||
|
res = await self.GET(f"/api/player/{self.token}/stats")
|
||||||
|
if not res: return
|
||||||
|
health = self._ci_get(res, "Health", default={}) or {}
|
||||||
|
self.health = float(self._ci_get(health, "Currenthealth", "CurrentHealth", "currenthealth", default=self.health))
|
||||||
|
|
||||||
|
s = self._ci_get(res, "Stats", default={}) or {}
|
||||||
|
k = self._as_int(self._ci_get(s, "Kills", "kills", default=self.kills), self.kills)
|
||||||
|
if k > self.kills: log.info(f"KILL! Total: {k}")
|
||||||
|
self.kills = k
|
||||||
|
self.deaths = self._as_int(self._ci_get(s, "Deaths", "deaths", default=self.deaths), self.deaths)
|
||||||
|
|
||||||
|
# ── Act ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def move(self, d):
|
||||||
|
if not self.ready("move"): return False
|
||||||
|
res = await self.POST(f"/api/player/{self.token}/move/{d}")
|
||||||
|
if not res: return False
|
||||||
|
self.mark("move")
|
||||||
|
|
||||||
|
moved = self._as_bool(self._ci_get(res, "Move", default=False))
|
||||||
|
if moved and self.has_target:
|
||||||
|
# Update relative target position as we move
|
||||||
|
deltas = [(0,1),(-1,0),(0,-1),(1,0)]
|
||||||
|
self.target_dx = (self.target_dx or 0) + deltas[d][0]
|
||||||
|
self.target_dy = (self.target_dy or 0) + deltas[d][1]
|
||||||
|
return moved
|
||||||
|
|
||||||
|
async def hit(self, d):
|
||||||
|
if not self.ready("hit"): return False
|
||||||
|
res = await self.POST(f"/api/player/{self.token}/hit/{d}")
|
||||||
|
if not res: return False
|
||||||
|
self.mark("hit")
|
||||||
|
|
||||||
|
dmg = self._as_int(self._ci_get(res, "Hit", default=0), 0)
|
||||||
|
crit = self._as_int(self._ci_get(res, "Critical", default=0), 0)
|
||||||
|
if dmg or crit:
|
||||||
|
log.info(f"Hit {NAMES[d]}! dmg={dmg} crit={crit}")
|
||||||
|
return self._as_bool(self._ci_get(res, "Executed", default=False))
|
||||||
|
|
||||||
|
async def special(self):
|
||||||
|
if not self.ready("special"): return False
|
||||||
|
res = await self.POST(f"/api/player/{self.token}/specialattack")
|
||||||
|
if not res: return False
|
||||||
|
self.mark("special")
|
||||||
|
|
||||||
|
h = self._as_int(self._ci_get(res, "Hitcount", "HitCount", default=0), 0)
|
||||||
|
if h: log.info(f"Special hit {h}!")
|
||||||
|
return self._as_bool(self._ci_get(res, "Executed", default=False))
|
||||||
|
|
||||||
|
async def dash(self, d):
|
||||||
|
if not self.ready("dash"): return False
|
||||||
|
res = await self.POST(f"/api/player/{self.token}/dash/{d}")
|
||||||
|
if not res: return False
|
||||||
|
self.mark("dash")
|
||||||
|
|
||||||
|
executed = self._as_bool(self._ci_get(res, "Executed", default=False))
|
||||||
|
if executed and self.has_target:
|
||||||
|
blocks = max(0, self._as_int(self._ci_get(res, "BlocksDashed", default=0), 0))
|
||||||
|
deltas = [(0, blocks), (-blocks, 0), (0, -blocks), (blocks, 0)]
|
||||||
|
self.target_dx = (self.target_dx or 0) + deltas[d][0]
|
||||||
|
self.target_dy = (self.target_dy or 0) + deltas[d][1]
|
||||||
|
return executed
|
||||||
|
|
||||||
|
async def shoot(self, d):
|
||||||
|
if not self.ready("shoot"): return False
|
||||||
|
res = await self.POST(f"/api/player/{self.token}/shoot/{d}")
|
||||||
|
if not res: return False
|
||||||
|
self.mark("shoot")
|
||||||
|
|
||||||
|
if self._as_bool(self._ci_get(res, "HitSomeone", default=False)):
|
||||||
|
log.info("Shoot HIT!")
|
||||||
|
return self._as_bool(self._ci_get(res, "Executed", default=False))
|
||||||
|
|
||||||
|
# ── Strategy ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def engage(self):
|
||||||
|
d = dir_toward(self.target_dx, self.target_dy)
|
||||||
|
|
||||||
|
dist = self.dist
|
||||||
|
if dist <= 1:
|
||||||
|
await self.special()
|
||||||
|
await self.hit(d)
|
||||||
|
await self.hit((d + 1) % 4)
|
||||||
|
await self.hit((d + 3) % 4)
|
||||||
|
await self.move(d)
|
||||||
|
return
|
||||||
|
|
||||||
|
if dist <= 3:
|
||||||
|
await self.special()
|
||||||
|
await self.shoot(d)
|
||||||
|
await self.hit(d)
|
||||||
|
if self.ready("dash"):
|
||||||
|
await self.dash(d)
|
||||||
|
else:
|
||||||
|
await self.move(d)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.shoot(d)
|
||||||
|
if self.ready("dash"):
|
||||||
|
await self.dash(d)
|
||||||
|
await self.move(d)
|
||||||
|
|
||||||
|
async def explore(self):
|
||||||
|
"""Aggressive sweep while probing and pushing forward."""
|
||||||
|
self.explore_steps += 1
|
||||||
|
if self.explore_steps >= 3:
|
||||||
|
self.explore_dir = (self.explore_dir + 1) % 4
|
||||||
|
self.explore_steps = 0
|
||||||
|
|
||||||
|
# Bias exploration toward the most recent enemy direction.
|
||||||
|
preferred = self.last_known_dir if random.random() < 0.7 else self.explore_dir
|
||||||
|
|
||||||
|
moved = await self.move(preferred)
|
||||||
|
if not moved:
|
||||||
|
self.explore_dir = (self.explore_dir + 1) % 4
|
||||||
|
self.explore_steps = 0
|
||||||
|
await self.move(self.explore_dir)
|
||||||
|
else:
|
||||||
|
self.explore_dir = preferred
|
||||||
|
await self.move(preferred)
|
||||||
|
|
||||||
|
await self.shoot(preferred)
|
||||||
|
await self.special()
|
||||||
|
|
||||||
|
# ── Main ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
await self.start()
|
||||||
|
log.info("Bot v3 running (aggressive mode). Ctrl+C to stop.")
|
||||||
|
|
||||||
|
tick = 0
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
tick += 1
|
||||||
|
|
||||||
|
await self.radar() # primary detection (0.27s cooldown)
|
||||||
|
await self.scan() # backup detection (2s cooldown)
|
||||||
|
self._expire_target()
|
||||||
|
|
||||||
|
# Reacquire quickly when no target is known.
|
||||||
|
if not self.has_target and tick % 6 == 0:
|
||||||
|
await self.peek_all()
|
||||||
|
|
||||||
|
if tick % 8 == 0:
|
||||||
|
await self.stats()
|
||||||
|
t = f"delta=({self.target_dx:+d},{self.target_dy:+d}) d={self.dist}" if self.has_target else "searching"
|
||||||
|
log.info(f"HP={self.health:.0f} K={self.kills} D={self.deaths} {t}")
|
||||||
|
|
||||||
|
if self.has_target:
|
||||||
|
await self.engage()
|
||||||
|
else:
|
||||||
|
await self.explore()
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
except (KeyboardInterrupt, asyncio.CancelledError):
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
await self.stop()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument("--token", required=True)
|
||||||
|
p.add_argument("--multiplayer", action="store_true")
|
||||||
|
args = p.parse_args()
|
||||||
|
await Bot(args.token, args.multiplayer).run()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try: asyncio.run(main())
|
||||||
|
except KeyboardInterrupt: sys.exit(0)
|
||||||
Reference in New Issue
Block a user