Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: "CI"

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
strategy:
matrix:
os: [windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build -c Release --no-restore
- name: Test
run: dotnet test --no-restore --logger trx --results-directory "TestResults-${{ matrix.os }}" --verbosity normal
- name: Upload dotnet test results
uses: actions/upload-artifact@v4
with:
name: dotnet-results-${{ matrix.os }}
path: TestResults-${{ matrix.os }}
# Use always() to always run this step to publish test results when there are test failures
if: ${{ always() }}
6 changes: 6 additions & 0 deletions ArcadePointsBot.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEM
.github\ISSUE_TEMPLATE\config.yml = .github\ISSUE_TEMPLATE\config.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcadePointsBot.Tests", "tests\ArcadePointsBot.Tests\ArcadePointsBot.Tests.csproj", "{631CC0FD-4906-41DE-841C-856D3330709A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -29,6 +31,10 @@ Global
{BB6C1FEA-EA74-47AD-AC7A-B619C243DE3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB6C1FEA-EA74-47AD-AC7A-B619C243DE3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB6C1FEA-EA74-47AD-AC7A-B619C243DE3C}.Release|Any CPU.Build.0 = Release|Any CPU
{631CC0FD-4906-41DE-841C-856D3330709A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{631CC0FD-4906-41DE-841C-856D3330709A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{631CC0FD-4906-41DE-841C-856D3330709A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{631CC0FD-4906-41DE-841C-856D3330709A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
42 changes: 21 additions & 21 deletions src/ArcadePointsBot/ArcadePointsBot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,30 @@

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App"></FrameworkReference>
<PackageReference Include="Avalonia" Version="11.0.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.5" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.5" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.5" />
<PackageReference Include="Avalonia" Version="11.1.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.1.3" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.1.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.3" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.5" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.2" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.5" />
<PackageReference Include="IdentityModel.OidcClient" Version="5.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="ReactiveUI" Version="19.5.1" />
<PackageReference Include="ReactiveUI.Fody" Version="19.5.1" />
<PackageReference Include="System.Reactive" Version="6.0.0" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.1.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.1.3" />
<PackageReference Include="IdentityModel.OidcClient" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="ReactiveUI" Version="20.1.1" />
<PackageReference Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageReference Include="System.Reactive" Version="6.0.1" />
<PackageReference Include="TwitchLib.Api" Version="3.9.0" />
<PackageReference Include="TwitchLib.PubSub" Version="3.2.6" />
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.13">
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<!--<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />-->
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;

namespace ArcadePointsBot.Data.Abstractions.Repositories;
Expand Down Expand Up @@ -116,7 +115,7 @@ public interface IReadRepository<T>
/// </summary>
/// <param name="predicate">Condition</param>
/// <returns>Query</returns>
IQueryable<T> GetBy(Expression<Func<T, bool>> predicate);
IEnumerable<T> GetBy(Func<T, bool> predicate);

/// <summary>
/// Retrieves the first entity from data store that satisfies the given predicate
Expand Down
4 changes: 2 additions & 2 deletions src/ArcadePointsBot/Data/Repositories/DataEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ public IQueryable<T> GetAll()
return GetEntities();
}

public IQueryable<T> GetBy(Expression<Func<T, bool>> predicate)
public IEnumerable<T> GetBy(Func<T, bool> predicate)
{
return GetEntities().Where(predicate);
return GetEntities().ToList().Where(predicate);
}

public T? GetFirst(Expression<Func<T, bool>> predicate)
Expand Down
4 changes: 4 additions & 0 deletions src/ArcadePointsBot/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("ArcadePointsBot.Tests")]
public class AssemblyInfo;
18 changes: 11 additions & 7 deletions src/ArcadePointsBot/TwitchWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using ArcadePointsBot.Auth;
using ArcadePointsBot.Domain.Rewards;
using ArcadePointsBot.Infrastructure.Interop;
using ArcadePointsBot.Util;
using ArcadePointsBot.Views;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
Expand All @@ -24,6 +25,7 @@ namespace ArcadePointsBot;

public class TwitchWorker : BackgroundService, INotifyPropertyChanged
{
private const double SimilarityThreshold = .6;
private readonly Keyboard _keyboard;
private readonly Mouse _mouse;
private readonly ILogger<TwitchWorker> _logger;
Expand Down Expand Up @@ -83,18 +85,20 @@ public TwitchWorker(IServiceProvider provider)
PropertyChanged += ChangeActiveRewards;
}

private async void ChangeActiveRewards(object? sender, PropertyChangedEventArgs e)
private void ChangeActiveRewards(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is not nameof(CurrentCategory))
return;
if (!isLive)
return;
var toDisableIds = await _rewardRepository
.GetBy(x => x.IsEnabled && x.Category != null && x.Category != CurrentCategory)
var toDisableIds = _rewardRepository
.GetBy(x => x.IsEnabled && x.Category != null && !StringUtils.TestSimilarity(x.Category, CurrentCategory, SimilarityThreshold))
.Select(x => x.Id)
.ToListAsync();
var toEnableIds = await _rewardRepository
.GetBy(x => !x.IsEnabled && x.Category != null && x.Category == CurrentCategory)
.ToList();
var toEnableIds = _rewardRepository
.GetBy(x => !x.IsEnabled && x.Category != null && StringUtils.TestSimilarity(x.Category, CurrentCategory, SimilarityThreshold))
.Select(x => x.Id)
.ToListAsync();
.ToList();

Dispatcher.UIThread.Post(async () =>
{
Expand Down
60 changes: 60 additions & 0 deletions src/ArcadePointsBot/Util/StringUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using TwitchLib.Api.Helix.Models.Soundtrack;

namespace ArcadePointsBot.Util;
internal static partial class StringUtils
{
public static double CalculateSimilarity(string? source, string? target)
{
if ((source == null) || (target == null)) return 0.0;
if ((source.Length == 0) || (target.Length == 0)) return 0.0;
if (source == target) return 1.0;

int stepsToSame = ComputeLevenshteinDistance(source, target);
return (1.0 - ((double)stepsToSame / (double)Math.Max(source.Length, target.Length)));
}

public static double CalculateSimilarity(string? source, string? target, bool roundUp = false)
{
var similarity = CalculateSimilarity(source, target);
if (roundUp)
return double.Round(similarity, 2);
else
return similarity;
}

public static bool TestSimilarity(string? source, string? target, double minThreshold, bool roundUp = false)
{
var similarity = CalculateSimilarity(source, target, roundUp);
return similarity >= minThreshold;
}
public static bool TestSimilarity(string? source, string? target, double minThreshold)
{
var similarity = CalculateSimilarity(source, target);
return similarity >= minThreshold;
}

private static int ComputeLevenshteinDistance(string s, string t)
{
int n = s.Length;
int m = t.Length;
int[,] d = new int[n + 1, m + 1];

// initialize the top and right of the table to 0, 1, 2, ...
for (int i = 0; i <= n; d[i, 0] = i++) ;
for (int j = 1; j <= m; d[0, j] = j++) ;

for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
int min1 = d[i - 1, j] + 1;
int min2 = d[i, j - 1] + 1;
int min3 = d[i - 1, j - 1] + cost;
d[i, j] = Math.Min(Math.Min(min1, min2), min3);
}
}
return d[n, m];
}
}
3 changes: 2 additions & 1 deletion src/ArcadePointsBot/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"IdentityModel.OidcClient.OidcClient": "Warning",
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Warning",
"Microsoft.EntityFrameworkCore.Infrastructure": "Warning"
"Microsoft.EntityFrameworkCore.Infrastructure": "Warning",
"TwitchLib.Api.Core.HttpCallHandlers.TwitchHttpClient": "Verbose"
}
},
"WriteTo": [
Expand Down
33 changes: 33 additions & 0 deletions tests/ArcadePointsBot.Tests/ArcadePointsBot.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ArcadePointsBot\ArcadePointsBot.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
59 changes: 59 additions & 0 deletions tests/ArcadePointsBot.Tests/StringUtilTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using ArcadePointsBot.Util;
using Xunit.Abstractions;

namespace ArcadePointsBot.Tests;

public class StringUtilTests(ITestOutputHelper testLogger)
{
const double SimilarityThresholdMin = 0.6;

[Theory]
[MemberData(nameof(CalcTestData))]
public void TestCalculateSimilarity(string a, string b, double min, double max)
{
var result = StringUtils.CalculateSimilarity(a, b);
testLogger.WriteLine("Calculated similarity: {0}", result);
Assert.InRange(result, min, max);
}

[Theory]
[MemberData(nameof(CalcTestData))]
public void TestCalculateSimilarityWithRounding(string a, string b, double min, double max)
{
var result = StringUtils.CalculateSimilarity(a, b, true);
testLogger.WriteLine("Calculated similarity: {0}", result);
Assert.InRange(result, min, max);
}

[Theory]
[MemberData(nameof(ComparisonTestData))]
public void TestSimilarity(string a, string b, double threshold, bool expected)
{
var result = StringUtils.TestSimilarity(a, b, threshold, true);
testLogger.WriteLine("Calculated similarity: {0}", result);
Assert.Equal(expected, result);
}
[Theory]
[MemberData(nameof(ComparisonTestData))]
public void TestSimilarityWithRounding(string a, string b, double threshold, bool expected)
{
var result = StringUtils.TestSimilarity(a, b, threshold, true);
testLogger.WriteLine("Calculated similarity: {0}", result);
Assert.Equal(expected, result);
}

public static IEnumerable<object[]> CalcTestData()
{
yield return new object[] { "Escape From Tarkov", "Escape From Tarkov: Arena", SimilarityThresholdMin, 0.9 };
yield return new object[] { "Escape From Tarkov", "Escape From", SimilarityThresholdMin, 0.9 };
yield return new object[] { "Escape From Tarkov", "Escape Torkiv", SimilarityThresholdMin, 0.9 };
yield return new object[] { "Rocket League", "Escape From Tarkov", 0, SimilarityThresholdMin };
}
public static IEnumerable<object[]> ComparisonTestData()
{
yield return new object[] { "Escape From Tarkov", "Escape From Tarkov: Arena", SimilarityThresholdMin, true };
yield return new object[] { "Escape From Tarkov", "Escape From", SimilarityThresholdMin, true };
yield return new object[] { "Escape From Tarkov", "Escape Torkiv", SimilarityThresholdMin, true };
yield return new object[] { "Rocket League", "Escape From Tarkov", SimilarityThresholdMin, false };
}
}