diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..cd967fc
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,25 @@
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/.idea
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..acbec22
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,25 @@
+name: ci
+
+on:
+ push:
+ branches:
+ - 'main'
+
+jobs:
+ docker:
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Login to Docker Registry
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERUSER }}
+ password: ${{ secrets.DOCKERPASSWORD }}
+ registry: ${{ secrets.DOCKERREGISTRY }}
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Publish Website
+ run: dotnet publish Website/Website.csproj --os linux --arch x64 -c Release -p:PublishProfile=DefaultContainer -p:ContainerRegistry=${{ secrets.DOCKERREGISTRY }} -p:ContainerRepository=mecatol_archives_website
+ - name: Publish API
+ run: dotnet publish API/API.csproj --os linux --arch x64 -c Release -p:PublishProfile=DefaultContainer -p:ContainerRegistry=${{ secrets.DOCKERREGISTRY }} -p:ContainerRepository=mecatol_archives_api
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..97f6a61
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,367 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Oo]ut/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+*.db
+*.db-wal
+WebAPI/bookofnuffle.db-shm
+*.db-shm
diff --git a/.idea/.idea.MecatolArchives/.idea/.gitignore b/.idea/.idea.MecatolArchives/.idea/.gitignore
new file mode 100644
index 0000000..2c740ae
--- /dev/null
+++ b/.idea/.idea.MecatolArchives/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/contentModel.xml
+/modules.xml
+/projectSettingsUpdater.xml
+/.idea.MecatolArchives.iml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.idea.MecatolArchives/.idea/indexLayout.xml b/.idea/.idea.MecatolArchives/.idea/indexLayout.xml
new file mode 100644
index 0000000..1ead36c
--- /dev/null
+++ b/.idea/.idea.MecatolArchives/.idea/indexLayout.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ .github
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.MecatolArchives/.idea/vcs.xml b/.idea/.idea.MecatolArchives/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/.idea.MecatolArchives/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/API.Client/API.Client.csproj b/API.Client/API.Client.csproj
new file mode 100644
index 0000000..33506a5
--- /dev/null
+++ b/API.Client/API.Client.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/API.Client/Auth/BasicAuthHeaderProvider.cs b/API.Client/Auth/BasicAuthHeaderProvider.cs
new file mode 100644
index 0000000..d354351
--- /dev/null
+++ b/API.Client/Auth/BasicAuthHeaderProvider.cs
@@ -0,0 +1,14 @@
+using System.Net.Http.Headers;
+using System.Text;
+
+namespace Hesketh.MecatolArchives.API.Client.Auth;
+
+public class BasicAuthHeaderProvider(IAdminCredentialStore credentialStore) : IAuthHeaderProvider
+{
+ public async Task GetHeaderAsync()
+ {
+ var details = await credentialStore.GetDetailsAsync();
+ return new AuthenticationHeaderValue("Basic",
+ Convert.ToBase64String(Encoding.ASCII.GetBytes($"{details.Username}:{details.Password}")));
+ }
+}
\ No newline at end of file
diff --git a/API.Client/Auth/IAdminCredentialStore.cs b/API.Client/Auth/IAdminCredentialStore.cs
new file mode 100644
index 0000000..0843a20
--- /dev/null
+++ b/API.Client/Auth/IAdminCredentialStore.cs
@@ -0,0 +1,9 @@
+namespace Hesketh.MecatolArchives.API.Client.Auth;
+
+public interface IAdminCredentialStore
+{
+ Task SetDetailsAsync(string username, string password);
+ Task<(string Username, string Password)> GetDetailsAsync();
+ Task AreDetailsSet();
+ Task ResetAsync();
+}
\ No newline at end of file
diff --git a/API.Client/Auth/IAuthHeaderProvider.cs b/API.Client/Auth/IAuthHeaderProvider.cs
new file mode 100644
index 0000000..389373a
--- /dev/null
+++ b/API.Client/Auth/IAuthHeaderProvider.cs
@@ -0,0 +1,8 @@
+using System.Net.Http.Headers;
+
+namespace Hesketh.MecatolArchives.API.Client.Auth;
+
+public interface IAuthHeaderProvider
+{
+ Task GetHeaderAsync();
+}
\ No newline at end of file
diff --git a/API.Client/Clients/AdminClient.cs b/API.Client/Clients/AdminClient.cs
new file mode 100644
index 0000000..a8fe351
--- /dev/null
+++ b/API.Client/Clients/AdminClient.cs
@@ -0,0 +1,23 @@
+using System.Net.Http.Json;
+using Hesketh.MecatolArchives.API.Client.Auth;
+
+namespace Hesketh.MecatolArchives.API.Client.Clients;
+
+public sealed class AdminClient : Client
+{
+ public AdminClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider)
+ {
+ }
+
+ public async Task Confirm()
+ {
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, "admin");
+ requestMessage.Headers.Authorization = await AuthHeaderProvider.GetHeaderAsync();
+
+ using var response = await HttpClient.SendAsync(requestMessage);
+ response.EnsureSuccessStatusCode();
+
+ var content = await response.Content.ReadFromJsonAsync();
+ return content;
+ }
+}
\ No newline at end of file
diff --git a/API.Client/Clients/Client.cs b/API.Client/Clients/Client.cs
new file mode 100644
index 0000000..ddeab2f
--- /dev/null
+++ b/API.Client/Clients/Client.cs
@@ -0,0 +1,90 @@
+using System.Net.Http.Json;
+using Hesketh.MecatolArchives.API.Client.Auth;
+using Hesketh.MecatolArchives.API.Data;
+
+namespace Hesketh.MecatolArchives.API.Client.Clients;
+
+public abstract class Client
+{
+ protected Client(HttpClient client, IAuthHeaderProvider authHeaderProvider)
+ {
+ HttpClient = client;
+ AuthHeaderProvider = authHeaderProvider;
+ }
+
+ protected HttpClient HttpClient { get; }
+ public IAuthHeaderProvider AuthHeaderProvider { get; }
+
+ protected async Task PostAsync(string endpoint, TPost entity) where TTransfer : IEntity
+ {
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Post, endpoint);
+ requestMessage.Headers.Authorization = await AuthHeaderProvider.GetHeaderAsync();
+ requestMessage.Content = JsonContent.Create(entity);
+
+ using var response = await HttpClient.SendAsync(requestMessage);
+ response.EnsureSuccessStatusCode();
+
+ var content = await response.Content.ReadFromJsonAsync()
+ ?? throw new InvalidOperationException("Expected valid JSON response from request");
+
+ return content;
+ }
+
+ protected async Task PostAsync(string endpoint, TPostType content)
+ {
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Post, endpoint);
+ requestMessage.Headers.Authorization = await AuthHeaderProvider.GetHeaderAsync();
+ requestMessage.Content = JsonContent.Create(content);
+
+ using var response = await HttpClient.SendAsync(requestMessage);
+ response.EnsureSuccessStatusCode();
+ }
+
+ protected async Task PutAsync(string endpoint, TPut entity)
+ where TTransfer : IEntity
+ where TPut : IEntity
+ {
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Put, endpoint);
+ requestMessage.Headers.Authorization = await AuthHeaderProvider.GetHeaderAsync();
+ requestMessage.Content = JsonContent.Create(entity);
+
+ using var response = await HttpClient.SendAsync(requestMessage);
+ response.EnsureSuccessStatusCode();
+
+ var content = await response.Content.ReadFromJsonAsync()
+ ?? throw new InvalidOperationException("Expected valid JSON response from request");
+
+ return content;
+ }
+
+ protected async Task DeleteAsync(string endpoint)
+ {
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Delete, endpoint);
+ requestMessage.Headers.Authorization = await AuthHeaderProvider.GetHeaderAsync();
+
+ using var response = await HttpClient.SendAsync(requestMessage);
+ response.EnsureSuccessStatusCode();
+ }
+
+ protected async Task GetAsync(string endpoint)
+ {
+ using var response = await HttpClient.GetAsync(endpoint);
+ response.EnsureSuccessStatusCode();
+
+ var content = await response.Content.ReadFromJsonAsync()
+ ?? throw new InvalidOperationException("Expected valid JSON response from request");
+
+ return content;
+ }
+
+ protected async Task> GetAllAsync(string endpoint)
+ {
+ using var response = await HttpClient.GetAsync(endpoint);
+ response.EnsureSuccessStatusCode();
+
+ var content = await response.Content.ReadFromJsonAsync>()
+ ?? throw new InvalidOperationException("Expected valid JSON response from request");
+
+ return content;
+ }
+}
\ No newline at end of file
diff --git a/API.Client/Clients/ColourClient.cs b/API.Client/Clients/ColourClient.cs
new file mode 100644
index 0000000..1140a19
--- /dev/null
+++ b/API.Client/Clients/ColourClient.cs
@@ -0,0 +1,12 @@
+using Hesketh.MecatolArchives.API.Client.Auth;
+using Hesketh.MecatolArchives.API.Data;
+
+namespace Hesketh.MecatolArchives.API.Client.Clients;
+
+public sealed class ColourClient : EntityClient
+{
+ public ColourClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider,
+ "colours")
+ {
+ }
+}
\ No newline at end of file
diff --git a/API.Client/Clients/EntityClient.cs b/API.Client/Clients/EntityClient.cs
new file mode 100644
index 0000000..d829e4c
--- /dev/null
+++ b/API.Client/Clients/EntityClient.cs
@@ -0,0 +1,47 @@
+using Hesketh.MecatolArchives.API.Client.Auth;
+using Hesketh.MecatolArchives.API.Data;
+
+namespace Hesketh.MecatolArchives.API.Client.Clients;
+
+public class EntityClient : Client
+ where TTransfer : IEntity
+ where TPut : IEntity
+{
+ private readonly string _endpoint;
+
+ public EntityClient(HttpClient client, IAuthHeaderProvider authHeaderProvider, string endpoint) : base(client,
+ authHeaderProvider)
+ {
+ _endpoint = endpoint;
+ }
+
+ public async Task GetAsync(Guid identifier)
+ {
+ return await GetAsync($"{_endpoint}/{identifier}");
+ }
+
+ public async Task> GetAsync()
+ {
+ return await GetAllAsync(_endpoint);
+ }
+
+ public async Task DeleteAsync(Guid identifier)
+ {
+ await DeleteAsync($"{_endpoint}/{identifier}");
+ }
+
+ public async Task DeleteAsync(TTransfer entity)
+ {
+ await DeleteAsync(entity.Identifier);
+ }
+
+ public async Task UpdateAsync(TPut entity)
+ {
+ return await PutAsync(_endpoint, entity);
+ }
+
+ public async Task CreateAsync(TPost entity)
+ {
+ return await PostAsync(_endpoint, entity);
+ }
+}
\ No newline at end of file
diff --git a/API.Client/Clients/ExpansionClient.cs b/API.Client/Clients/ExpansionClient.cs
new file mode 100644
index 0000000..03dd4e4
--- /dev/null
+++ b/API.Client/Clients/ExpansionClient.cs
@@ -0,0 +1,12 @@
+using Hesketh.MecatolArchives.API.Client.Auth;
+using Hesketh.MecatolArchives.API.Data;
+
+namespace Hesketh.MecatolArchives.API.Client.Clients;
+
+public sealed class ExpansionClient : EntityClient
+{
+ public ExpansionClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider,
+ "expansions")
+ {
+ }
+}
\ No newline at end of file
diff --git a/API.Client/Clients/FactionClient.cs b/API.Client/Clients/FactionClient.cs
new file mode 100644
index 0000000..11bb900
--- /dev/null
+++ b/API.Client/Clients/FactionClient.cs
@@ -0,0 +1,12 @@
+using Hesketh.MecatolArchives.API.Client.Auth;
+using Hesketh.MecatolArchives.API.Data;
+
+namespace Hesketh.MecatolArchives.API.Client.Clients;
+
+public sealed class FactionClient : EntityClient
+{
+ public FactionClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider,
+ "factions")
+ {
+ }
+}
\ No newline at end of file
diff --git a/API.Client/Clients/PersonClient.cs b/API.Client/Clients/PersonClient.cs
new file mode 100644
index 0000000..37b89ab
--- /dev/null
+++ b/API.Client/Clients/PersonClient.cs
@@ -0,0 +1,12 @@
+using Hesketh.MecatolArchives.API.Client.Auth;
+using Hesketh.MecatolArchives.API.Data;
+
+namespace Hesketh.MecatolArchives.API.Client.Clients;
+
+public sealed class PersonClient : EntityClient
+{
+ public PersonClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider,
+ "people")
+ {
+ }
+}
\ No newline at end of file
diff --git a/API.Client/Clients/PlayClient.cs b/API.Client/Clients/PlayClient.cs
new file mode 100644
index 0000000..d5d9376
--- /dev/null
+++ b/API.Client/Clients/PlayClient.cs
@@ -0,0 +1,24 @@
+using Hesketh.MecatolArchives.API.Client.Auth;
+using Hesketh.MecatolArchives.API.Data;
+
+namespace Hesketh.MecatolArchives.API.Client.Clients
+{
+ public sealed class PlayClient : EntityClient
+ {
+ private const string Endpoint = "plays";
+
+ public PlayClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider, Endpoint)
+ {
+ }
+
+ public async Task> GetPersonsPlaysAsync(Guid personIdentifier)
+ {
+ return await GetAsync>($"{Endpoint}/person={personIdentifier}");
+ }
+
+ public async Task> GetFactionsPlaysAsync(Guid factionIdentifier)
+ {
+ return await GetAsync>($"{Endpoint}/faction={factionIdentifier}");
+ }
+ }
+}
diff --git a/API.Client/Clients/StatisticClient.cs b/API.Client/Clients/StatisticClient.cs
new file mode 100644
index 0000000..34ee122
--- /dev/null
+++ b/API.Client/Clients/StatisticClient.cs
@@ -0,0 +1,32 @@
+using Hesketh.MecatolArchives.API.Client.Auth;
+using Hesketh.MecatolArchives.API.Data;
+
+namespace Hesketh.MecatolArchives.API.Client.Clients;
+
+public sealed class StatisticClient : Client
+{
+ public StatisticClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider)
+ {
+ }
+
+ public async Task GetFactionStatistics()
+ {
+ return await GetAsync("stats/factions");
+ }
+
+ public async Task GetPersonFactionStatistics(Guid personIdentifier)
+ {
+ return await GetAsync($"stats/factions/person={personIdentifier}");
+ }
+
+ public async Task GetPeopleStatistics()
+ {
+ return await GetAsync("stats/people");
+ }
+
+ public async Task GetFactionPeopleStatistics(Guid factionIdentifier)
+ {
+ return await GetAsync($"stats/people/faction={factionIdentifier}");
+ }
+
+}
\ No newline at end of file
diff --git a/API.Client/Clients/VariantClient.cs b/API.Client/Clients/VariantClient.cs
new file mode 100644
index 0000000..461d989
--- /dev/null
+++ b/API.Client/Clients/VariantClient.cs
@@ -0,0 +1,12 @@
+using Hesketh.MecatolArchives.API.Client.Auth;
+using Hesketh.MecatolArchives.API.Data;
+
+namespace Hesketh.MecatolArchives.API.Client.Clients;
+
+public sealed class VariantClient : EntityClient
+{
+ public VariantClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider,
+ "variants")
+ {
+ }
+}
\ No newline at end of file
diff --git a/API.Client/Extensions/ServiceCollectionExtensions.cs b/API.Client/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..f819094
--- /dev/null
+++ b/API.Client/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,30 @@
+using Hesketh.MecatolArchives.API.Client.Auth;
+using Hesketh.MecatolArchives.API.Client.Clients;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Hesketh.MecatolArchives.API.Client.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// The assumption is that you have already configured a IAdminCredentialStore
+ ///
+ /// The application services you are adding too
+ /// The address of the Mecatol Archives API Server
+ ///
+ public static IServiceCollection AddClients(this IServiceCollection serviceCollection, Uri apiAddress)
+ {
+ serviceCollection.AddTransient();
+
+ serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; });
+ serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; });
+ serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; });
+ serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; });
+ serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; });
+ serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; });
+ serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; });
+ serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; });
+
+ return serviceCollection;
+ }
+}
\ No newline at end of file
diff --git a/API.Client/Global.cs b/API.Client/Global.cs
new file mode 100644
index 0000000..4104b20
--- /dev/null
+++ b/API.Client/Global.cs
@@ -0,0 +1,3 @@
+global using Get = Hesketh.MecatolArchives.API.Data;
+global using Post = Hesketh.MecatolArchives.API.Data.Post;
+global using Put = Hesketh.MecatolArchives.API.Data.Put;
\ No newline at end of file
diff --git a/API.Data/API.Data.csproj b/API.Data/API.Data.csproj
new file mode 100644
index 0000000..fa71b7a
--- /dev/null
+++ b/API.Data/API.Data.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/API.Data/Colour.cs b/API.Data/Colour.cs
new file mode 100644
index 0000000..a0ee358
--- /dev/null
+++ b/API.Data/Colour.cs
@@ -0,0 +1,8 @@
+namespace Hesketh.MecatolArchives.API.Data;
+
+public sealed class Colour : IEntity
+{
+ public string Name { get; set; } = null!;
+ public string Hex { get; set; } = null!;
+ public Guid Identifier { get; set; }
+}
\ No newline at end of file
diff --git a/API.Data/Expansion.cs b/API.Data/Expansion.cs
new file mode 100644
index 0000000..64b0a5f
--- /dev/null
+++ b/API.Data/Expansion.cs
@@ -0,0 +1,7 @@
+namespace Hesketh.MecatolArchives.API.Data;
+
+public sealed class Expansion : IEntity
+{
+ public string Name { get; set; } = null!;
+ public Guid Identifier { get; set; }
+}
\ No newline at end of file
diff --git a/API.Data/Faction.cs b/API.Data/Faction.cs
new file mode 100644
index 0000000..fb02971
--- /dev/null
+++ b/API.Data/Faction.cs
@@ -0,0 +1,7 @@
+namespace Hesketh.MecatolArchives.API.Data;
+
+public sealed class Faction : IEntity
+{
+ public string Name { get; set; } = null!;
+ public Guid Identifier { get; set; }
+}
\ No newline at end of file
diff --git a/API.Data/IEntity.cs b/API.Data/IEntity.cs
new file mode 100644
index 0000000..0ab935b
--- /dev/null
+++ b/API.Data/IEntity.cs
@@ -0,0 +1,6 @@
+namespace Hesketh.MecatolArchives.API.Data;
+
+public interface IEntity
+{
+ public Guid Identifier { get; }
+}
\ No newline at end of file
diff --git a/API.Data/Person.cs b/API.Data/Person.cs
new file mode 100644
index 0000000..15d1ca9
--- /dev/null
+++ b/API.Data/Person.cs
@@ -0,0 +1,29 @@
+using System.Text;
+
+namespace Hesketh.MecatolArchives.API.Data;
+
+public sealed class Person : IEntity
+{
+ public string Name { get; set; } = null!;
+ public Guid Identifier { get; set; }
+
+ public string Initials
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(Name))
+ return string.Empty;
+
+ var sb = new StringBuilder();
+ var split = Name.Split(' ');
+
+ foreach (var item in split)
+ {
+ var firstCharacter = item[0];
+ sb.Append(firstCharacter);
+ }
+
+ return sb.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/API.Data/Play.cs b/API.Data/Play.cs
new file mode 100644
index 0000000..ed80c06
--- /dev/null
+++ b/API.Data/Play.cs
@@ -0,0 +1,14 @@
+namespace Hesketh.MecatolArchives.API.Data;
+
+public sealed class Play : IEntity
+{
+ public DateTime UtcDate { get; set; } = DateTime.UtcNow;
+ public double RulesVersion { get; set; } = 1.0;
+ public uint PointGoal { get; set; } = 10;
+ public string? Map { get; set; } = null;
+
+ public ICollection Players { get; set; } = null!;
+ public ICollection Expansions { get; set; } = null!;
+ public ICollection Variants { get; set; } = null!;
+ public Guid Identifier { get; set; }
+}
\ No newline at end of file
diff --git a/API.Data/Player.cs b/API.Data/Player.cs
new file mode 100644
index 0000000..78075e5
--- /dev/null
+++ b/API.Data/Player.cs
@@ -0,0 +1,13 @@
+namespace Hesketh.MecatolArchives.API.Data;
+
+public sealed class Player : IEntity
+{
+ public uint Points { get; set; } = 0;
+ public bool Winner { get; set; } = false;
+ public bool Eliminated { get; set; } = false;
+
+ public Person Person { get; set; } = null!;
+ public Faction Faction { get; set; } = null!;
+ public Colour Colour { get; set; } = null!;
+ public Guid Identifier { get; set; }
+}
\ No newline at end of file
diff --git a/API.Data/Post/Colour.cs b/API.Data/Post/Colour.cs
new file mode 100644
index 0000000..cba864d
--- /dev/null
+++ b/API.Data/Post/Colour.cs
@@ -0,0 +1,7 @@
+namespace Hesketh.MecatolArchives.API.Data.Post;
+
+public sealed class Colour
+{
+ public string Name { get; set; } = null!;
+ public string Hex { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/API.Data/Post/Expansion.cs b/API.Data/Post/Expansion.cs
new file mode 100644
index 0000000..c5e056c
--- /dev/null
+++ b/API.Data/Post/Expansion.cs
@@ -0,0 +1,6 @@
+namespace Hesketh.MecatolArchives.API.Data.Post;
+
+public sealed class Expansion
+{
+ public string Name { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/API.Data/Post/Faction.cs b/API.Data/Post/Faction.cs
new file mode 100644
index 0000000..6c8d583
--- /dev/null
+++ b/API.Data/Post/Faction.cs
@@ -0,0 +1,6 @@
+namespace Hesketh.MecatolArchives.API.Data.Post;
+
+public sealed class Faction
+{
+ public string Name { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/API.Data/Post/Person.cs b/API.Data/Post/Person.cs
new file mode 100644
index 0000000..d79a92f
--- /dev/null
+++ b/API.Data/Post/Person.cs
@@ -0,0 +1,6 @@
+namespace Hesketh.MecatolArchives.API.Data.Post;
+
+public sealed class Person
+{
+ public string Name { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/API.Data/Post/Play.cs b/API.Data/Post/Play.cs
new file mode 100644
index 0000000..d55dfb3
--- /dev/null
+++ b/API.Data/Post/Play.cs
@@ -0,0 +1,13 @@
+namespace Hesketh.MecatolArchives.API.Data.Post;
+
+public sealed class Play
+{
+ public DateTime UtcDate { get; set; } = DateTime.Now;
+ public double RulesVersion { get; set; } = 1.0;
+ public uint PointGoal { get; set; } = 10;
+ public string? Map { get; set; } = null;
+
+ public ICollection VariantIdentifiers { get; set; } = new HashSet();
+ public ICollection ExpansionIdentifiers { get; set; } = new HashSet();
+ public ICollection Players { get; set; } = new HashSet();
+}
\ No newline at end of file
diff --git a/API.Data/Post/Player.cs b/API.Data/Post/Player.cs
new file mode 100644
index 0000000..b8f3041
--- /dev/null
+++ b/API.Data/Post/Player.cs
@@ -0,0 +1,24 @@
+namespace Hesketh.MecatolArchives.API.Data.Post;
+
+public sealed class Player
+{
+ public Player() {}
+ public Player(Data.Player model)
+ {
+ Points = model.Points;
+ Winner = model.Winner;
+ Eliminated = model.Eliminated;
+
+ PersonIdentifier = model.Person.Identifier;
+ FactionIdentifier = model.Faction.Identifier;
+ ColourIdentifier = model.Colour.Identifier;
+ }
+
+ public uint Points { get; set; }
+ public bool Winner { get; set; }
+ public bool Eliminated { get; set; }
+
+ public Guid PersonIdentifier { get; set; }
+ public Guid FactionIdentifier { get; set; }
+ public Guid ColourIdentifier { get; set; }
+}
\ No newline at end of file
diff --git a/API.Data/Post/Variant.cs b/API.Data/Post/Variant.cs
new file mode 100644
index 0000000..49abd7a
--- /dev/null
+++ b/API.Data/Post/Variant.cs
@@ -0,0 +1,6 @@
+namespace Hesketh.MecatolArchives.API.Data.Post;
+
+public sealed class Variant
+{
+ public string Name { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/API.Data/Put/Play.cs b/API.Data/Put/Play.cs
new file mode 100644
index 0000000..a402c68
--- /dev/null
+++ b/API.Data/Put/Play.cs
@@ -0,0 +1,30 @@
+namespace Hesketh.MecatolArchives.API.Data.Put;
+
+public sealed class Play : IEntity
+{
+ public Play() {}
+ public Play(Data.Play model)
+ {
+ Identifier = model.Identifier;
+
+ UtcDate = model.UtcDate;
+ RulesVersion = model.RulesVersion;
+ PointGoal = model.PointGoal;
+ Map = model.Map;
+
+ VariantIdentifiers = new List(model.Variants.Select(x => x.Identifier));
+ ExpansionIdentifiers = new List(model.Expansions.Select(x => x.Identifier));
+ Players = new List(model.Players.Select(x => new Post.Player(x)));
+ }
+
+ public Guid Identifier { get; set; }
+
+ public DateTime UtcDate { get; set; } = DateTime.Now;
+ public double RulesVersion { get; set; } = 1.0;
+ public uint PointGoal { get; set; } = 10;
+ public string? Map { get; set; }
+
+ public ICollection VariantIdentifiers { get; set; } = new HashSet();
+ public ICollection ExpansionIdentifiers { get; set; } = new HashSet();
+ public ICollection Players { get; set; } = new HashSet();
+}
\ No newline at end of file
diff --git a/API.Data/Statistic.cs b/API.Data/Statistic.cs
new file mode 100644
index 0000000..26e3936
--- /dev/null
+++ b/API.Data/Statistic.cs
@@ -0,0 +1,23 @@
+namespace Hesketh.MecatolArchives.API.Data;
+
+public class Statistic
+{
+ public Guid? LinkedIdentifier { get; set; }
+ public string Name { get; }
+ public int Wins { get; set; } = 0;
+ public int Plays { get; set; } = 0;
+ public int Losses => Plays - Wins;
+ public double WinPercentage => Plays > 0 ? (double)Wins / Plays * 100 : 0;
+
+ // The point percentage is the total percentage of Points earned in each game
+ // So if you earned 4/5 points in one game and 10/10 in another. That is 180% total
+ // But it is in the range 0...1
+ public double PointPercentSum { get; set; }
+ public double PointPercentage => Plays > 0 ? (double)PointPercentSum / Plays * 100 : 0;
+
+ public Statistic(string name, Guid? linkedIdentifier)
+ {
+ Name = name;
+ LinkedIdentifier = linkedIdentifier;
+ }
+}
\ No newline at end of file
diff --git a/API.Data/Statistics.cs b/API.Data/Statistics.cs
new file mode 100644
index 0000000..a0f1217
--- /dev/null
+++ b/API.Data/Statistics.cs
@@ -0,0 +1,16 @@
+namespace Hesketh.MecatolArchives.API.Data;
+
+public class Statistics
+{
+ public Statistic Overall { get; set; } = new("Overall", null);
+ public List Stats { get; set; } = new();
+
+ public void AddStatistic(Statistic statistic)
+ {
+ Stats.Add(statistic);
+
+ Overall.Plays += statistic.Plays;
+ Overall.Wins += statistic.Wins;
+ Overall.PointPercentSum += statistic.PointPercentSum;
+ }
+}
diff --git a/API.Data/Variant.cs b/API.Data/Variant.cs
new file mode 100644
index 0000000..f87a7b5
--- /dev/null
+++ b/API.Data/Variant.cs
@@ -0,0 +1,7 @@
+namespace Hesketh.MecatolArchives.API.Data;
+
+public sealed class Variant : IEntity
+{
+ public string Name { get; set; } = null!;
+ public Guid Identifier { get; set; }
+}
\ No newline at end of file
diff --git a/API/API.csproj b/API/API.csproj
new file mode 100644
index 0000000..39b3c4b
--- /dev/null
+++ b/API/API.csproj
@@ -0,0 +1,44 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+ Linux
+ f29964b2-e609-453e-8e0c-5cf5fee26a49
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .dockerignore
+
+
+
+
diff --git a/API/Auth/AdminAccountOptions.cs b/API/Auth/AdminAccountOptions.cs
new file mode 100644
index 0000000..22df083
--- /dev/null
+++ b/API/Auth/AdminAccountOptions.cs
@@ -0,0 +1,26 @@
+namespace Hesketh.MecatolArchives.API.Auth;
+
+public class AdminAccountOptions
+{
+ public const string SectionName = "Admin";
+
+ public AdminAccount? Account { get; set; } = null;
+
+ public void Validate()
+ {
+ if (Account == null)
+ throw new InvalidOperationException("No admin account details specified");
+
+ if (string.IsNullOrWhiteSpace(Account.Username))
+ throw new InvalidOperationException($"Admin account does not have a valid username");
+
+ if (string.IsNullOrEmpty(Account.Username))
+ throw new InvalidOperationException($"Admin account does not have a valid password");
+ }
+}
+
+public sealed class AdminAccount
+{
+ public string Username { get; set; } = null!;
+ public string Password { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/API/Auth/BasicAuthenticationHandler.cs b/API/Auth/BasicAuthenticationHandler.cs
new file mode 100644
index 0000000..38219e1
--- /dev/null
+++ b/API/Auth/BasicAuthenticationHandler.cs
@@ -0,0 +1,45 @@
+using System.Security.Claims;
+using System.Text;
+using System.Text.Encodings.Web;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Options;
+
+namespace Hesketh.MecatolArchives.API.Auth;
+
+public class BasicAuthenticationHandler : AuthenticationHandler
+{
+ private readonly IBasicAuthenticationService _authenticationService;
+
+ public BasicAuthenticationHandler(
+ IBasicAuthenticationService authenticationService,
+ IOptionsMonitor options,
+ ILoggerFactory logger,
+ UrlEncoder encoder) :
+ base(options, logger, encoder)
+ {
+ _authenticationService = authenticationService;
+ }
+
+ protected override async Task HandleAuthenticateAsync()
+ {
+ var authorizationHeader = Request.Headers["Authorization"].ToString();
+ if (authorizationHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
+ {
+ var token = authorizationHeader.Substring("Basic ".Length).Trim();
+ var credentialsAsEncodedString = Encoding.UTF8.GetString(Convert.FromBase64String(token));
+ var credentials = credentialsAsEncodedString.Split(':');
+ if (_authenticationService.Authenticate(credentials[0], credentials[1]))
+ {
+ var claims = new[] { new Claim("name", credentials[0]), new Claim(ClaimTypes.Role, "Admin") };
+ var identity = new ClaimsIdentity(claims, "Basic");
+ var claimsPrincipal = new ClaimsPrincipal(identity);
+ return await Task.FromResult(
+ AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, Scheme.Name)));
+ }
+ }
+
+ Response.StatusCode = 401;
+ Response.Headers.Append("WWW-Authenticate", "Basic realm=\"joydipkanjilal.com\"");
+ return await Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
+ }
+}
\ No newline at end of file
diff --git a/API/Auth/BasicAuthenticationService.cs b/API/Auth/BasicAuthenticationService.cs
new file mode 100644
index 0000000..58820c5
--- /dev/null
+++ b/API/Auth/BasicAuthenticationService.cs
@@ -0,0 +1,28 @@
+using Microsoft.Extensions.Options;
+
+namespace Hesketh.MecatolArchives.API.Auth;
+
+public class BasicAuthenticationService : IBasicAuthenticationService
+{
+ private readonly AdminAccountOptions _adminAccountOptions;
+
+ public BasicAuthenticationService(IOptions adminAccountOptions)
+ {
+ _adminAccountOptions = adminAccountOptions.Value;
+ _adminAccountOptions.Validate();
+ }
+
+ public bool Authenticate(string username, string password)
+ {
+ if (_adminAccountOptions.Account == null)
+ return false;
+
+ if (!_adminAccountOptions.Account.Username.Equals(username, StringComparison.InvariantCultureIgnoreCase))
+ return false;
+
+ if (!_adminAccountOptions.Account.Password.Equals(password, StringComparison.InvariantCulture))
+ return false;
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/API/Auth/IBasicAuthenticationService.cs b/API/Auth/IBasicAuthenticationService.cs
new file mode 100644
index 0000000..e3d4a44
--- /dev/null
+++ b/API/Auth/IBasicAuthenticationService.cs
@@ -0,0 +1,6 @@
+namespace Hesketh.MecatolArchives.API.Auth;
+
+public interface IBasicAuthenticationService
+{
+ bool Authenticate(string username, string password);
+}
\ No newline at end of file
diff --git a/API/Controllers/AdminController.cs b/API/Controllers/AdminController.cs
new file mode 100644
index 0000000..fe33232
--- /dev/null
+++ b/API/Controllers/AdminController.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hesketh.MecatolArchives.API.Controllers;
+
+[ApiController]
+[Route("admin")]
+[Authorize]
+public sealed class AdminController : ControllerBase
+{
+ [HttpGet]
+ public ActionResult ConfirmAsync()
+ {
+ return Ok(true);
+ }
+}
\ No newline at end of file
diff --git a/API/Controllers/ColourController.cs b/API/Controllers/ColourController.cs
new file mode 100644
index 0000000..15841a5
--- /dev/null
+++ b/API/Controllers/ColourController.cs
@@ -0,0 +1,53 @@
+using AutoMapper;
+using Hesketh.MecatolArchives.API.Helpers;
+using Hesketh.MecatolArchives.DB;
+using Hesketh.MecatolArchives.DB.Models;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hesketh.MecatolArchives.API.Controllers;
+
+[ApiController]
+[Route("colours")]
+[Authorize]
+public sealed class ColourController : ControllerBase
+{
+ private readonly CrudControllerHelper _crud;
+
+ public ColourController(MecatolArchivesDbContext db, IMapper mapper)
+ {
+ _crud = new CrudControllerHelper(db, mapper);
+ }
+
+ [HttpGet("{identifier}")]
+ [AllowAnonymous]
+ public async Task> Get(Guid identifier)
+ {
+ return await _crud.GetAsync(identifier);
+ }
+
+ [HttpGet]
+ [AllowAnonymous]
+ public async Task>> Get()
+ {
+ return await _crud.GetAsync();
+ }
+
+ [HttpPost]
+ public async Task> Post(Data.Post.Colour model)
+ {
+ return await _crud.PostAsync(model);
+ }
+
+ [HttpPut]
+ public async Task> Put(Data.Colour model)
+ {
+ return await _crud.PutAsync(model);
+ }
+
+ [HttpDelete("{identifier}")]
+ public async Task Delete(Guid identifier)
+ {
+ return await _crud.DeleteAsync(identifier);
+ }
+}
\ No newline at end of file
diff --git a/API/Controllers/ExpansionController.cs b/API/Controllers/ExpansionController.cs
new file mode 100644
index 0000000..61dc4d5
--- /dev/null
+++ b/API/Controllers/ExpansionController.cs
@@ -0,0 +1,53 @@
+using AutoMapper;
+using Hesketh.MecatolArchives.API.Helpers;
+using Hesketh.MecatolArchives.DB;
+using Hesketh.MecatolArchives.DB.Models;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hesketh.MecatolArchives.API.Controllers;
+
+[ApiController]
+[Route("expansions")]
+[Authorize]
+public sealed class ExpansionController : ControllerBase
+{
+ private readonly CrudControllerHelper _crud;
+
+ public ExpansionController(MecatolArchivesDbContext db, IMapper mapper)
+ {
+ _crud = new CrudControllerHelper(db, mapper);
+ }
+
+ [HttpGet("{identifier}")]
+ [AllowAnonymous]
+ public async Task> Get(Guid identifier)
+ {
+ return await _crud.GetAsync(identifier);
+ }
+
+ [HttpGet]
+ [AllowAnonymous]
+ public async Task>> Get()
+ {
+ return await _crud.GetAsync();
+ }
+
+ [HttpPost]
+ public async Task> Post(Data.Post.Expansion model)
+ {
+ return await _crud.PostAsync(model);
+ }
+
+ [HttpPut]
+ public async Task> Put(Data.Expansion model)
+ {
+ return await _crud.PutAsync(model);
+ }
+
+ [HttpDelete("{identifier}")]
+ public async Task Delete(Guid identifier)
+ {
+ return await _crud.DeleteAsync(identifier);
+ }
+}
\ No newline at end of file
diff --git a/API/Controllers/FactionController.cs b/API/Controllers/FactionController.cs
new file mode 100644
index 0000000..b26e721
--- /dev/null
+++ b/API/Controllers/FactionController.cs
@@ -0,0 +1,57 @@
+using AutoMapper;
+using Hesketh.MecatolArchives.API.Helpers;
+using Hesketh.MecatolArchives.DB;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Faction = Hesketh.MecatolArchives.DB.Models.Faction;
+
+namespace Hesketh.MecatolArchives.API.Controllers;
+
+[ApiController]
+[Route("factions")]
+[Authorize]
+public sealed class FactionController : ControllerBase
+{
+ private readonly MecatolArchivesDbContext _db;
+ private readonly IMapper _mapper;
+ private readonly CrudControllerHelper _crud;
+
+ public FactionController(MecatolArchivesDbContext db, IMapper mapper)
+ {
+ _db = db;
+ _mapper = mapper;
+ _crud = new CrudControllerHelper(db, mapper);
+ }
+
+ [HttpGet("{identifier}")]
+ [AllowAnonymous]
+ public async Task> Get(Guid identifier)
+ {
+ return await _crud.GetAsync(identifier);
+ }
+
+ [HttpGet]
+ [AllowAnonymous]
+ public async Task>> Get()
+ {
+ return await _crud.GetAsync();
+ }
+
+ [HttpPost]
+ public async Task> Post(Data.Post.Faction model)
+ {
+ return await _crud.PostAsync(model);
+ }
+
+ [HttpPut]
+ public async Task> Put(Data.Faction model)
+ {
+ return await _crud.PutAsync(model);
+ }
+
+ [HttpDelete("{identifier}")]
+ public async Task Delete(Guid identifier)
+ {
+ return await _crud.DeleteAsync(identifier);
+ }
+}
\ No newline at end of file
diff --git a/API/Controllers/PersonController.cs b/API/Controllers/PersonController.cs
new file mode 100644
index 0000000..c328d6d
--- /dev/null
+++ b/API/Controllers/PersonController.cs
@@ -0,0 +1,53 @@
+using AutoMapper;
+using Hesketh.MecatolArchives.API.Helpers;
+using Hesketh.MecatolArchives.DB;
+using Hesketh.MecatolArchives.DB.Models;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hesketh.MecatolArchives.API.Controllers;
+
+[ApiController]
+[Route("people")]
+[Authorize]
+public sealed class PersonController : ControllerBase
+{
+ private readonly CrudControllerHelper _crud;
+
+ public PersonController(MecatolArchivesDbContext db, IMapper mapper)
+ {
+ _crud = new CrudControllerHelper(db, mapper);
+ }
+
+ [HttpGet("{identifier}")]
+ [AllowAnonymous]
+ public async Task> Get(Guid identifier)
+ {
+ return await _crud.GetAsync(identifier);
+ }
+
+ [HttpGet]
+ [AllowAnonymous]
+ public async Task>> Get()
+ {
+ return await _crud.GetAsync();
+ }
+
+ [HttpPost]
+ public async Task> Post(Data.Post.Person model)
+ {
+ return await _crud.PostAsync(model);
+ }
+
+ [HttpPut]
+ public async Task> Put(Data.Person model)
+ {
+ return await _crud.PutAsync(model);
+ }
+
+ [HttpDelete("{identifier}")]
+ public async Task Delete(Guid identifier)
+ {
+ return await _crud.DeleteAsync(identifier);
+ }
+}
\ No newline at end of file
diff --git a/API/Controllers/PlayController.cs b/API/Controllers/PlayController.cs
new file mode 100644
index 0000000..008d39a
--- /dev/null
+++ b/API/Controllers/PlayController.cs
@@ -0,0 +1,198 @@
+using AutoMapper;
+using Hesketh.MecatolArchives.API.Helpers;
+using Hesketh.MecatolArchives.DB;
+using Hesketh.MecatolArchives.DB.Models;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace Hesketh.MecatolArchives.API.Controllers;
+
+[ApiController]
+[Route("plays")]
+[Authorize]
+public sealed class PlayController : ControllerBase
+{
+ private readonly MecatolArchivesDbContext _db;
+ private readonly IMapper _mapper;
+ private readonly CrudControllerHelper _crud;
+
+ public PlayController(MecatolArchivesDbContext db, IMapper mapper)
+ {
+ _db = db;
+ _mapper = mapper;
+ _crud = new CrudControllerHelper(db, mapper);
+ }
+
+ [HttpGet("{identifier}")]
+ [AllowAnonymous]
+ public async Task> Get(Guid identifier)
+ {
+ var res = await _db.GetDbSet()
+ .Include(x => x.Players).ThenInclude(x => x.Person)
+ .Include(x => x.Players).ThenInclude(x => x.Faction)
+ .Include(x => x.Players).ThenInclude(x => x.Colour)
+ .Include(x => x.Variants)
+ .Include(x => x.Expansions)
+ .FirstOrDefaultAsync(x => x.Identifier == identifier);
+ var mapped = _mapper.Map(res);
+ return Ok(mapped);
+ }
+
+ [HttpGet]
+ [AllowAnonymous]
+ public async Task>> Get()
+ {
+ var res = await _db.GetDbSet()
+ .OrderByDescending(x => x.UtcDate)
+ .Include(x => x.Players).ThenInclude(x => x.Person)
+ .Include(x => x.Players).ThenInclude(x => x.Faction)
+ .Include(x => x.Players).ThenInclude(x => x.Colour)
+ .Include(x => x.Variants)
+ .Include(x => x.Expansions)
+ .ToListAsync();
+ var mapped = _mapper.Map>(res);
+ return Ok(mapped);
+ }
+
+ [HttpGet("person={personIdentifier}")]
+ [AllowAnonymous]
+ public async Task>> GetWithPlayer(Guid personIdentifier)
+ {
+ var res = await _db.GetDbSet()
+ .OrderByDescending(x => x.UtcDate)
+ .Include(x => x.Players).ThenInclude(x => x.Person)
+ .Include(x => x.Players).ThenInclude(x => x.Faction)
+ .Include(x => x.Players).ThenInclude(x => x.Colour)
+ .Include(x => x.Variants)
+ .Include(x => x.Expansions)
+ .Where(x => x.Players.Any(y => y.Person.Identifier == personIdentifier))
+ .ToListAsync();
+ var mapped = _mapper.Map>(res);
+ return Ok(mapped);
+ }
+
+ [HttpGet("faction={factionIdentifier}")]
+ [AllowAnonymous]
+ public async Task>> GetWithFaction(Guid factionIdentifier)
+ {
+ var res = await _db.GetDbSet()
+ .OrderByDescending(x => x.UtcDate)
+ .Include(x => x.Players).ThenInclude(x => x.Person)
+ .Include(x => x.Players).ThenInclude(x => x.Faction)
+ .Include(x => x.Players).ThenInclude(x => x.Colour)
+ .Include(x => x.Variants)
+ .Include(x => x.Expansions)
+ .Where(x => x.Players.Any(y => y.Faction.Identifier == factionIdentifier))
+ .ToListAsync();
+ var mapped = _mapper.Map>(res);
+ return Ok(mapped);
+ }
+
+ [HttpPost]
+ public async Task> Post(Data.Post.Play model)
+ {
+ var entity = _mapper.Map(model);
+
+ entity.Variants = await _db.Variants.Where(x => model.VariantIdentifiers.Contains(x.Identifier)).ToListAsync();
+ if (entity.Variants.Count != model.VariantIdentifiers.Count)
+ return NotFound("Variants could not be located");
+
+ entity.Expansions = await _db.Expansions.Where(x => model.ExpansionIdentifiers.Contains(x.Identifier)).ToListAsync();
+ if (entity.Expansions.Count != model.ExpansionIdentifiers.Count)
+ return NotFound("Expansions could not be located");
+
+ entity.Players = new List();
+ foreach (var playerPost in model.Players)
+ {
+ var playerEntity = _mapper.Map(playerPost);
+
+ var person = await _db.People.FirstOrDefaultAsync(x => x.Identifier == playerPost.PersonIdentifier);
+ if (person == null)
+ return NotFound($"Player person could not be found");
+
+ var faction = await _db.Factions.FirstOrDefaultAsync(x => x.Identifier == playerPost.FactionIdentifier);
+ if (faction == null)
+ return NotFound($"Player person could not be found");
+
+ var colour = await _db.Colours.FirstOrDefaultAsync(x => x.Identifier == playerPost.ColourIdentifier);
+ if (colour == null)
+ return NotFound($"Player colour could not be found");
+
+ playerEntity.Person = person;
+ playerEntity.Faction = faction;
+ playerEntity.Colour = colour;
+
+ entity.Players.Add(playerEntity);
+ playerEntity.Play = entity;
+ await _db.GetDbSet().AddAsync(playerEntity);
+ }
+
+ await _db.GetDbSet().AddAsync(entity);
+ await _db.SaveChangesAsync();
+
+ var mapped = _mapper.Map(entity);
+ return Ok(mapped);
+ }
+
+ [HttpPut]
+ public async Task> Put(Data.Put.Play model)
+ {
+ var existingEntity = await _db.GetDbSet()
+ .Include(x => x.Players).ThenInclude(x => x.Person)
+ .Include(x => x.Players).ThenInclude(x => x.Faction)
+ .Include(x => x.Players).ThenInclude(x => x.Colour)
+ .Include(x => x.Variants)
+ .Include(x => x.Expansions)
+ .FirstOrDefaultAsync(x => x.Identifier == model.Identifier);
+ if (existingEntity == null)
+ return NotFound("Play could not be located");
+
+ var entity = _mapper.Map(model, existingEntity);
+
+ entity.Variants = await _db.Variants.Where(x => model.VariantIdentifiers.Contains(x.Identifier)).ToListAsync();
+ if (entity.Variants.Count != model.VariantIdentifiers.Count)
+ return NotFound("Variants could not be located");
+
+ entity.Expansions = await _db.Expansions.Where(x => model.ExpansionIdentifiers.Contains(x.Identifier)).ToListAsync();
+ if (entity.Expansions.Count != model.ExpansionIdentifiers.Count)
+ return NotFound("Expansions could not be located");
+
+ entity.Players.Clear();
+ foreach (var playerPost in model.Players)
+ {
+ var playerEntity = _mapper.Map(playerPost);
+
+ var person = await _db.People.FirstOrDefaultAsync(x => x.Identifier == playerPost.PersonIdentifier);
+ if (person == null)
+ return NotFound($"Player person could not be found");
+
+ var faction = await _db.Factions.FirstOrDefaultAsync(x => x.Identifier == playerPost.FactionIdentifier);
+ if (faction == null)
+ return NotFound($"Player person could not be found");
+
+ var colour = await _db.Colours.FirstOrDefaultAsync(x => x.Identifier == playerPost.ColourIdentifier);
+ if (colour == null)
+ return NotFound($"Player colour could not be found");
+
+ playerEntity.Person = person;
+ playerEntity.Faction = faction;
+ playerEntity.Colour = colour;
+
+ entity.Players.Add(playerEntity);
+ playerEntity.Play = entity;
+ await _db.Players.AddAsync(playerEntity);
+ }
+
+ await _db.SaveChangesAsync();
+
+ var mapped = _mapper.Map(entity);
+ return Ok(mapped);
+ }
+
+ [HttpDelete("{identifier}")]
+ public async Task Delete(Guid identifier)
+ {
+ return await _crud.DeleteAsync(identifier);
+ }
+}
\ No newline at end of file
diff --git a/API/Controllers/StatsController.cs b/API/Controllers/StatsController.cs
new file mode 100644
index 0000000..165237b
--- /dev/null
+++ b/API/Controllers/StatsController.cs
@@ -0,0 +1,182 @@
+using Hesketh.MecatolArchives.API.Data;
+using Hesketh.MecatolArchives.DB;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace Hesketh.MecatolArchives.API.Controllers;
+
+[ApiController]
+[Route("stats")]
+public sealed class StatsController : ControllerBase
+{
+ private readonly MecatolArchivesDbContext _db;
+
+ public StatsController(MecatolArchivesDbContext db)
+ {
+ _db = db;
+ }
+
+ [HttpGet("factions/person={personIdentifier}")]
+ public ActionResult GetPlayersFactions(Guid personIdentifier)
+ {
+ var overall = new Statistics();
+
+ var statsLookup = new Dictionary();
+ foreach (var faction in _db.Factions.Where(x => x.Name != MecatolArchivesDbContext.UnknownName))
+ {
+ statsLookup[faction.Identifier] = new Statistic(faction.Name, faction.Identifier);
+ }
+
+ foreach (var play in _db.Plays.Include(x => x.Players)
+ .ThenInclude(x => x.Faction)
+ .Include(x => x.Players).ThenInclude(x => x.Person)
+ .Where(x => x.Players.Any(y => y.Winner)))
+ {
+ foreach (var player in play.Players.Where(x => x.Person.Identifier == personIdentifier))
+ {
+ if (!statsLookup.TryGetValue(player.Faction.Identifier, out var stats))
+ {
+ stats = new Statistic(player.Faction.Name, player.Faction.Identifier);
+ statsLookup[player.Faction.Identifier] = stats;
+ }
+
+ stats.Plays += 1;
+
+ if (player.Winner)
+ {
+ stats.Wins += 1;
+ }
+
+ var pointPercentage = (double)player.Points / play.PointGoal;
+ stats.PointPercentSum += pointPercentage;
+ }
+ }
+
+ foreach (var (id, stat) in statsLookup)
+ {
+ overall.AddStatistic(stat);
+ }
+
+ return Ok(overall);
+ }
+
+ [HttpGet("factions")]
+ public ActionResult GetFactionStats()
+ {
+ var overall = new Statistics();
+
+ var statsLookup = new Dictionary();
+ foreach (var faction in _db.Factions.Where(x => x.Name != MecatolArchivesDbContext.UnknownName))
+ {
+ statsLookup[faction.Identifier] = new Statistic(faction.Name, faction.Identifier);
+ }
+
+ foreach (var play in _db.Plays.Include(x => x.Players)
+ .ThenInclude(x => x.Faction)
+ .Where(x => x.Players.Any(y => y.Winner)))
+ {
+ foreach (var player in play.Players)
+ {
+ if (!statsLookup.TryGetValue(player.Faction.Identifier, out var stats))
+ {
+ stats = new Statistic(player.Faction.Name, player.Faction.Identifier);
+ statsLookup[player.Faction.Identifier] = stats;
+ }
+
+ stats.Plays += 1;
+
+ if (player.Winner)
+ {
+ stats.Wins += 1;
+ }
+
+ var pointPercentage = (double)player.Points / play.PointGoal;
+ stats.PointPercentSum += pointPercentage;
+ }
+ }
+
+ foreach (var (id, stat) in statsLookup)
+ {
+ overall.AddStatistic(stat);
+ }
+
+ return Ok(overall);
+ }
+
+ [HttpGet("people")]
+ public ActionResult GetPeopleStats()
+ {
+ var overall = new Statistics();
+
+ var statsLookup = new Dictionary();
+ foreach (var play in _db.Plays.Include(x => x.Players)
+ .ThenInclude(x => x.Person)
+ .Where(x => x.Players.Any(y => y.Winner)))
+ {
+ foreach (var player in play.Players)
+ {
+ if (!statsLookup.TryGetValue(player.Person.Identifier, out var stats))
+ {
+ stats = new Statistic(player.Person.Name, player.Person.Identifier);
+ statsLookup[player.Person.Identifier] = stats;
+ }
+
+ stats.Plays += 1;
+
+ if (player.Winner)
+ {
+ stats.Wins += 1;
+ }
+
+ var pointPercentage = (double)player.Points / play.PointGoal;
+ stats.PointPercentSum += pointPercentage;
+ }
+ }
+
+ foreach (var (id, stat) in statsLookup)
+ {
+ overall.AddStatistic(stat);
+ }
+
+ return Ok(overall);
+ }
+
+ [HttpGet("people/faction={factionIdentifier}")]
+ public ActionResult GetFactionPeopleStats(Guid factionIdentifier)
+ {
+ var overall = new Statistics();
+
+ var statsLookup = new Dictionary();
+ foreach (var play in _db.Plays.Include(x => x.Players)
+ .ThenInclude(x => x.Person)
+ .Include(x => x.Players).ThenInclude(x => x.Faction)
+ .Where(x => x.Players.Any(y => y.Winner)))
+ {
+ foreach (var player in play.Players.Where(x => x.Faction.Identifier == factionIdentifier))
+ {
+ if (!statsLookup.TryGetValue(player.Person.Identifier, out var stats))
+ {
+ stats = new Statistic(player.Person.Name, player.Person.Identifier);
+ statsLookup[player.Person.Identifier] = stats;
+ }
+
+ stats.Plays += 1;
+
+ if (player.Winner)
+ {
+ stats.Wins += 1;
+ }
+
+ var pointPercentage = (double)player.Points / play.PointGoal;
+ stats.PointPercentSum += pointPercentage;
+ }
+ }
+
+ foreach (var (id, stat) in statsLookup)
+ {
+ overall.AddStatistic(stat);
+ }
+
+ return Ok(overall);
+ }
+}
\ No newline at end of file
diff --git a/API/Controllers/VariantController.cs b/API/Controllers/VariantController.cs
new file mode 100644
index 0000000..d201d79
--- /dev/null
+++ b/API/Controllers/VariantController.cs
@@ -0,0 +1,53 @@
+using AutoMapper;
+using Hesketh.MecatolArchives.API.Helpers;
+using Hesketh.MecatolArchives.DB;
+using Hesketh.MecatolArchives.DB.Models;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hesketh.MecatolArchives.API.Controllers;
+
+[ApiController]
+[Route("variants")]
+[Authorize]
+public sealed class VariantController : ControllerBase
+{
+ private readonly CrudControllerHelper _crud;
+
+ public VariantController(MecatolArchivesDbContext db, IMapper mapper)
+ {
+ _crud = new CrudControllerHelper(db, mapper);
+ }
+
+ [HttpGet("{identifier}")]
+ [AllowAnonymous]
+ public async Task> Get(Guid identifier)
+ {
+ return await _crud.GetAsync(identifier);
+ }
+
+ [HttpGet]
+ [AllowAnonymous]
+ public async Task>> Get()
+ {
+ return await _crud.GetAsync();
+ }
+
+ [HttpPost]
+ public async Task> Post(Data.Post.Variant model)
+ {
+ return await _crud.PostAsync(model);
+ }
+
+ [HttpPut]
+ public async Task> Put(Data.Variant model)
+ {
+ return await _crud.PutAsync(model);
+ }
+
+ [HttpDelete("{identifier}")]
+ public async Task Delete(Guid identifier)
+ {
+ return await _crud.DeleteAsync(identifier);
+ }
+}
\ No newline at end of file
diff --git a/API/Dockerfile b/API/Dockerfile
new file mode 100644
index 0000000..8da9a34
--- /dev/null
+++ b/API/Dockerfile
@@ -0,0 +1,25 @@
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+USER $APP_UID
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["API/API.csproj", "API/"]
+COPY ["API.Data/API.Data.csproj", "API.Data/"]
+COPY ["DB/DB.csproj", "DB/"]
+RUN dotnet restore "API/API.csproj"
+COPY . .
+WORKDIR "/src/API"
+RUN dotnet build "API.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "API.dll"]
diff --git a/API/Global.cs b/API/Global.cs
new file mode 100644
index 0000000..c100c60
--- /dev/null
+++ b/API/Global.cs
@@ -0,0 +1,4 @@
+global using DTM = Hesketh.MecatolArchives.API.Data;
+global using DTPost = Hesketh.MecatolArchives.API.Data.Post;
+global using DTPut = Hesketh.MecatolArchives.API.Data.Put;
+global using DBM = Hesketh.MecatolArchives.DB.Models;
\ No newline at end of file
diff --git a/API/Helpers/CrudControllerHelper.cs b/API/Helpers/CrudControllerHelper.cs
new file mode 100644
index 0000000..213187e
--- /dev/null
+++ b/API/Helpers/CrudControllerHelper.cs
@@ -0,0 +1,79 @@
+using AutoMapper;
+using Hesketh.MecatolArchives.DB;
+using Hesketh.MecatolArchives.DB.Models;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace Hesketh.MecatolArchives.API.Helpers;
+
+public sealed class CrudControllerHelper : ControllerBase
+ where TDatabase : class, IEntity, INamed
+ where TGet : Data.IEntity
+ where TPut : Data.IEntity
+{
+ private readonly MecatolArchivesDbContext _db;
+ private readonly IMapper _mapper;
+
+ public CrudControllerHelper(MecatolArchivesDbContext db,
+ IMapper mapper)
+ {
+ _db = db;
+ _mapper = mapper;
+ }
+
+ //[HttpGet("{identifier}")]
+ public async Task> GetAsync(Guid identifier)
+ {
+ var res = await _db.GetDbSet().FirstOrDefaultAsync(x => x.Identifier == identifier);
+ if (res == null) return NotFound();
+
+ var mapped = _mapper.Map(res);
+ return Ok(mapped);
+ }
+
+ //[HttpGet]
+ public async Task>> GetAsync()
+ {
+ var res = await _db.GetDbSet()
+ .OrderBy(x => x.Name)
+ .ToListAsync();
+ var mapped = _mapper.Map>(res);
+ return Ok(mapped);
+ }
+
+ //[HttpPost]
+ public async Task> PostAsync(TPost model)
+ {
+ var entity = _mapper.Map(model);
+ await _db.GetDbSet().AddAsync(entity);
+ await _db.SaveChangesAsync();
+
+ var mapped = _mapper.Map(entity);
+ return Ok(mapped);
+ }
+
+ //[HttpPut]
+ public async Task> PutAsync(TPut model)
+ {
+ var entity = await _db.GetDbSet().FirstOrDefaultAsync(x => x.Identifier == model.Identifier);
+ if (entity == null) return NotFound();
+
+ entity = _mapper.Map(model, entity);
+ await _db.SaveChangesAsync();
+
+ var mapped = _mapper.Map(entity);
+ return Ok(mapped);
+ }
+
+ //[HttpDelete("{identifier}")]
+ public async Task DeleteAsync(Guid identifier)
+ {
+ var entity = await _db.GetDbSet().FirstOrDefaultAsync(x => x.Identifier == identifier);
+ if (entity == null) return NotFound();
+
+ _db.GetDbSet().Remove(entity);
+ await _db.SaveChangesAsync();
+
+ return Ok();
+ }
+}
\ No newline at end of file
diff --git a/API/Helpers/DevelopmentDataSeedingHelper.cs b/API/Helpers/DevelopmentDataSeedingHelper.cs
new file mode 100644
index 0000000..5022288
--- /dev/null
+++ b/API/Helpers/DevelopmentDataSeedingHelper.cs
@@ -0,0 +1,196 @@
+using Hesketh.MecatolArchives.DB;
+
+namespace Hesketh.MecatolArchives.API.Helpers;
+
+public static class DevelopmentDataSeedingHelper
+{
+ private const uint PlaysToGenerate = 25;
+ private const int MinPlayersInPlay = 3;
+ private const int MaxPlayersInPlay = 8;
+ private const int MaxPoints = 10;
+ private const int MinPoints = 0;
+ private const int MaxDaysDifferenceInDate = 1000;
+
+ private const uint DataFieldsToGenerate = 10;
+
+ private static readonly Random Random = new Random(404);
+ private static readonly NounHelper NounHelper = new NounHelper(Random);
+
+ public static async Task Seed(MecatolArchivesDbContext db)
+ {
+ await GenerateDataFields(db);
+ await GeneratePlays(db);
+
+ await db.SaveChangesAsync();
+ }
+
+ private static async Task GeneratePlays(MecatolArchivesDbContext db)
+ {
+ for (int i = 0; i < PlaysToGenerate; i++)
+ {
+ var play = new DBM.Play()
+ {
+ Identifier = Guid.NewGuid(),
+ UtcDate = GetRandomUtcDate(),
+ PointGoal = 10,
+ RulesVersion = 2.0,
+ Map = null,
+ Expansions = GetRandomExpansions(db),
+ Variants = GetRandomVariants(db),
+ };
+ play.Players = GetRandomPlayers(db, play);
+ await db.AddAsync(play);
+ }
+ await db.SaveChangesAsync();
+ }
+
+ private static DBM.Colour GetRandomColour(MecatolArchivesDbContext db, ICollection invalid)
+ {
+ var index = Random.Next(0, db.Colours.Count(x => !invalid.Contains(x.Identifier)));
+ return db.Colours.Where(x => !invalid.Contains(x.Identifier)).Skip(index - 1).First();
+ }
+
+ private static DBM.Faction GetRandomFaction(MecatolArchivesDbContext db, ICollection invalid)
+ {
+ var index = Random.Next(0, db.Factions.Count(x => !invalid.Contains(x.Identifier)));
+ return db.Factions.Where(x => !invalid.Contains(x.Identifier)).Skip(index - 1).First();
+ }
+
+ private static DBM.Person GetRandomPerson(MecatolArchivesDbContext db, ICollection invalid)
+ {
+ var index = Random.Next(0, db.People.Count(x => !invalid.Contains(x.Identifier)));
+ return db.People.Where(x => !invalid.Contains(x.Identifier)).Skip(index - 1).First();
+ }
+
+ private static ICollection GetRandomPlayers(MecatolArchivesDbContext db, DBM.Play play)
+ {
+ var res = new List();
+ var playerCount = Random.Next(MinPlayersInPlay, MaxPlayersInPlay + 1);
+ for (int i = 0; i < playerCount; i++)
+ {
+ var player = new DBM.Player
+ {
+ Identifier = Guid.NewGuid(),
+ Person = GetRandomPerson(db, res.Select(x => x.Person.Identifier).ToList()),
+ Faction = GetRandomFaction(db, res.Select(x => x.Faction.Identifier).ToList()),
+ Colour = GetRandomColour(db, res.Select(x => x.Colour.Identifier).ToList()),
+ Points = (uint)Random.Next(MinPoints, MaxPoints + 1),
+ Eliminated = GetRandomBool(50),
+ };
+
+ db.Add(player);
+ player.Play = play;
+ res.Add(player);
+ }
+
+ var winner = res.OrderByDescending(x => x.Points).First();
+ winner.Winner = !GetRandomBool(10);
+
+ return res;
+ }
+
+ private static ICollection GetRandomExpansions(MecatolArchivesDbContext db)
+ {
+ var res = new List();
+ foreach (var expansion in db.Expansions)
+ {
+ if (GetRandomBool(1))
+ {
+ res.Add(expansion);
+ }
+ }
+ return res;
+ }
+
+ private static ICollection GetRandomVariants(MecatolArchivesDbContext db)
+ {
+ var res = new List();
+ foreach (var variant in db.Variants)
+ {
+ if (GetRandomBool(3))
+ {
+ res.Add(variant);
+ }
+ }
+ return res;
+ }
+ private static DateTime GetRandomUtcDate()
+ {
+ var modifier = Random.Next(-MaxDaysDifferenceInDate, MaxDaysDifferenceInDate);
+ return DateTime.UtcNow.AddDays(modifier);
+ }
+
+ private static bool GetRandomBool(int chance)
+ {
+ return Random.Next(0, chance) == 0;
+ }
+
+ private static async Task GenerateDataFields(MecatolArchivesDbContext db)
+ {
+ for (int i = 0; i < DataFieldsToGenerate; i++)
+ {
+ var person = GeneratePerson();
+ await db.AddAsync(person);
+
+ var colour = GenerateColour();
+ await db.AddAsync(colour);
+
+ var faction = GenerateFaction();
+ await db.AddAsync(faction);
+
+ var variant = GenerateVariant();
+ await db.AddAsync(variant);
+
+ var expansion = GenerateExpansion();
+ await db.AddAsync(expansion);
+ }
+
+ await db.SaveChangesAsync();
+ }
+
+ private static DBM.Person GeneratePerson()
+ {
+ return new DBM.Person
+ {
+ Identifier = Guid.NewGuid(),
+ Name = NounHelper.GetRandomNoun()
+ };
+ }
+
+ private static DBM.Colour GenerateColour()
+ {
+ return new DBM.Colour
+ {
+ Identifier = Guid.NewGuid(),
+ Name = NounHelper.GetRandomNoun(),
+ Hex = NounHelper.GetRandomColour()
+ };
+ }
+
+ private static DBM.Faction GenerateFaction()
+ {
+ return new DBM.Faction
+ {
+ Identifier = Guid.NewGuid(),
+ Name = NounHelper.GetRandomNoun()
+ };
+ }
+
+ private static DBM.Variant GenerateVariant()
+ {
+ return new DBM.Variant
+ {
+ Identifier = Guid.NewGuid(),
+ Name = NounHelper.GetRandomNoun()
+ };
+ }
+
+ private static DBM.Expansion GenerateExpansion()
+ {
+ return new DBM.Expansion
+ {
+ Identifier = Guid.NewGuid(),
+ Name = NounHelper.GetRandomNoun()
+ };
+ }
+}
\ No newline at end of file
diff --git a/API/Helpers/NounHelper.cs b/API/Helpers/NounHelper.cs
new file mode 100644
index 0000000..b6886de
--- /dev/null
+++ b/API/Helpers/NounHelper.cs
@@ -0,0 +1,6846 @@
+namespace Hesketh.MecatolArchives.API.Helpers;
+
+public class NounHelper
+{
+ private readonly Random _random;
+
+ #region Data
+ private static readonly string[] Nouns = new[]
+ {
+ "ATM",
+ "CD",
+ "SUV",
+ "TV",
+ "aardvark",
+ "abacus",
+ "abbey",
+ "abbreviation",
+ "abdomen",
+ "ability",
+ "abnormality",
+ "abolishment",
+ "abortion",
+ "abrogation",
+ "absence",
+ "abundance",
+ "abuse",
+ "academics",
+ "academy",
+ "accelerant",
+ "accelerator",
+ "accent",
+ "acceptance",
+ "access",
+ "accessory",
+ "accident",
+ "accommodation",
+ "accompanist",
+ "accomplishment",
+ "accord",
+ "accordance",
+ "accordion",
+ "account",
+ "accountability",
+ "accountant",
+ "accounting",
+ "accuracy",
+ "accusation",
+ "acetate",
+ "achievement",
+ "achiever",
+ "acid",
+ "acknowledgment",
+ "acorn",
+ "acoustics",
+ "acquaintance",
+ "acquisition",
+ "acre",
+ "acrylic",
+ "act",
+ "action",
+ "activation",
+ "activist",
+ "activity",
+ "actor",
+ "actress",
+ "acupuncture",
+ "ad",
+ "adaptation",
+ "adapter",
+ "addiction",
+ "addition",
+ "address",
+ "adjective",
+ "adjustment",
+ "admin",
+ "administration",
+ "administrator",
+ "admire",
+ "admission",
+ "adobe",
+ "adoption",
+ "adrenalin",
+ "adrenaline",
+ "adult",
+ "adulthood",
+ "advance",
+ "advancement",
+ "advantage",
+ "advent",
+ "adverb",
+ "advertisement",
+ "advertising",
+ "advice",
+ "adviser",
+ "advocacy",
+ "advocate",
+ "affair",
+ "affect",
+ "affidavit",
+ "affiliate",
+ "affinity",
+ "afoul",
+ "afterlife",
+ "aftermath",
+ "afternoon",
+ "aftershave",
+ "aftershock",
+ "afterthought",
+ "age",
+ "agency",
+ "agenda",
+ "agent",
+ "aggradation",
+ "aggression",
+ "aglet",
+ "agony",
+ "agreement",
+ "agriculture",
+ "aid",
+ "aide",
+ "aim",
+ "air",
+ "airbag",
+ "airbus",
+ "aircraft",
+ "airfare",
+ "airfield",
+ "airforce",
+ "airline",
+ "airmail",
+ "airman",
+ "airplane",
+ "airport",
+ "airship",
+ "airspace",
+ "alarm",
+ "alb",
+ "albatross",
+ "album",
+ "alcohol",
+ "alcove",
+ "alder",
+ "ale",
+ "alert",
+ "alfalfa",
+ "algebra",
+ "algorithm",
+ "alias",
+ "alibi",
+ "alien",
+ "allegation",
+ "allergist",
+ "alley",
+ "alliance",
+ "alligator",
+ "allocation",
+ "allowance",
+ "alloy",
+ "alluvium",
+ "almanac",
+ "almighty",
+ "almond",
+ "alpaca",
+ "alpenglow",
+ "alpenhorn",
+ "alpha",
+ "alphabet",
+ "altar",
+ "alteration",
+ "alternative",
+ "altitude",
+ "alto",
+ "aluminium",
+ "aluminum",
+ "amazement",
+ "amazon",
+ "ambassador",
+ "amber",
+ "ambience",
+ "ambiguity",
+ "ambition",
+ "ambulance",
+ "amendment",
+ "amenity",
+ "ammunition",
+ "amnesty",
+ "amount",
+ "amusement",
+ "anagram",
+ "analgesia",
+ "analog",
+ "analogue",
+ "analogy",
+ "analysis",
+ "analyst",
+ "analytics",
+ "anarchist",
+ "anarchy",
+ "anatomy",
+ "ancestor",
+ "anchovy",
+ "android",
+ "anesthesiologist",
+ "anesthesiology",
+ "angel",
+ "anger",
+ "angina",
+ "angiosperm",
+ "angle",
+ "angora",
+ "angstrom",
+ "anguish",
+ "animal",
+ "anime",
+ "anise",
+ "ankle",
+ "anklet",
+ "anniversary",
+ "announcement",
+ "annual",
+ "anorak",
+ "answer",
+ "ant",
+ "anteater",
+ "antecedent",
+ "antechamber",
+ "antelope",
+ "antennae",
+ "anterior",
+ "anthropology",
+ "antibody",
+ "anticipation",
+ "anticodon",
+ "antigen",
+ "antique",
+ "antiquity",
+ "antler",
+ "antling",
+ "anxiety",
+ "anybody",
+ "anyone",
+ "anything",
+ "anywhere",
+ "apartment",
+ "ape",
+ "aperitif",
+ "apology",
+ "app",
+ "apparatus",
+ "apparel",
+ "appeal",
+ "appearance",
+ "appellation",
+ "appendix",
+ "appetiser",
+ "appetite",
+ "appetizer",
+ "applause",
+ "apple",
+ "applewood",
+ "appliance",
+ "application",
+ "appointment",
+ "appreciation",
+ "apprehension",
+ "approach",
+ "appropriation",
+ "approval",
+ "apricot",
+ "apron",
+ "apse",
+ "aquarium",
+ "aquifer",
+ "arcade",
+ "arch",
+ "arch-rival",
+ "archaeologist",
+ "archaeology",
+ "archeology",
+ "archer",
+ "architect",
+ "architecture",
+ "archives",
+ "area",
+ "arena",
+ "argument",
+ "arithmetic",
+ "ark",
+ "arm",
+ "arm-rest",
+ "armadillo",
+ "armament",
+ "armchair",
+ "armoire",
+ "armor",
+ "armour",
+ "armpit",
+ "armrest",
+ "army",
+ "arrangement",
+ "array",
+ "arrest",
+ "arrival",
+ "arrogance",
+ "arrow",
+ "art",
+ "artery",
+ "arthur",
+ "artichoke",
+ "article",
+ "artifact",
+ "artificer",
+ "artist",
+ "ascend",
+ "ascent",
+ "ascot",
+ "ash",
+ "ashram",
+ "ashtray",
+ "aside",
+ "asparagus",
+ "aspect",
+ "asphalt",
+ "aspic",
+ "ass",
+ "assassination",
+ "assault",
+ "assembly",
+ "assertion",
+ "assessment",
+ "asset",
+ "assignment",
+ "assist",
+ "assistance",
+ "assistant",
+ "associate",
+ "association",
+ "assumption",
+ "assurance",
+ "asterisk",
+ "astrakhan",
+ "astrolabe",
+ "astrologer",
+ "astrology",
+ "astronomy",
+ "asymmetry",
+ "atelier",
+ "atheist",
+ "athlete",
+ "athletics",
+ "atmosphere",
+ "atom",
+ "atrium",
+ "attachment",
+ "attack",
+ "attacker",
+ "attainment",
+ "attempt",
+ "attendance",
+ "attendant",
+ "attention",
+ "attenuation",
+ "attic",
+ "attitude",
+ "attorney",
+ "attraction",
+ "attribute",
+ "auction",
+ "audience",
+ "audit",
+ "auditorium",
+ "aunt",
+ "authentication",
+ "authenticity",
+ "author",
+ "authorisation",
+ "authority",
+ "authorization",
+ "auto",
+ "autoimmunity",
+ "automation",
+ "automaton",
+ "autumn",
+ "availability",
+ "avalanche",
+ "avenue",
+ "average",
+ "avocado",
+ "award",
+ "awareness",
+ "awe",
+ "axis",
+ "azimuth",
+ "babe",
+ "baboon",
+ "babushka",
+ "baby",
+ "bachelor",
+ "back",
+ "back-up",
+ "backbone",
+ "backburn",
+ "backdrop",
+ "background",
+ "backpack",
+ "backup",
+ "backyard",
+ "bacon",
+ "bacterium",
+ "badge",
+ "badger",
+ "bafflement",
+ "bag",
+ "bagel",
+ "baggage",
+ "baggie",
+ "baggy",
+ "bagpipe",
+ "bail",
+ "bait",
+ "bake",
+ "baker",
+ "bakery",
+ "bakeware",
+ "balaclava",
+ "balalaika",
+ "balance",
+ "balcony",
+ "ball",
+ "ballet",
+ "balloon",
+ "balloonist",
+ "ballot",
+ "ballpark",
+ "bamboo",
+ "ban",
+ "banana",
+ "band",
+ "bandana",
+ "bandanna",
+ "bandolier",
+ "bandwidth",
+ "bangle",
+ "banjo",
+ "bank",
+ "bankbook",
+ "banker",
+ "banking",
+ "bankruptcy",
+ "banner",
+ "banquette",
+ "banyan",
+ "baobab",
+ "bar",
+ "barbecue",
+ "barbeque",
+ "barber",
+ "barbiturate",
+ "bargain",
+ "barge",
+ "baritone",
+ "barium",
+ "bark",
+ "barley",
+ "barn",
+ "barometer",
+ "barracks",
+ "barrage",
+ "barrel",
+ "barrier",
+ "barstool",
+ "bartender",
+ "base",
+ "baseball",
+ "baseboard",
+ "baseline",
+ "basement",
+ "basics",
+ "basil",
+ "basin",
+ "basis",
+ "basket",
+ "basketball",
+ "bass",
+ "bassinet",
+ "bassoon",
+ "bat",
+ "bath",
+ "bather",
+ "bathhouse",
+ "bathrobe",
+ "bathroom",
+ "bathtub",
+ "battalion",
+ "batter",
+ "battery",
+ "batting",
+ "battle",
+ "battleship",
+ "bay",
+ "bayou",
+ "beach",
+ "bead",
+ "beak",
+ "beam",
+ "bean",
+ "beancurd",
+ "beanie",
+ "beanstalk",
+ "bear",
+ "beard",
+ "beast",
+ "beastie",
+ "beat",
+ "beating",
+ "beauty",
+ "beaver",
+ "beck",
+ "bed",
+ "bedrock",
+ "bedroom",
+ "bee",
+ "beech",
+ "beef",
+ "beer",
+ "beet",
+ "beetle",
+ "beggar",
+ "beginner",
+ "beginning",
+ "begonia",
+ "behalf",
+ "behavior",
+ "behaviour",
+ "beheading",
+ "behest",
+ "behold",
+ "being",
+ "belfry",
+ "belief",
+ "believer",
+ "bell",
+ "belligerency",
+ "bellows",
+ "belly",
+ "belt",
+ "bench",
+ "bend",
+ "beneficiary",
+ "benefit",
+ "beret",
+ "berry",
+ "best-seller",
+ "bestseller",
+ "bet",
+ "beverage",
+ "beyond",
+ "bias",
+ "bibliography",
+ "bicycle",
+ "bid",
+ "bidder",
+ "bidding",
+ "bidet",
+ "bifocals",
+ "bijou",
+ "bike",
+ "bikini",
+ "bill",
+ "billboard",
+ "billing",
+ "billion",
+ "bin",
+ "binoculars",
+ "biology",
+ "biopsy",
+ "biosphere",
+ "biplane",
+ "birch",
+ "bird",
+ "bird-watcher",
+ "birdbath",
+ "birdcage",
+ "birdhouse",
+ "birth",
+ "birthday",
+ "biscuit",
+ "bit",
+ "bite",
+ "bitten",
+ "bitter",
+ "black",
+ "blackberry",
+ "blackbird",
+ "blackboard",
+ "blackfish",
+ "blackness",
+ "bladder",
+ "blade",
+ "blame",
+ "blank",
+ "blanket",
+ "blast",
+ "blazer",
+ "blend",
+ "blessing",
+ "blight",
+ "blind",
+ "blinker",
+ "blister",
+ "blizzard",
+ "block",
+ "blocker",
+ "blog",
+ "blogger",
+ "blood",
+ "bloodflow",
+ "bloom",
+ "bloomer",
+ "blossom",
+ "blouse",
+ "blow",
+ "blowgun",
+ "blowhole",
+ "blue",
+ "blueberry",
+ "blush",
+ "boar",
+ "board",
+ "boat",
+ "boatload",
+ "boatyard",
+ "bob",
+ "bobcat",
+ "body",
+ "bog",
+ "bolero",
+ "bolt",
+ "bomb",
+ "bomber",
+ "bombing",
+ "bond",
+ "bonding",
+ "bondsman",
+ "bone",
+ "bonfire",
+ "bongo",
+ "bonnet",
+ "bonsai",
+ "bonus",
+ "boogeyman",
+ "book",
+ "bookcase",
+ "bookend",
+ "booking",
+ "booklet",
+ "bookmark",
+ "boolean",
+ "boom",
+ "boon",
+ "boost",
+ "booster",
+ "boot",
+ "bootee",
+ "bootie",
+ "booty",
+ "border",
+ "bore",
+ "borrower",
+ "borrowing",
+ "bosom",
+ "boss",
+ "botany",
+ "bother",
+ "bottle",
+ "bottling",
+ "bottom",
+ "bottom-line",
+ "boudoir",
+ "bough",
+ "boulder",
+ "boulevard",
+ "boundary",
+ "bouquet",
+ "bourgeoisie",
+ "bout",
+ "boutique",
+ "bow",
+ "bower",
+ "bowl",
+ "bowler",
+ "bowling",
+ "bowtie",
+ "box",
+ "boxer",
+ "boxspring",
+ "boy",
+ "boycott",
+ "boyfriend",
+ "boyhood",
+ "boysenberry",
+ "bra",
+ "brace",
+ "bracelet",
+ "bracket",
+ "brain",
+ "brake",
+ "bran",
+ "branch",
+ "brand",
+ "brandy",
+ "brass",
+ "brassiere",
+ "bratwurst",
+ "bread",
+ "breadcrumb",
+ "breadfruit",
+ "break",
+ "breakdown",
+ "breakfast",
+ "breakpoint",
+ "breakthrough",
+ "breast",
+ "breastplate",
+ "breath",
+ "breeze",
+ "brewer",
+ "bribery",
+ "brick",
+ "bricklaying",
+ "bride",
+ "bridge",
+ "brief",
+ "briefing",
+ "briefly",
+ "briefs",
+ "brilliant",
+ "brink",
+ "brisket",
+ "broad",
+ "broadcast",
+ "broccoli",
+ "brochure",
+ "brocolli",
+ "broiler",
+ "broker",
+ "bronchitis",
+ "bronco",
+ "bronze",
+ "brooch",
+ "brood",
+ "brook",
+ "broom",
+ "brother",
+ "brother-in-law",
+ "brow",
+ "brown",
+ "brownie",
+ "browser",
+ "browsing",
+ "brunch",
+ "brush",
+ "brushfire",
+ "brushing",
+ "bubble",
+ "buck",
+ "bucket",
+ "buckle",
+ "buckwheat",
+ "bud",
+ "buddy",
+ "budget",
+ "buffalo",
+ "buffer",
+ "buffet",
+ "bug",
+ "buggy",
+ "bugle",
+ "builder",
+ "building",
+ "bulb",
+ "bulk",
+ "bull",
+ "bull-fighter",
+ "bulldozer",
+ "bullet",
+ "bump",
+ "bumper",
+ "bun",
+ "bunch",
+ "bungalow",
+ "bunghole",
+ "bunkhouse",
+ "burden",
+ "bureau",
+ "burglar",
+ "burial",
+ "burlesque",
+ "burn",
+ "burn-out",
+ "burning",
+ "burrito",
+ "burro",
+ "burrow",
+ "burst",
+ "bus",
+ "bush",
+ "business",
+ "businessman",
+ "bust",
+ "bustle",
+ "butane",
+ "butcher",
+ "butler",
+ "butter",
+ "butterfly",
+ "button",
+ "buy",
+ "buyer",
+ "buying",
+ "buzz",
+ "buzzard",
+ "c-clamp",
+ "cabana",
+ "cabbage",
+ "cabin",
+ "cabinet",
+ "cable",
+ "caboose",
+ "cacao",
+ "cactus",
+ "caddy",
+ "cadet",
+ "cafe",
+ "caffeine",
+ "caftan",
+ "cage",
+ "cake",
+ "calcification",
+ "calculation",
+ "calculator",
+ "calculus",
+ "calendar",
+ "calf",
+ "caliber",
+ "calibre",
+ "calico",
+ "call",
+ "calm",
+ "calorie",
+ "camel",
+ "cameo",
+ "camera",
+ "camp",
+ "campaign",
+ "campaigning",
+ "campanile",
+ "camper",
+ "campus",
+ "can",
+ "canal",
+ "cancer",
+ "candelabra",
+ "candidacy",
+ "candidate",
+ "candle",
+ "candy",
+ "cane",
+ "cannibal",
+ "cannon",
+ "canoe",
+ "canon",
+ "canopy",
+ "cantaloupe",
+ "canteen",
+ "canvas",
+ "cap",
+ "capability",
+ "capacity",
+ "cape",
+ "caper",
+ "capital",
+ "capitalism",
+ "capitulation",
+ "capon",
+ "cappelletti",
+ "cappuccino",
+ "captain",
+ "caption",
+ "captor",
+ "car",
+ "carabao",
+ "caramel",
+ "caravan",
+ "carbohydrate",
+ "carbon",
+ "carboxyl",
+ "card",
+ "cardboard",
+ "cardigan",
+ "care",
+ "career",
+ "cargo",
+ "caribou",
+ "carload",
+ "carnation",
+ "carnival",
+ "carol",
+ "carotene",
+ "carp",
+ "carpenter",
+ "carpet",
+ "carpeting",
+ "carport",
+ "carriage",
+ "carrier",
+ "carrot",
+ "carry",
+ "cart",
+ "cartel",
+ "carter",
+ "cartilage",
+ "cartload",
+ "cartoon",
+ "cartridge",
+ "carving",
+ "cascade",
+ "case",
+ "casement",
+ "cash",
+ "cashew",
+ "cashier",
+ "casino",
+ "casket",
+ "cassava",
+ "casserole",
+ "cassock",
+ "cast",
+ "castanet",
+ "castle",
+ "casualty",
+ "cat",
+ "catacomb",
+ "catalogue",
+ "catalysis",
+ "catalyst",
+ "catamaran",
+ "catastrophe",
+ "catch",
+ "catcher",
+ "category",
+ "caterpillar",
+ "cathedral",
+ "cation",
+ "catsup",
+ "cattle",
+ "cauliflower",
+ "causal",
+ "cause",
+ "causeway",
+ "caution",
+ "cave",
+ "caviar",
+ "cayenne",
+ "ceiling",
+ "celebration",
+ "celebrity",
+ "celeriac",
+ "celery",
+ "cell",
+ "cellar",
+ "cello",
+ "celsius",
+ "cement",
+ "cemetery",
+ "cenotaph",
+ "census",
+ "cent",
+ "center",
+ "centimeter",
+ "centre",
+ "centurion",
+ "century",
+ "cephalopod",
+ "ceramic",
+ "ceramics",
+ "cereal",
+ "ceremony",
+ "certainty",
+ "certificate",
+ "certification",
+ "cesspool",
+ "chafe",
+ "chain",
+ "chainstay",
+ "chair",
+ "chairlift",
+ "chairman",
+ "chairperson",
+ "chaise",
+ "chalet",
+ "chalice",
+ "chalk",
+ "challenge",
+ "chamber",
+ "champagne",
+ "champion",
+ "championship",
+ "chance",
+ "chandelier",
+ "change",
+ "channel",
+ "chaos",
+ "chap",
+ "chapel",
+ "chaplain",
+ "chapter",
+ "character",
+ "characteristic",
+ "characterization",
+ "chard",
+ "charge",
+ "charger",
+ "charity",
+ "charlatan",
+ "charm",
+ "charset",
+ "chart",
+ "charter",
+ "chasm",
+ "chassis",
+ "chastity",
+ "chasuble",
+ "chateau",
+ "chatter",
+ "chauffeur",
+ "chauvinist",
+ "check",
+ "checkbook",
+ "checking",
+ "checkout",
+ "checkroom",
+ "cheddar",
+ "cheek",
+ "cheer",
+ "cheese",
+ "cheesecake",
+ "cheetah",
+ "chef",
+ "chem",
+ "chemical",
+ "chemistry",
+ "chemotaxis",
+ "cheque",
+ "cherry",
+ "chess",
+ "chest",
+ "chestnut",
+ "chick",
+ "chicken",
+ "chicory",
+ "chief",
+ "chiffonier",
+ "child",
+ "childbirth",
+ "childhood",
+ "chili",
+ "chill",
+ "chime",
+ "chimpanzee",
+ "chin",
+ "chinchilla",
+ "chino",
+ "chip",
+ "chipmunk",
+ "chit-chat",
+ "chivalry",
+ "chive",
+ "chives",
+ "chocolate",
+ "choice",
+ "choir",
+ "choker",
+ "cholesterol",
+ "choosing",
+ "chop",
+ "chops",
+ "chopstick",
+ "chopsticks",
+ "chord",
+ "chorus",
+ "chow",
+ "chowder",
+ "chrome",
+ "chromolithograph",
+ "chronicle",
+ "chronograph",
+ "chronometer",
+ "chrysalis",
+ "chub",
+ "chuck",
+ "chug",
+ "church",
+ "churn",
+ "chutney",
+ "cicada",
+ "cigarette",
+ "cilantro",
+ "cinder",
+ "cinema",
+ "cinnamon",
+ "circadian",
+ "circle",
+ "circuit",
+ "circulation",
+ "circumference",
+ "circumstance",
+ "cirrhosis",
+ "cirrus",
+ "citizen",
+ "citizenship",
+ "citron",
+ "citrus",
+ "city",
+ "civilian",
+ "civilisation",
+ "civilization",
+ "claim",
+ "clam",
+ "clamp",
+ "clan",
+ "clank",
+ "clapboard",
+ "clarification",
+ "clarinet",
+ "clarity",
+ "clasp",
+ "class",
+ "classic",
+ "classification",
+ "classmate",
+ "classroom",
+ "clause",
+ "clave",
+ "clavicle",
+ "clavier",
+ "claw",
+ "clay",
+ "cleaner",
+ "clearance",
+ "clearing",
+ "cleat",
+ "cleavage",
+ "clef",
+ "cleft",
+ "clergyman",
+ "cleric",
+ "clerk",
+ "click",
+ "client",
+ "cliff",
+ "climate",
+ "climb",
+ "clinic",
+ "clip",
+ "clipboard",
+ "clipper",
+ "cloak",
+ "cloakroom",
+ "clock",
+ "clockwork",
+ "clogs",
+ "cloister",
+ "clone",
+ "close",
+ "closet",
+ "closing",
+ "closure",
+ "cloth",
+ "clothes",
+ "clothing",
+ "cloud",
+ "cloudburst",
+ "clove",
+ "clover",
+ "cloves",
+ "club",
+ "clue",
+ "cluster",
+ "clutch",
+ "co-producer",
+ "coach",
+ "coal",
+ "coalition",
+ "coast",
+ "coaster",
+ "coat",
+ "cob",
+ "cobbler",
+ "cobweb",
+ "cock",
+ "cockpit",
+ "cockroach",
+ "cocktail",
+ "cocoa",
+ "coconut",
+ "cod",
+ "code",
+ "codepage",
+ "codling",
+ "codon",
+ "codpiece",
+ "coevolution",
+ "cofactor",
+ "coffee",
+ "coffin",
+ "cohesion",
+ "cohort",
+ "coil",
+ "coin",
+ "coincidence",
+ "coinsurance",
+ "coke",
+ "cold",
+ "coleslaw",
+ "coliseum",
+ "collaboration",
+ "collagen",
+ "collapse",
+ "collar",
+ "collard",
+ "collateral",
+ "colleague",
+ "collection",
+ "collectivisation",
+ "collectivization",
+ "collector",
+ "college",
+ "collision",
+ "colloquy",
+ "colon",
+ "colonial",
+ "colonialism",
+ "colonisation",
+ "colonization",
+ "colony",
+ "color",
+ "colorlessness",
+ "colt",
+ "column",
+ "columnist",
+ "comb",
+ "combat",
+ "combination",
+ "combine",
+ "comeback",
+ "comedy",
+ "comestible",
+ "comfort",
+ "comfortable",
+ "comic",
+ "comics",
+ "comma",
+ "command",
+ "commander",
+ "commandment",
+ "comment",
+ "commerce",
+ "commercial",
+ "commission",
+ "commitment",
+ "committee",
+ "commodity",
+ "common",
+ "commonsense",
+ "commotion",
+ "communicant",
+ "communication",
+ "communion",
+ "communist",
+ "community",
+ "commuter",
+ "company",
+ "comparison",
+ "compass",
+ "compassion",
+ "compassionate",
+ "compensation",
+ "competence",
+ "competition",
+ "competitor",
+ "complaint",
+ "complement",
+ "completion",
+ "complex",
+ "complexity",
+ "compliance",
+ "complication",
+ "complicity",
+ "compliment",
+ "component",
+ "comportment",
+ "composer",
+ "composite",
+ "composition",
+ "compost",
+ "comprehension",
+ "compress",
+ "compromise",
+ "comptroller",
+ "compulsion",
+ "computer",
+ "comradeship",
+ "con",
+ "concentrate",
+ "concentration",
+ "concept",
+ "conception",
+ "concern",
+ "concert",
+ "conclusion",
+ "concrete",
+ "condition",
+ "conditioner",
+ "condominium",
+ "condor",
+ "conduct",
+ "conductor",
+ "cone",
+ "confectionery",
+ "conference",
+ "confidence",
+ "confidentiality",
+ "configuration",
+ "confirmation",
+ "conflict",
+ "conformation",
+ "confusion",
+ "conga",
+ "congo",
+ "congregation",
+ "congress",
+ "congressman",
+ "congressperson",
+ "conifer",
+ "connection",
+ "connotation",
+ "conscience",
+ "consciousness",
+ "consensus",
+ "consent",
+ "consequence",
+ "conservation",
+ "conservative",
+ "consideration",
+ "consignment",
+ "consist",
+ "consistency",
+ "console",
+ "consonant",
+ "conspiracy",
+ "conspirator",
+ "constant",
+ "constellation",
+ "constitution",
+ "constraint",
+ "construction",
+ "consul",
+ "consulate",
+ "consulting",
+ "consumer",
+ "consumption",
+ "contact",
+ "contact lens",
+ "contagion",
+ "container",
+ "content",
+ "contention",
+ "contest",
+ "context",
+ "continent",
+ "contingency",
+ "continuity",
+ "contour",
+ "contract",
+ "contractor",
+ "contrail",
+ "contrary",
+ "contrast",
+ "contribution",
+ "contributor",
+ "control",
+ "controller",
+ "controversy",
+ "convection",
+ "convenience",
+ "convention",
+ "conversation",
+ "conversion",
+ "convert",
+ "convertible",
+ "conviction",
+ "cook",
+ "cookbook",
+ "cookie",
+ "cooking",
+ "coonskin",
+ "cooperation",
+ "coordination",
+ "coordinator",
+ "cop",
+ "cop-out",
+ "cope",
+ "copper",
+ "copy",
+ "copying",
+ "copyright",
+ "copywriter",
+ "coral",
+ "cord",
+ "corduroy",
+ "core",
+ "cork",
+ "cormorant",
+ "corn",
+ "corner",
+ "cornerstone",
+ "cornet",
+ "cornflakes",
+ "cornmeal",
+ "corporal",
+ "corporation",
+ "corporatism",
+ "corps",
+ "corral",
+ "correspondence",
+ "correspondent",
+ "corridor",
+ "corruption",
+ "corsage",
+ "cosset",
+ "cost",
+ "costume",
+ "cot",
+ "cottage",
+ "cotton",
+ "couch",
+ "cougar",
+ "cough",
+ "council",
+ "councilman",
+ "councilor",
+ "councilperson",
+ "counsel",
+ "counseling",
+ "counselling",
+ "counsellor",
+ "counselor",
+ "count",
+ "counter",
+ "counter-force",
+ "counterpart",
+ "counterterrorism",
+ "countess",
+ "country",
+ "countryside",
+ "county",
+ "couple",
+ "coupon",
+ "courage",
+ "course",
+ "court",
+ "courthouse",
+ "courtroom",
+ "cousin",
+ "covariate",
+ "cover",
+ "coverage",
+ "coverall",
+ "cow",
+ "cowbell",
+ "cowboy",
+ "coyote",
+ "crab",
+ "crack",
+ "cracker",
+ "crackers",
+ "cradle",
+ "craft",
+ "craftsman",
+ "cranberry",
+ "crane",
+ "cranky",
+ "crap",
+ "crash",
+ "crate",
+ "cravat",
+ "craw",
+ "crawdad",
+ "crayfish",
+ "crayon",
+ "crazy",
+ "cream",
+ "creation",
+ "creationism",
+ "creationist",
+ "creative",
+ "creativity",
+ "creator",
+ "creature",
+ "creche",
+ "credential",
+ "credenza",
+ "credibility",
+ "credit",
+ "creditor",
+ "creek",
+ "creme brulee",
+ "crepe",
+ "crest",
+ "crew",
+ "crewman",
+ "crewmate",
+ "crewmember",
+ "crewmen",
+ "cria",
+ "crib",
+ "cribbage",
+ "cricket",
+ "cricketer",
+ "crime",
+ "criminal",
+ "crinoline",
+ "crisis",
+ "crisp",
+ "criteria",
+ "criterion",
+ "critic",
+ "criticism",
+ "crocodile",
+ "crocus",
+ "croissant",
+ "crook",
+ "crop",
+ "cross",
+ "cross-contamination",
+ "cross-stitch",
+ "crotch",
+ "croup",
+ "crow",
+ "crowd",
+ "crown",
+ "crucifixion",
+ "crude",
+ "cruelty",
+ "cruise",
+ "crumb",
+ "crunch",
+ "crusader",
+ "crush",
+ "crust",
+ "cry",
+ "crystal",
+ "crystallography",
+ "cub",
+ "cube",
+ "cuckoo",
+ "cucumber",
+ "cue",
+ "cuff-link",
+ "cuisine",
+ "cultivar",
+ "cultivator",
+ "culture",
+ "culvert",
+ "cummerbund",
+ "cup",
+ "cupboard",
+ "cupcake",
+ "cupola",
+ "curd",
+ "cure",
+ "curio",
+ "curiosity",
+ "curl",
+ "curler",
+ "currant",
+ "currency",
+ "current",
+ "curriculum",
+ "curry",
+ "curse",
+ "cursor",
+ "curtailment",
+ "curtain",
+ "curve",
+ "cushion",
+ "custard",
+ "custody",
+ "custom",
+ "customer",
+ "cut",
+ "cuticle",
+ "cutlet",
+ "cutover",
+ "cutting",
+ "cyclamen",
+ "cycle",
+ "cyclone",
+ "cyclooxygenase",
+ "cygnet",
+ "cylinder",
+ "cymbal",
+ "cynic",
+ "cyst",
+ "cytokine",
+ "cytoplasm",
+ "dad",
+ "daddy",
+ "daffodil",
+ "dagger",
+ "dahlia",
+ "daikon",
+ "daily",
+ "dairy",
+ "daisy",
+ "dam",
+ "damage",
+ "dame",
+ "damn",
+ "dance",
+ "dancer",
+ "dancing",
+ "dandelion",
+ "danger",
+ "dare",
+ "dark",
+ "darkness",
+ "darn",
+ "dart",
+ "dash",
+ "dashboard",
+ "data",
+ "database",
+ "date",
+ "daughter",
+ "dawn",
+ "day",
+ "daybed",
+ "daylight",
+ "dead",
+ "deadline",
+ "deal",
+ "dealer",
+ "dealing",
+ "dearest",
+ "death",
+ "deathwatch",
+ "debate",
+ "debris",
+ "debt",
+ "debtor",
+ "decade",
+ "decadence",
+ "decency",
+ "decimal",
+ "decision",
+ "decision-making",
+ "deck",
+ "declaration",
+ "declination",
+ "decline",
+ "decoder",
+ "decongestant",
+ "decoration",
+ "decrease",
+ "decryption",
+ "dedication",
+ "deduce",
+ "deduction",
+ "deed",
+ "deep",
+ "deer",
+ "default",
+ "defeat",
+ "defendant",
+ "defender",
+ "defense",
+ "deficit",
+ "definition",
+ "deformation",
+ "degradation",
+ "degree",
+ "delay",
+ "deliberation",
+ "delight",
+ "delivery",
+ "demand",
+ "democracy",
+ "democrat",
+ "demon",
+ "demur",
+ "den",
+ "denim",
+ "denominator",
+ "density",
+ "dentist",
+ "deodorant",
+ "department",
+ "departure",
+ "dependency",
+ "dependent",
+ "deployment",
+ "deposit",
+ "deposition",
+ "depot",
+ "depression",
+ "depressive",
+ "depth",
+ "deputy",
+ "derby",
+ "derivation",
+ "derivative",
+ "derrick",
+ "descendant",
+ "descent",
+ "description",
+ "desert",
+ "design",
+ "designation",
+ "designer",
+ "desire",
+ "desk",
+ "desktop",
+ "dessert",
+ "destination",
+ "destiny",
+ "destroyer",
+ "destruction",
+ "detail",
+ "detainee",
+ "detainment",
+ "detection",
+ "detective",
+ "detector",
+ "detention",
+ "determination",
+ "detour",
+ "devastation",
+ "developer",
+ "developing",
+ "development",
+ "developmental",
+ "deviance",
+ "deviation",
+ "device",
+ "devil",
+ "dew",
+ "dhow",
+ "diabetes",
+ "diadem",
+ "diagnosis",
+ "diagram",
+ "dial",
+ "dialect",
+ "dialogue",
+ "diam",
+ "diamond",
+ "diaper",
+ "diaphragm",
+ "diarist",
+ "diary",
+ "dibble",
+ "dick",
+ "dickey",
+ "dictaphone",
+ "dictator",
+ "diction",
+ "dictionary",
+ "die",
+ "diesel",
+ "diet",
+ "difference",
+ "differential",
+ "difficulty",
+ "diffuse",
+ "dig",
+ "digestion",
+ "digestive",
+ "digger",
+ "digging",
+ "digit",
+ "dignity",
+ "dilapidation",
+ "dill",
+ "dilution",
+ "dime",
+ "dimension",
+ "dimple",
+ "diner",
+ "dinghy",
+ "dining",
+ "dinner",
+ "dinosaur",
+ "dioxide",
+ "dip",
+ "diploma",
+ "diplomacy",
+ "dipstick",
+ "direction",
+ "directive",
+ "director",
+ "directory",
+ "dirndl",
+ "dirt",
+ "disability",
+ "disadvantage",
+ "disagreement",
+ "disappointment",
+ "disarmament",
+ "disaster",
+ "discharge",
+ "discipline",
+ "disclaimer",
+ "disclosure",
+ "disco",
+ "disconnection",
+ "discount",
+ "discourse",
+ "discovery",
+ "discrepancy",
+ "discretion",
+ "discrimination",
+ "discussion",
+ "disdain",
+ "disease",
+ "disembodiment",
+ "disengagement",
+ "disguise",
+ "disgust",
+ "dish",
+ "dishwasher",
+ "disk",
+ "disparity",
+ "dispatch",
+ "displacement",
+ "display",
+ "disposal",
+ "disposer",
+ "disposition",
+ "dispute",
+ "disregard",
+ "disruption",
+ "dissemination",
+ "dissonance",
+ "distance",
+ "distinction",
+ "distortion",
+ "distribution",
+ "distributor",
+ "district",
+ "divalent",
+ "divan",
+ "diver",
+ "diversity",
+ "divide",
+ "dividend",
+ "divider",
+ "divine",
+ "diving",
+ "division",
+ "divorce",
+ "doc",
+ "dock",
+ "doctor",
+ "doctorate",
+ "doctrine",
+ "document",
+ "documentary",
+ "documentation",
+ "doe",
+ "dog",
+ "doggie",
+ "dogsled",
+ "dogwood",
+ "doing",
+ "doll",
+ "dollar",
+ "dollop",
+ "dolman",
+ "dolor",
+ "dolphin",
+ "domain",
+ "dome",
+ "domination",
+ "donation",
+ "donkey",
+ "donor",
+ "donut",
+ "door",
+ "doorbell",
+ "doorknob",
+ "doorpost",
+ "doorway",
+ "dory",
+ "dose",
+ "dot",
+ "double",
+ "doubling",
+ "doubt",
+ "doubter",
+ "dough",
+ "doughnut",
+ "down",
+ "downfall",
+ "downforce",
+ "downgrade",
+ "download",
+ "downstairs",
+ "downtown",
+ "downturn",
+ "dozen",
+ "draft",
+ "drag",
+ "dragon",
+ "dragonfly",
+ "dragonfruit",
+ "dragster",
+ "drain",
+ "drainage",
+ "drake",
+ "drama",
+ "dramaturge",
+ "drapes",
+ "draw",
+ "drawbridge",
+ "drawer",
+ "drawing",
+ "dream",
+ "dreamer",
+ "dredger",
+ "dress",
+ "dresser",
+ "dressing",
+ "drill",
+ "drink",
+ "drinking",
+ "drive",
+ "driver",
+ "driveway",
+ "driving",
+ "drizzle",
+ "dromedary",
+ "drop",
+ "drudgery",
+ "drug",
+ "drum",
+ "drummer",
+ "drunk",
+ "dryer",
+ "duck",
+ "duckling",
+ "dud",
+ "dude",
+ "due",
+ "duel",
+ "dueling",
+ "duffel",
+ "dugout",
+ "dulcimer",
+ "dumbwaiter",
+ "dump",
+ "dump truck",
+ "dune",
+ "dune buggy",
+ "dungarees",
+ "dungeon",
+ "duplexer",
+ "duration",
+ "durian",
+ "dusk",
+ "dust",
+ "dust storm",
+ "duster",
+ "duty",
+ "dwarf",
+ "dwell",
+ "dwelling",
+ "dynamics",
+ "dynamite",
+ "dynamo",
+ "dynasty",
+ "dysfunction",
+ "e-book",
+ "e-mail",
+ "e-reader",
+ "eagle",
+ "eaglet",
+ "ear",
+ "eardrum",
+ "earmuffs",
+ "earnings",
+ "earplug",
+ "earring",
+ "earrings",
+ "earth",
+ "earthquake",
+ "earthworm",
+ "ease",
+ "easel",
+ "east",
+ "eating",
+ "eaves",
+ "eavesdropper",
+ "ecclesia",
+ "echidna",
+ "eclipse",
+ "ecliptic",
+ "ecology",
+ "economics",
+ "economy",
+ "ecosystem",
+ "ectoderm",
+ "ectodermal",
+ "ecumenist",
+ "eddy",
+ "edge",
+ "edger",
+ "edible",
+ "editing",
+ "edition",
+ "editor",
+ "editorial",
+ "education",
+ "eel",
+ "effacement",
+ "effect",
+ "effective",
+ "effectiveness",
+ "effector",
+ "efficacy",
+ "efficiency",
+ "effort",
+ "egg",
+ "egghead",
+ "eggnog",
+ "eggplant",
+ "ego",
+ "eicosanoid",
+ "ejector",
+ "elbow",
+ "elderberry",
+ "election",
+ "electricity",
+ "electrocardiogram",
+ "electronics",
+ "element",
+ "elephant",
+ "elevation",
+ "elevator",
+ "eleventh",
+ "elf",
+ "elicit",
+ "eligibility",
+ "elimination",
+ "elite",
+ "elixir",
+ "elk",
+ "ellipse",
+ "elm",
+ "elongation",
+ "elver",
+ "email",
+ "emanate",
+ "embarrassment",
+ "embassy",
+ "embellishment",
+ "embossing",
+ "embryo",
+ "emerald",
+ "emergence",
+ "emergency",
+ "emergent",
+ "emery",
+ "emission",
+ "emitter",
+ "emotion",
+ "emphasis",
+ "empire",
+ "employ",
+ "employee",
+ "employer",
+ "employment",
+ "empowerment",
+ "emu",
+ "enactment",
+ "encirclement",
+ "enclave",
+ "enclosure",
+ "encounter",
+ "encouragement",
+ "encyclopedia",
+ "end",
+ "endive",
+ "endoderm",
+ "endorsement",
+ "endothelium",
+ "endpoint",
+ "enemy",
+ "energy",
+ "enforcement",
+ "engagement",
+ "engine",
+ "engineer",
+ "engineering",
+ "enigma",
+ "enjoyment",
+ "enquiry",
+ "enrollment",
+ "enterprise",
+ "entertainment",
+ "enthusiasm",
+ "entirety",
+ "entity",
+ "entrance",
+ "entree",
+ "entrepreneur",
+ "entry",
+ "envelope",
+ "environment",
+ "envy",
+ "enzyme",
+ "epauliere",
+ "epee",
+ "ephemera",
+ "ephemeris",
+ "ephyra",
+ "epic",
+ "episode",
+ "epithelium",
+ "epoch",
+ "eponym",
+ "epoxy",
+ "equal",
+ "equality",
+ "equation",
+ "equinox",
+ "equipment",
+ "equity",
+ "equivalent",
+ "era",
+ "eraser",
+ "erection",
+ "erosion",
+ "error",
+ "escalator",
+ "escape",
+ "escort",
+ "espadrille",
+ "espalier",
+ "essay",
+ "essence",
+ "essential",
+ "establishment",
+ "estate",
+ "estimate",
+ "estrogen",
+ "estuary",
+ "eternity",
+ "ethernet",
+ "ethics",
+ "ethnicity",
+ "ethyl",
+ "euphonium",
+ "eurocentrism",
+ "evaluation",
+ "evaluator",
+ "evaporation",
+ "eve",
+ "evening",
+ "evening-wear",
+ "event",
+ "everybody",
+ "everyone",
+ "everything",
+ "eviction",
+ "evidence",
+ "evil",
+ "evocation",
+ "evolution",
+ "ex-husband",
+ "ex-wife",
+ "exaggeration",
+ "exam",
+ "examination",
+ "examiner",
+ "example",
+ "exasperation",
+ "excellence",
+ "exception",
+ "excerpt",
+ "excess",
+ "exchange",
+ "excitement",
+ "exclamation",
+ "excursion",
+ "excuse",
+ "execution",
+ "executive",
+ "executor",
+ "exercise",
+ "exhaust",
+ "exhaustion",
+ "exhibit",
+ "exhibition",
+ "exile",
+ "existence",
+ "exit",
+ "exocrine",
+ "expansion",
+ "expansionism",
+ "expectancy",
+ "expectation",
+ "expedition",
+ "expense",
+ "experience",
+ "experiment",
+ "experimentation",
+ "expert",
+ "expertise",
+ "explanation",
+ "exploration",
+ "explorer",
+ "explosion",
+ "export",
+ "expose",
+ "exposition",
+ "exposure",
+ "expression",
+ "extension",
+ "extent",
+ "exterior",
+ "external",
+ "extinction",
+ "extreme",
+ "extremist",
+ "eye",
+ "eyeball",
+ "eyebrow",
+ "eyebrows",
+ "eyeglasses",
+ "eyelash",
+ "eyelashes",
+ "eyelid",
+ "eyelids",
+ "eyeliner",
+ "eyestrain",
+ "eyrie",
+ "fabric",
+ "face",
+ "facelift",
+ "facet",
+ "facility",
+ "facsimile",
+ "fact",
+ "factor",
+ "factory",
+ "faculty",
+ "fahrenheit",
+ "fail",
+ "failure",
+ "fairness",
+ "fairy",
+ "faith",
+ "faithful",
+ "fall",
+ "fallacy",
+ "falling-out",
+ "fame",
+ "familiar",
+ "familiarity",
+ "family",
+ "fan",
+ "fang",
+ "fanlight",
+ "fanny",
+ "fanny-pack",
+ "fantasy",
+ "farm",
+ "farmer",
+ "farming",
+ "farmland",
+ "farrow",
+ "fascia",
+ "fashion",
+ "fat",
+ "fate",
+ "father",
+ "father-in-law",
+ "fatigue",
+ "fatigues",
+ "faucet",
+ "fault",
+ "fav",
+ "fava",
+ "favor",
+ "favorite",
+ "fawn",
+ "fax",
+ "fear",
+ "feast",
+ "feather",
+ "feature",
+ "fedelini",
+ "federation",
+ "fedora",
+ "fee",
+ "feed",
+ "feedback",
+ "feeding",
+ "feel",
+ "feeling",
+ "fellow",
+ "felony",
+ "female",
+ "fen",
+ "fence",
+ "fencing",
+ "fender",
+ "feng",
+ "fennel",
+ "ferret",
+ "ferry",
+ "ferryboat",
+ "fertilizer",
+ "festival",
+ "fetus",
+ "few",
+ "fiber",
+ "fiberglass",
+ "fibre",
+ "fibroblast",
+ "fibrosis",
+ "ficlet",
+ "fiction",
+ "fiddle",
+ "field",
+ "fiery",
+ "fiesta",
+ "fifth",
+ "fig",
+ "fight",
+ "fighter",
+ "figure",
+ "figurine",
+ "file",
+ "filing",
+ "fill",
+ "fillet",
+ "filly",
+ "film",
+ "filter",
+ "filth",
+ "final",
+ "finance",
+ "financing",
+ "finding",
+ "fine",
+ "finer",
+ "finger",
+ "fingerling",
+ "fingernail",
+ "finish",
+ "finisher",
+ "fir",
+ "fire",
+ "fireman",
+ "fireplace",
+ "firewall",
+ "firm",
+ "first",
+ "fish",
+ "fishbone",
+ "fisherman",
+ "fishery",
+ "fishing",
+ "fishmonger",
+ "fishnet",
+ "fisting",
+ "fit",
+ "fitness",
+ "fix",
+ "fixture",
+ "flag",
+ "flair",
+ "flame",
+ "flan",
+ "flanker",
+ "flare",
+ "flash",
+ "flat",
+ "flatboat",
+ "flavor",
+ "flax",
+ "fleck",
+ "fledgling",
+ "fleece",
+ "flesh",
+ "flexibility",
+ "flick",
+ "flicker",
+ "flight",
+ "flint",
+ "flintlock",
+ "flip-flops",
+ "flock",
+ "flood",
+ "floodplain",
+ "floor",
+ "floozie",
+ "flour",
+ "flow",
+ "flower",
+ "flu",
+ "flugelhorn",
+ "fluke",
+ "flume",
+ "flung",
+ "flute",
+ "fly",
+ "flytrap",
+ "foal",
+ "foam",
+ "fob",
+ "focus",
+ "fog",
+ "fold",
+ "folder",
+ "folk",
+ "folklore",
+ "follower",
+ "following",
+ "fondue",
+ "font",
+ "food",
+ "foodstuffs",
+ "fool",
+ "foot",
+ "footage",
+ "football",
+ "footnote",
+ "footprint",
+ "footrest",
+ "footstep",
+ "footstool",
+ "footwear",
+ "forage",
+ "forager",
+ "foray",
+ "force",
+ "ford",
+ "forearm",
+ "forebear",
+ "forecast",
+ "forehead",
+ "foreigner",
+ "forelimb",
+ "forest",
+ "forestry",
+ "forever",
+ "forgery",
+ "fork",
+ "form",
+ "formal",
+ "formamide",
+ "format",
+ "formation",
+ "former",
+ "formicarium",
+ "formula",
+ "fort",
+ "forte",
+ "fortnight",
+ "fortress",
+ "fortune",
+ "forum",
+ "foundation",
+ "founder",
+ "founding",
+ "fountain",
+ "fourths",
+ "fowl",
+ "fox",
+ "foxglove",
+ "fraction",
+ "fragrance",
+ "frame",
+ "framework",
+ "fratricide",
+ "fraud",
+ "fraudster",
+ "freak",
+ "freckle",
+ "freedom",
+ "freelance",
+ "freezer",
+ "freezing",
+ "freight",
+ "freighter",
+ "frenzy",
+ "freon",
+ "frequency",
+ "fresco",
+ "friction",
+ "fridge",
+ "friend",
+ "friendship",
+ "fries",
+ "frigate",
+ "fright",
+ "fringe",
+ "fritter",
+ "frock",
+ "frog",
+ "front",
+ "frontier",
+ "frost",
+ "frosting",
+ "frown",
+ "fruit",
+ "frustration",
+ "fry",
+ "fuck",
+ "fuel",
+ "fugato",
+ "fulfillment",
+ "full",
+ "fun",
+ "function",
+ "functionality",
+ "fund",
+ "funding",
+ "fundraising",
+ "funeral",
+ "fur",
+ "furnace",
+ "furniture",
+ "furry",
+ "fusarium",
+ "futon",
+ "future",
+ "gadget",
+ "gaffe",
+ "gaffer",
+ "gain",
+ "gaiters",
+ "gale",
+ "gall-bladder",
+ "gallery",
+ "galley",
+ "gallon",
+ "galoshes",
+ "gambling",
+ "game",
+ "gamebird",
+ "gaming",
+ "gamma-ray",
+ "gander",
+ "gang",
+ "gap",
+ "garage",
+ "garb",
+ "garbage",
+ "garden",
+ "garlic",
+ "garment",
+ "garter",
+ "gas",
+ "gasket",
+ "gasoline",
+ "gasp",
+ "gastronomy",
+ "gastropod",
+ "gate",
+ "gateway",
+ "gather",
+ "gathering",
+ "gator",
+ "gauge",
+ "gauntlet",
+ "gavel",
+ "gazebo",
+ "gazelle",
+ "gear",
+ "gearshift",
+ "geek",
+ "gel",
+ "gelatin",
+ "gelding",
+ "gem",
+ "gemsbok",
+ "gender",
+ "gene",
+ "general",
+ "generation",
+ "generator",
+ "generosity",
+ "genetics",
+ "genie",
+ "genius",
+ "genocide",
+ "genre",
+ "gentleman",
+ "geography",
+ "geology",
+ "geometry",
+ "geranium",
+ "gerbil",
+ "gesture",
+ "geyser",
+ "gherkin",
+ "ghost",
+ "giant",
+ "gift",
+ "gig",
+ "gigantism",
+ "giggle",
+ "ginger",
+ "gingerbread",
+ "ginseng",
+ "giraffe",
+ "girdle",
+ "girl",
+ "girlfriend",
+ "git",
+ "glacier",
+ "gladiolus",
+ "glance",
+ "gland",
+ "glass",
+ "glasses",
+ "glee",
+ "glen",
+ "glider",
+ "gliding",
+ "glimpse",
+ "globe",
+ "glockenspiel",
+ "gloom",
+ "glory",
+ "glove",
+ "glow",
+ "glucose",
+ "glue",
+ "glut",
+ "glutamate",
+ "gnat",
+ "gnu",
+ "go-kart",
+ "goal",
+ "goat",
+ "gobbler",
+ "god",
+ "goddess",
+ "godfather",
+ "godmother",
+ "godparent",
+ "goggles",
+ "going",
+ "gold",
+ "goldfish",
+ "golf",
+ "gondola",
+ "gong",
+ "good",
+ "good-bye",
+ "goodbye",
+ "goodie",
+ "goodness",
+ "goodnight",
+ "goodwill",
+ "goose",
+ "gopher",
+ "gorilla",
+ "gosling",
+ "gossip",
+ "governance",
+ "government",
+ "governor",
+ "gown",
+ "grab-bag",
+ "grace",
+ "grade",
+ "gradient",
+ "graduate",
+ "graduation",
+ "graffiti",
+ "graft",
+ "grain",
+ "gram",
+ "grammar",
+ "gran",
+ "grand",
+ "grandchild",
+ "granddaughter",
+ "grandfather",
+ "grandma",
+ "grandmom",
+ "grandmother",
+ "grandpa",
+ "grandparent",
+ "grandson",
+ "granny",
+ "granola",
+ "grant",
+ "grape",
+ "grapefruit",
+ "graph",
+ "graphic",
+ "grasp",
+ "grass",
+ "grasshopper",
+ "grassland",
+ "gratitude",
+ "gravel",
+ "gravitas",
+ "gravity",
+ "gravy",
+ "gray",
+ "grease",
+ "great-grandfather",
+ "great-grandmother",
+ "greatness",
+ "greed",
+ "green",
+ "greenhouse",
+ "greens",
+ "grenade",
+ "grey",
+ "grid",
+ "grief",
+ "grill",
+ "grin",
+ "grip",
+ "gripper",
+ "grit",
+ "grocery",
+ "ground",
+ "group",
+ "grouper",
+ "grouse",
+ "grove",
+ "growth",
+ "grub",
+ "guacamole",
+ "guarantee",
+ "guard",
+ "guava",
+ "guerrilla",
+ "guess",
+ "guest",
+ "guestbook",
+ "guidance",
+ "guide",
+ "guideline",
+ "guilder",
+ "guilt",
+ "guilty",
+ "guinea",
+ "guitar",
+ "guitarist",
+ "gum",
+ "gumshoe",
+ "gun",
+ "gunpowder",
+ "gutter",
+ "guy",
+ "gym",
+ "gymnast",
+ "gymnastics",
+ "gynaecology",
+ "gyro",
+ "habit",
+ "habitat",
+ "hacienda",
+ "hacksaw",
+ "hackwork",
+ "hail",
+ "hair",
+ "haircut",
+ "hake",
+ "half",
+ "half-brother",
+ "half-sister",
+ "halibut",
+ "hall",
+ "halloween",
+ "hallway",
+ "halt",
+ "ham",
+ "hamburger",
+ "hammer",
+ "hammock",
+ "hamster",
+ "hand",
+ "hand-holding",
+ "handball",
+ "handful",
+ "handgun",
+ "handicap",
+ "handle",
+ "handlebar",
+ "handmaiden",
+ "handover",
+ "handrail",
+ "handsaw",
+ "hanger",
+ "happening",
+ "happiness",
+ "harald",
+ "harbor",
+ "harbour",
+ "hard-hat",
+ "hardboard",
+ "hardcover",
+ "hardening",
+ "hardhat",
+ "hardship",
+ "hardware",
+ "hare",
+ "harm",
+ "harmonica",
+ "harmonise",
+ "harmonize",
+ "harmony",
+ "harp",
+ "harpooner",
+ "harpsichord",
+ "harvest",
+ "harvester",
+ "hash",
+ "hashtag",
+ "hassock",
+ "haste",
+ "hat",
+ "hatbox",
+ "hatchet",
+ "hatchling",
+ "hate",
+ "hatred",
+ "haunt",
+ "haven",
+ "haversack",
+ "havoc",
+ "hawk",
+ "hay",
+ "haze",
+ "hazel",
+ "hazelnut",
+ "head",
+ "headache",
+ "headlight",
+ "headline",
+ "headphones",
+ "headquarters",
+ "headrest",
+ "health",
+ "health-care",
+ "hearing",
+ "hearsay",
+ "heart",
+ "heart-throb",
+ "heartache",
+ "heartbeat",
+ "hearth",
+ "hearthside",
+ "heartwood",
+ "heat",
+ "heater",
+ "heating",
+ "heaven",
+ "heavy",
+ "hectare",
+ "hedge",
+ "hedgehog",
+ "heel",
+ "heifer",
+ "height",
+ "heir",
+ "heirloom",
+ "helicopter",
+ "helium",
+ "hell",
+ "hellcat",
+ "hello",
+ "helmet",
+ "helo",
+ "help",
+ "hemisphere",
+ "hemp",
+ "hen",
+ "hepatitis",
+ "herb",
+ "herbs",
+ "heritage",
+ "hermit",
+ "hero",
+ "heroine",
+ "heron",
+ "herring",
+ "hesitation",
+ "heterosexual",
+ "hexagon",
+ "heyday",
+ "hiccups",
+ "hide",
+ "hierarchy",
+ "high",
+ "high-rise",
+ "highland",
+ "highlight",
+ "highway",
+ "hike",
+ "hiking",
+ "hill",
+ "hint",
+ "hip",
+ "hippodrome",
+ "hippopotamus",
+ "hire",
+ "hiring",
+ "historian",
+ "history",
+ "hit",
+ "hive",
+ "hobbit",
+ "hobby",
+ "hockey",
+ "hoe",
+ "hog",
+ "hold",
+ "holder",
+ "hole",
+ "holiday",
+ "home",
+ "homeland",
+ "homeownership",
+ "hometown",
+ "homework",
+ "homicide",
+ "homogenate",
+ "homonym",
+ "homosexual",
+ "homosexuality",
+ "honesty",
+ "honey",
+ "honeybee",
+ "honeydew",
+ "honor",
+ "honoree",
+ "hood",
+ "hoof",
+ "hook",
+ "hop",
+ "hope",
+ "hops",
+ "horde",
+ "horizon",
+ "hormone",
+ "horn",
+ "hornet",
+ "horror",
+ "horse",
+ "horseradish",
+ "horst",
+ "hose",
+ "hosiery",
+ "hospice",
+ "hospital",
+ "hospitalisation",
+ "hospitality",
+ "hospitalization",
+ "host",
+ "hostel",
+ "hostess",
+ "hotdog",
+ "hotel",
+ "hound",
+ "hour",
+ "hourglass",
+ "house",
+ "houseboat",
+ "household",
+ "housewife",
+ "housework",
+ "housing",
+ "hovel",
+ "hovercraft",
+ "howard",
+ "howitzer",
+ "hub",
+ "hubcap",
+ "hubris",
+ "hug",
+ "hugger",
+ "hull",
+ "human",
+ "humanity",
+ "humidity",
+ "hummus",
+ "humor",
+ "humour",
+ "hunchback",
+ "hundred",
+ "hunger",
+ "hunt",
+ "hunter",
+ "hunting",
+ "hurdle",
+ "hurdler",
+ "hurricane",
+ "hurry",
+ "hurt",
+ "husband",
+ "hut",
+ "hutch",
+ "hyacinth",
+ "hybridisation",
+ "hybridization",
+ "hydrant",
+ "hydraulics",
+ "hydrocarb",
+ "hydrocarbon",
+ "hydrofoil",
+ "hydrogen",
+ "hydrolyse",
+ "hydrolysis",
+ "hydrolyze",
+ "hydroxyl",
+ "hyena",
+ "hygienic",
+ "hype",
+ "hyphenation",
+ "hypochondria",
+ "hypothermia",
+ "hypothesis",
+ "ice",
+ "ice-cream",
+ "iceberg",
+ "icebreaker",
+ "icecream",
+ "icicle",
+ "icing",
+ "icon",
+ "icy",
+ "id",
+ "idea",
+ "ideal",
+ "identification",
+ "identity",
+ "ideology",
+ "idiom",
+ "idiot",
+ "igloo",
+ "ignorance",
+ "ignorant",
+ "ikebana",
+ "illegal",
+ "illiteracy",
+ "illness",
+ "illusion",
+ "illustration",
+ "image",
+ "imagination",
+ "imbalance",
+ "imitation",
+ "immigrant",
+ "immigration",
+ "immortal",
+ "impact",
+ "impairment",
+ "impala",
+ "impediment",
+ "implement",
+ "implementation",
+ "implication",
+ "import",
+ "importance",
+ "impostor",
+ "impress",
+ "impression",
+ "imprisonment",
+ "impropriety",
+ "improvement",
+ "impudence",
+ "impulse",
+ "in-joke",
+ "in-laws",
+ "inability",
+ "inauguration",
+ "inbox",
+ "incandescence",
+ "incarnation",
+ "incense",
+ "incentive",
+ "inch",
+ "incidence",
+ "incident",
+ "incision",
+ "inclusion",
+ "income",
+ "incompetence",
+ "inconvenience",
+ "increase",
+ "incubation",
+ "independence",
+ "independent",
+ "index",
+ "indication",
+ "indicator",
+ "indigence",
+ "individual",
+ "industrialisation",
+ "industrialization",
+ "industry",
+ "inequality",
+ "inevitable",
+ "infancy",
+ "infant",
+ "infarction",
+ "infection",
+ "infiltration",
+ "infinite",
+ "infix",
+ "inflammation",
+ "inflation",
+ "influence",
+ "influx",
+ "info",
+ "information",
+ "infrastructure",
+ "infusion",
+ "inglenook",
+ "ingrate",
+ "ingredient",
+ "inhabitant",
+ "inheritance",
+ "inhibition",
+ "inhibitor",
+ "initial",
+ "initialise",
+ "initialize",
+ "initiative",
+ "injunction",
+ "injury",
+ "injustice",
+ "ink",
+ "inlay",
+ "inn",
+ "innervation",
+ "innocence",
+ "innocent",
+ "innovation",
+ "input",
+ "inquiry",
+ "inscription",
+ "insect",
+ "insectarium",
+ "insert",
+ "inside",
+ "insight",
+ "insolence",
+ "insomnia",
+ "inspection",
+ "inspector",
+ "inspiration",
+ "installation",
+ "instance",
+ "instant",
+ "instinct",
+ "institute",
+ "institution",
+ "instruction",
+ "instructor",
+ "instrument",
+ "instrumentalist",
+ "instrumentation",
+ "insulation",
+ "insurance",
+ "insurgence",
+ "insurrection",
+ "integer",
+ "integral",
+ "integration",
+ "integrity",
+ "intellect",
+ "intelligence",
+ "intensity",
+ "intent",
+ "intention",
+ "intentionality",
+ "interaction",
+ "interchange",
+ "interconnection",
+ "intercourse",
+ "interest",
+ "interface",
+ "interferometer",
+ "interior",
+ "interject",
+ "interloper",
+ "internet",
+ "interpretation",
+ "interpreter",
+ "interval",
+ "intervenor",
+ "intervention",
+ "interview",
+ "interviewer",
+ "intestine",
+ "introduction",
+ "intuition",
+ "invader",
+ "invasion",
+ "invention",
+ "inventor",
+ "inventory",
+ "inverse",
+ "inversion",
+ "investigation",
+ "investigator",
+ "investment",
+ "investor",
+ "invitation",
+ "invite",
+ "invoice",
+ "involvement",
+ "iridescence",
+ "iris",
+ "iron",
+ "ironclad",
+ "irony",
+ "irrigation",
+ "ischemia",
+ "island",
+ "isogloss",
+ "isolation",
+ "issue",
+ "item",
+ "itinerary",
+ "ivory",
+ "jack",
+ "jackal",
+ "jacket",
+ "jackfruit",
+ "jade",
+ "jaguar",
+ "jail",
+ "jailhouse",
+ "jalapeño",
+ "jam",
+ "jar",
+ "jasmine",
+ "jaw",
+ "jazz",
+ "jealousy",
+ "jeans",
+ "jeep",
+ "jelly",
+ "jellybeans",
+ "jellyfish",
+ "jerk",
+ "jet",
+ "jewel",
+ "jeweller",
+ "jewellery",
+ "jewelry",
+ "jicama",
+ "jiffy",
+ "job",
+ "jockey",
+ "jodhpurs",
+ "joey",
+ "jogging",
+ "joint",
+ "joke",
+ "jot",
+ "journal",
+ "journalism",
+ "journalist",
+ "journey",
+ "joy",
+ "judge",
+ "judgment",
+ "judo",
+ "jug",
+ "juggernaut",
+ "juice",
+ "julienne",
+ "jumbo",
+ "jump",
+ "jumper",
+ "jumpsuit",
+ "jungle",
+ "junior",
+ "junk",
+ "junker",
+ "junket",
+ "jury",
+ "justice",
+ "justification",
+ "jute",
+ "kale",
+ "kamikaze",
+ "kangaroo",
+ "karate",
+ "kayak",
+ "kazoo",
+ "kebab",
+ "keep",
+ "keeper",
+ "kendo",
+ "kennel",
+ "ketch",
+ "ketchup",
+ "kettle",
+ "kettledrum",
+ "key",
+ "keyboard",
+ "keyboarding",
+ "keystone",
+ "kick",
+ "kick-off",
+ "kid",
+ "kidney",
+ "kielbasa",
+ "kill",
+ "killer",
+ "killing",
+ "kilogram",
+ "kilometer",
+ "kilt",
+ "kimono",
+ "kinase",
+ "kind",
+ "kindness",
+ "king",
+ "kingdom",
+ "kingfish",
+ "kiosk",
+ "kiss",
+ "kit",
+ "kitchen",
+ "kite",
+ "kitsch",
+ "kitten",
+ "kitty",
+ "kiwi",
+ "knee",
+ "kneejerk",
+ "knickers",
+ "knife",
+ "knife-edge",
+ "knight",
+ "knitting",
+ "knock",
+ "knot",
+ "know-how",
+ "knowledge",
+ "knuckle",
+ "koala",
+ "kohlrabi",
+ "kumquat",
+ "lab",
+ "label",
+ "labor",
+ "laboratory",
+ "laborer",
+ "labour",
+ "labourer",
+ "lace",
+ "lack",
+ "lacquerware",
+ "lad",
+ "ladder",
+ "ladle",
+ "lady",
+ "ladybug",
+ "lag",
+ "lake",
+ "lamb",
+ "lambkin",
+ "lament",
+ "lamp",
+ "lanai",
+ "land",
+ "landform",
+ "landing",
+ "landmine",
+ "landscape",
+ "lane",
+ "language",
+ "lantern",
+ "lap",
+ "laparoscope",
+ "lapdog",
+ "laptop",
+ "larch",
+ "lard",
+ "larder",
+ "lark",
+ "larva",
+ "laryngitis",
+ "lasagna",
+ "lashes",
+ "last",
+ "latency",
+ "latex",
+ "lathe",
+ "latitude",
+ "latte",
+ "latter",
+ "laugh",
+ "laughter",
+ "laundry",
+ "lava",
+ "law",
+ "lawmaker",
+ "lawn",
+ "lawsuit",
+ "lawyer",
+ "lay",
+ "layer",
+ "layout",
+ "lead",
+ "leader",
+ "leadership",
+ "leading",
+ "leaf",
+ "league",
+ "leaker",
+ "leap",
+ "learning",
+ "leash",
+ "leather",
+ "leave",
+ "leaver",
+ "lecture",
+ "leek",
+ "leeway",
+ "left",
+ "leg",
+ "legacy",
+ "legal",
+ "legend",
+ "legging",
+ "legislation",
+ "legislator",
+ "legislature",
+ "legitimacy",
+ "legume",
+ "leisure",
+ "lemon",
+ "lemonade",
+ "lemur",
+ "lender",
+ "lending",
+ "length",
+ "lens",
+ "lentil",
+ "leopard",
+ "leprosy",
+ "leptocephalus",
+ "lesbian",
+ "lesson",
+ "letter",
+ "lettuce",
+ "level",
+ "lever",
+ "leverage",
+ "leveret",
+ "liability",
+ "liar",
+ "liberty",
+ "libido",
+ "library",
+ "licence",
+ "license",
+ "licensing",
+ "licorice",
+ "lid",
+ "lie",
+ "lieu",
+ "lieutenant",
+ "life",
+ "lifestyle",
+ "lifetime",
+ "lift",
+ "ligand",
+ "light",
+ "lighting",
+ "lightning",
+ "lightscreen",
+ "ligula",
+ "likelihood",
+ "likeness",
+ "lilac",
+ "lily",
+ "limb",
+ "lime",
+ "limestone",
+ "limit",
+ "limitation",
+ "limo",
+ "line",
+ "linen",
+ "liner",
+ "linguist",
+ "linguistics",
+ "lining",
+ "link",
+ "linkage",
+ "linseed",
+ "lion",
+ "lip",
+ "lipid",
+ "lipoprotein",
+ "lipstick",
+ "liquid",
+ "liquidity",
+ "liquor",
+ "list",
+ "listening",
+ "listing",
+ "literate",
+ "literature",
+ "litigation",
+ "litmus",
+ "litter",
+ "littleneck",
+ "liver",
+ "livestock",
+ "living",
+ "lizard",
+ "llama",
+ "load",
+ "loading",
+ "loaf",
+ "loafer",
+ "loan",
+ "lobby",
+ "lobotomy",
+ "lobster",
+ "local",
+ "locality",
+ "location",
+ "lock",
+ "locker",
+ "locket",
+ "locomotive",
+ "locust",
+ "lode",
+ "loft",
+ "log",
+ "loggia",
+ "logic",
+ "login",
+ "logistics",
+ "logo",
+ "loincloth",
+ "lollipop",
+ "loneliness",
+ "longboat",
+ "longitude",
+ "look",
+ "lookout",
+ "loop",
+ "loophole",
+ "loquat",
+ "lord",
+ "loss",
+ "lot",
+ "lotion",
+ "lottery",
+ "lounge",
+ "louse",
+ "lout",
+ "love",
+ "lover",
+ "lox",
+ "loyalty",
+ "luck",
+ "luggage",
+ "lumber",
+ "lumberman",
+ "lunch",
+ "luncheonette",
+ "lunchmeat",
+ "lunchroom",
+ "lung",
+ "lunge",
+ "lust",
+ "lute",
+ "luxury",
+ "lychee",
+ "lycra",
+ "lye",
+ "lymphocyte",
+ "lynx",
+ "lyocell",
+ "lyre",
+ "lyrics",
+ "lysine",
+ "mRNA",
+ "macadamia",
+ "macaroni",
+ "macaroon",
+ "macaw",
+ "machine",
+ "machinery",
+ "macrame",
+ "macro",
+ "macrofauna",
+ "madam",
+ "maelstrom",
+ "maestro",
+ "magazine",
+ "maggot",
+ "magic",
+ "magnet",
+ "magnitude",
+ "maid",
+ "maiden",
+ "mail",
+ "mailbox",
+ "mailer",
+ "mailing",
+ "mailman",
+ "main",
+ "mainland",
+ "mainstream",
+ "maintainer",
+ "maintenance",
+ "maize",
+ "major",
+ "major-league",
+ "majority",
+ "makeover",
+ "maker",
+ "makeup",
+ "making",
+ "male",
+ "malice",
+ "mall",
+ "mallard",
+ "mallet",
+ "malnutrition",
+ "mama",
+ "mambo",
+ "mammoth",
+ "man",
+ "manacle",
+ "management",
+ "manager",
+ "manatee",
+ "mandarin",
+ "mandate",
+ "mandolin",
+ "mangle",
+ "mango",
+ "mangrove",
+ "manhunt",
+ "maniac",
+ "manicure",
+ "manifestation",
+ "manipulation",
+ "mankind",
+ "manner",
+ "manor",
+ "mansard",
+ "manservant",
+ "mansion",
+ "mantel",
+ "mantle",
+ "mantua",
+ "manufacturer",
+ "manufacturing",
+ "many",
+ "map",
+ "maple",
+ "mapping",
+ "maracas",
+ "marathon",
+ "marble",
+ "march",
+ "mare",
+ "margarine",
+ "margin",
+ "mariachi",
+ "marimba",
+ "marines",
+ "marionberry",
+ "mark",
+ "marker",
+ "market",
+ "marketer",
+ "marketing",
+ "marketplace",
+ "marksman",
+ "markup",
+ "marmalade",
+ "marriage",
+ "marsh",
+ "marshland",
+ "marshmallow",
+ "marten",
+ "marxism",
+ "mascara",
+ "mask",
+ "masonry",
+ "mass",
+ "massage",
+ "mast",
+ "master",
+ "masterpiece",
+ "mastication",
+ "mastoid",
+ "mat",
+ "match",
+ "matchmaker",
+ "mate",
+ "material",
+ "maternity",
+ "math",
+ "mathematics",
+ "matrix",
+ "matter",
+ "mattock",
+ "mattress",
+ "max",
+ "maximum",
+ "maybe",
+ "mayonnaise",
+ "mayor",
+ "meadow",
+ "meal",
+ "mean",
+ "meander",
+ "meaning",
+ "means",
+ "meantime",
+ "measles",
+ "measure",
+ "measurement",
+ "meat",
+ "meatball",
+ "meatloaf",
+ "mecca",
+ "mechanic",
+ "mechanism",
+ "med",
+ "medal",
+ "media",
+ "median",
+ "medication",
+ "medicine",
+ "medium",
+ "meet",
+ "meeting",
+ "melatonin",
+ "melody",
+ "melon",
+ "member",
+ "membership",
+ "membrane",
+ "meme",
+ "memo",
+ "memorial",
+ "memory",
+ "men",
+ "menopause",
+ "menorah",
+ "mention",
+ "mentor",
+ "menu",
+ "merchandise",
+ "merchant",
+ "mercury",
+ "meridian",
+ "meringue",
+ "merit",
+ "mesenchyme",
+ "mess",
+ "message",
+ "messenger",
+ "messy",
+ "metabolite",
+ "metal",
+ "metallurgist",
+ "metaphor",
+ "meteor",
+ "meteorology",
+ "meter",
+ "methane",
+ "method",
+ "methodology",
+ "metric",
+ "metro",
+ "metronome",
+ "mezzanine",
+ "microlending",
+ "micronutrient",
+ "microphone",
+ "microwave",
+ "mid-course",
+ "midden",
+ "middle",
+ "middleman",
+ "midline",
+ "midnight",
+ "midwife",
+ "might",
+ "migrant",
+ "migration",
+ "mile",
+ "mileage",
+ "milepost",
+ "milestone",
+ "military",
+ "milk",
+ "milkshake",
+ "mill",
+ "millennium",
+ "millet",
+ "millimeter",
+ "million",
+ "millisecond",
+ "millstone",
+ "mime",
+ "mimosa",
+ "min",
+ "mincemeat",
+ "mind",
+ "mine",
+ "mineral",
+ "mineshaft",
+ "mini",
+ "mini-skirt",
+ "minibus",
+ "minimalism",
+ "minimum",
+ "mining",
+ "minion",
+ "minister",
+ "mink",
+ "minnow",
+ "minor",
+ "minor-league",
+ "minority",
+ "mint",
+ "minute",
+ "miracle",
+ "mirror",
+ "miscarriage",
+ "miscommunication",
+ "misfit",
+ "misnomer",
+ "misogyny",
+ "misplacement",
+ "misreading",
+ "misrepresentation",
+ "miss",
+ "missile",
+ "mission",
+ "missionary",
+ "mist",
+ "mistake",
+ "mister",
+ "misunderstand",
+ "miter",
+ "mitten",
+ "mix",
+ "mixer",
+ "mixture",
+ "moai",
+ "moat",
+ "mob",
+ "mobile",
+ "mobility",
+ "mobster",
+ "moccasins",
+ "mocha",
+ "mochi",
+ "mode",
+ "model",
+ "modeling",
+ "modem",
+ "modernist",
+ "modernity",
+ "modification",
+ "molar",
+ "molasses",
+ "molding",
+ "mole",
+ "molecule",
+ "mom",
+ "moment",
+ "monastery",
+ "monasticism",
+ "money",
+ "monger",
+ "monitor",
+ "monitoring",
+ "monk",
+ "monkey",
+ "monocle",
+ "monopoly",
+ "monotheism",
+ "monsoon",
+ "monster",
+ "month",
+ "monument",
+ "mood",
+ "moody",
+ "moon",
+ "moonlight",
+ "moonscape",
+ "moonshine",
+ "moose",
+ "mop",
+ "morale",
+ "morbid",
+ "morbidity",
+ "morning",
+ "moron",
+ "morphology",
+ "morsel",
+ "mortal",
+ "mortality",
+ "mortgage",
+ "mortise",
+ "mosque",
+ "mosquito",
+ "most",
+ "motel",
+ "moth",
+ "mother",
+ "mother-in-law",
+ "motion",
+ "motivation",
+ "motive",
+ "motor",
+ "motorboat",
+ "motorcar",
+ "motorcycle",
+ "mound",
+ "mountain",
+ "mouse",
+ "mouser",
+ "mousse",
+ "moustache",
+ "mouth",
+ "mouton",
+ "movement",
+ "mover",
+ "movie",
+ "mower",
+ "mozzarella",
+ "mud",
+ "muffin",
+ "mug",
+ "mukluk",
+ "mule",
+ "multimedia",
+ "murder",
+ "muscat",
+ "muscatel",
+ "muscle",
+ "musculature",
+ "museum",
+ "mushroom",
+ "music",
+ "music-box",
+ "music-making",
+ "musician",
+ "muskrat",
+ "mussel",
+ "mustache",
+ "mustard",
+ "mutation",
+ "mutt",
+ "mutton",
+ "mycoplasma",
+ "mystery",
+ "myth",
+ "mythology",
+ "nail",
+ "name",
+ "naming",
+ "nanoparticle",
+ "napkin",
+ "narrative",
+ "nasal",
+ "nation",
+ "nationality",
+ "native",
+ "naturalisation",
+ "nature",
+ "navigation",
+ "necessity",
+ "neck",
+ "necklace",
+ "necktie",
+ "nectar",
+ "nectarine",
+ "need",
+ "needle",
+ "neglect",
+ "negligee",
+ "negotiation",
+ "neighbor",
+ "neighborhood",
+ "neighbour",
+ "neighbourhood",
+ "neologism",
+ "neon",
+ "neonate",
+ "nephew",
+ "nerve",
+ "nest",
+ "nestling",
+ "nestmate",
+ "net",
+ "netball",
+ "netbook",
+ "netsuke",
+ "network",
+ "networking",
+ "neurobiologist",
+ "neuron",
+ "neuropathologist",
+ "neuropsychiatry",
+ "news",
+ "newsletter",
+ "newspaper",
+ "newsprint",
+ "newsstand",
+ "nexus",
+ "nibble",
+ "nicety",
+ "niche",
+ "nick",
+ "nickel",
+ "nickname",
+ "niece",
+ "night",
+ "nightclub",
+ "nightgown",
+ "nightingale",
+ "nightlife",
+ "nightlight",
+ "nightmare",
+ "ninja",
+ "nit",
+ "nitrogen",
+ "nobody",
+ "nod",
+ "node",
+ "noir",
+ "noise",
+ "nonbeliever",
+ "nonconformist",
+ "nondisclosure",
+ "nonsense",
+ "noodle",
+ "noodles",
+ "noon",
+ "norm",
+ "normal",
+ "normalisation",
+ "normalization",
+ "north",
+ "nose",
+ "notation",
+ "note",
+ "notebook",
+ "notepad",
+ "nothing",
+ "notice",
+ "notion",
+ "notoriety",
+ "nougat",
+ "noun",
+ "nourishment",
+ "novel",
+ "nucleotidase",
+ "nucleotide",
+ "nudge",
+ "nuke",
+ "number",
+ "numeracy",
+ "numeric",
+ "numismatist",
+ "nun",
+ "nurse",
+ "nursery",
+ "nursing",
+ "nurture",
+ "nut",
+ "nutmeg",
+ "nutrient",
+ "nutrition",
+ "nylon",
+ "nymph",
+ "oak",
+ "oar",
+ "oasis",
+ "oat",
+ "oatmeal",
+ "oats",
+ "obedience",
+ "obesity",
+ "obi",
+ "object",
+ "objection",
+ "objective",
+ "obligation",
+ "oboe",
+ "observation",
+ "observatory",
+ "obsession",
+ "obsidian",
+ "obstacle",
+ "occasion",
+ "occupation",
+ "occurrence",
+ "ocean",
+ "ocelot",
+ "octagon",
+ "octave",
+ "octavo",
+ "octet",
+ "octopus",
+ "odometer",
+ "odyssey",
+ "oeuvre",
+ "off-ramp",
+ "offence",
+ "offense",
+ "offer",
+ "offering",
+ "office",
+ "officer",
+ "official",
+ "offset",
+ "oil",
+ "okra",
+ "oldie",
+ "oleo",
+ "olive",
+ "omega",
+ "omelet",
+ "omission",
+ "omnivore",
+ "oncology",
+ "onion",
+ "online",
+ "onset",
+ "opening",
+ "opera",
+ "operating",
+ "operation",
+ "operator",
+ "ophthalmologist",
+ "opinion",
+ "opium",
+ "opossum",
+ "opponent",
+ "opportunist",
+ "opportunity",
+ "opposite",
+ "opposition",
+ "optimal",
+ "optimisation",
+ "optimist",
+ "optimization",
+ "option",
+ "orange",
+ "orangutan",
+ "orator",
+ "orchard",
+ "orchestra",
+ "orchid",
+ "order",
+ "ordinary",
+ "ordination",
+ "ore",
+ "oregano",
+ "organ",
+ "organisation",
+ "organising",
+ "organization",
+ "organizing",
+ "orient",
+ "orientation",
+ "origin",
+ "original",
+ "originality",
+ "ornament",
+ "osmosis",
+ "osprey",
+ "ostrich",
+ "other",
+ "otter",
+ "ottoman",
+ "ounce",
+ "outback",
+ "outcome",
+ "outfielder",
+ "outfit",
+ "outhouse",
+ "outlaw",
+ "outlay",
+ "outlet",
+ "outline",
+ "outlook",
+ "output",
+ "outrage",
+ "outrigger",
+ "outrun",
+ "outset",
+ "outside",
+ "oval",
+ "ovary",
+ "oven",
+ "overcharge",
+ "overclocking",
+ "overcoat",
+ "overexertion",
+ "overflight",
+ "overhead",
+ "overheard",
+ "overload",
+ "overnighter",
+ "overshoot",
+ "oversight",
+ "overview",
+ "overweight",
+ "owl",
+ "owner",
+ "ownership",
+ "ox",
+ "oxford",
+ "oxygen",
+ "oyster",
+ "ozone",
+ "pace",
+ "pacemaker",
+ "pack",
+ "package",
+ "packaging",
+ "packet",
+ "pad",
+ "paddle",
+ "paddock",
+ "pagan",
+ "page",
+ "pagoda",
+ "pail",
+ "pain",
+ "paint",
+ "painter",
+ "painting",
+ "paintwork",
+ "pair",
+ "pajamas",
+ "palace",
+ "palate",
+ "palm",
+ "pamphlet",
+ "pan",
+ "pancake",
+ "pancreas",
+ "panda",
+ "panel",
+ "panic",
+ "pannier",
+ "panpipe",
+ "pansy",
+ "panther",
+ "panties",
+ "pantologist",
+ "pantology",
+ "pantry",
+ "pants",
+ "pantsuit",
+ "panty",
+ "pantyhose",
+ "papa",
+ "papaya",
+ "paper",
+ "paperback",
+ "paperwork",
+ "parable",
+ "parachute",
+ "parade",
+ "paradise",
+ "paragraph",
+ "parallelogram",
+ "paramecium",
+ "paramedic",
+ "parameter",
+ "paranoia",
+ "parcel",
+ "parchment",
+ "pard",
+ "pardon",
+ "parent",
+ "parenthesis",
+ "parenting",
+ "park",
+ "parka",
+ "parking",
+ "parliament",
+ "parole",
+ "parrot",
+ "parser",
+ "parsley",
+ "parsnip",
+ "part",
+ "participant",
+ "participation",
+ "particle",
+ "particular",
+ "partner",
+ "partnership",
+ "partridge",
+ "party",
+ "pass",
+ "passage",
+ "passbook",
+ "passenger",
+ "passing",
+ "passion",
+ "passive",
+ "passport",
+ "password",
+ "past",
+ "pasta",
+ "paste",
+ "pastor",
+ "pastoralist",
+ "pastry",
+ "pasture",
+ "pat",
+ "patch",
+ "pate",
+ "patent",
+ "patentee",
+ "path",
+ "pathogenesis",
+ "pathology",
+ "pathway",
+ "patience",
+ "patient",
+ "patina",
+ "patio",
+ "patriarch",
+ "patrimony",
+ "patriot",
+ "patrol",
+ "patroller",
+ "patrolling",
+ "patron",
+ "pattern",
+ "patty",
+ "pattypan",
+ "pause",
+ "pavement",
+ "pavilion",
+ "paw",
+ "pawnshop",
+ "pay",
+ "payee",
+ "payment",
+ "payoff",
+ "pea",
+ "peace",
+ "peach",
+ "peacoat",
+ "peacock",
+ "peak",
+ "peanut",
+ "pear",
+ "pearl",
+ "peasant",
+ "pecan",
+ "pecker",
+ "pedal",
+ "peek",
+ "peen",
+ "peer",
+ "peer-to-peer",
+ "pegboard",
+ "pelican",
+ "pelt",
+ "pen",
+ "penalty",
+ "pence",
+ "pencil",
+ "pendant",
+ "pendulum",
+ "penguin",
+ "penicillin",
+ "peninsula",
+ "penis",
+ "pennant",
+ "penny",
+ "pension",
+ "pentagon",
+ "peony",
+ "people",
+ "pepper",
+ "pepperoni",
+ "percent",
+ "percentage",
+ "perception",
+ "perch",
+ "perennial",
+ "perfection",
+ "performance",
+ "perfume",
+ "period",
+ "periodical",
+ "peripheral",
+ "permafrost",
+ "permission",
+ "permit",
+ "perp",
+ "perpendicular",
+ "persimmon",
+ "person",
+ "personal",
+ "personality",
+ "personnel",
+ "perspective",
+ "pest",
+ "pet",
+ "petal",
+ "petition",
+ "petitioner",
+ "petticoat",
+ "pew",
+ "pharmacist",
+ "pharmacopoeia",
+ "phase",
+ "pheasant",
+ "phenomenon",
+ "phenotype",
+ "pheromone",
+ "philanthropy",
+ "philosopher",
+ "philosophy",
+ "phone",
+ "phosphate",
+ "photo",
+ "photodiode",
+ "photograph",
+ "photographer",
+ "photography",
+ "photoreceptor",
+ "phrase",
+ "phrasing",
+ "physical",
+ "physics",
+ "physiology",
+ "pianist",
+ "piano",
+ "piccolo",
+ "pick",
+ "pickax",
+ "pickaxe",
+ "picket",
+ "pickle",
+ "pickup",
+ "picnic",
+ "picture",
+ "picturesque",
+ "pie",
+ "piece",
+ "pier",
+ "piety",
+ "pig",
+ "pigeon",
+ "piglet",
+ "pigpen",
+ "pigsty",
+ "pike",
+ "pilaf",
+ "pile",
+ "pilgrim",
+ "pilgrimage",
+ "pill",
+ "pillar",
+ "pillbox",
+ "pillow",
+ "pilot",
+ "pimp",
+ "pimple",
+ "pin",
+ "pinafore",
+ "pince-nez",
+ "pine",
+ "pineapple",
+ "pinecone",
+ "ping",
+ "pink",
+ "pinkie",
+ "pinot",
+ "pinstripe",
+ "pint",
+ "pinto",
+ "pinworm",
+ "pioneer",
+ "pipe",
+ "pipeline",
+ "piracy",
+ "pirate",
+ "piss",
+ "pistol",
+ "pit",
+ "pita",
+ "pitch",
+ "pitcher",
+ "pitching",
+ "pith",
+ "pizza",
+ "place",
+ "placebo",
+ "placement",
+ "placode",
+ "plagiarism",
+ "plain",
+ "plaintiff",
+ "plan",
+ "plane",
+ "planet",
+ "planning",
+ "plant",
+ "plantation",
+ "planter",
+ "planula",
+ "plaster",
+ "plasterboard",
+ "plastic",
+ "plate",
+ "platelet",
+ "platform",
+ "platinum",
+ "platter",
+ "platypus",
+ "play",
+ "player",
+ "playground",
+ "playroom",
+ "playwright",
+ "plea",
+ "pleasure",
+ "pleat",
+ "pledge",
+ "plenty",
+ "plier",
+ "pliers",
+ "plight",
+ "plot",
+ "plough",
+ "plover",
+ "plow",
+ "plowman",
+ "plug",
+ "plugin",
+ "plum",
+ "plumber",
+ "plume",
+ "plunger",
+ "plywood",
+ "pneumonia",
+ "pocket",
+ "pocket-watch",
+ "pocketbook",
+ "pod",
+ "podcast",
+ "poem",
+ "poet",
+ "poetry",
+ "poignance",
+ "point",
+ "poison",
+ "poisoning",
+ "poker",
+ "polarisation",
+ "polarization",
+ "pole",
+ "polenta",
+ "police",
+ "policeman",
+ "policy",
+ "polish",
+ "politician",
+ "politics",
+ "poll",
+ "polliwog",
+ "pollutant",
+ "pollution",
+ "polo",
+ "polyester",
+ "polyp",
+ "pomegranate",
+ "pomelo",
+ "pompom",
+ "poncho",
+ "pond",
+ "pony",
+ "pool",
+ "poor",
+ "pop",
+ "popcorn",
+ "poppy",
+ "popsicle",
+ "popularity",
+ "population",
+ "populist",
+ "porcelain",
+ "porch",
+ "porcupine",
+ "pork",
+ "porpoise",
+ "port",
+ "porter",
+ "portfolio",
+ "porthole",
+ "portion",
+ "portrait",
+ "position",
+ "possession",
+ "possibility",
+ "possible",
+ "post",
+ "postage",
+ "postbox",
+ "poster",
+ "posterior",
+ "postfix",
+ "pot",
+ "potato",
+ "potential",
+ "pottery",
+ "potty",
+ "pouch",
+ "poultry",
+ "pound",
+ "pounding",
+ "poverty",
+ "powder",
+ "power",
+ "practice",
+ "practitioner",
+ "prairie",
+ "praise",
+ "pray",
+ "prayer",
+ "precedence",
+ "precedent",
+ "precipitation",
+ "precision",
+ "predecessor",
+ "preface",
+ "preference",
+ "prefix",
+ "pregnancy",
+ "prejudice",
+ "prelude",
+ "premeditation",
+ "premier",
+ "premise",
+ "premium",
+ "preoccupation",
+ "preparation",
+ "prescription",
+ "presence",
+ "present",
+ "presentation",
+ "preservation",
+ "preserves",
+ "presidency",
+ "president",
+ "press",
+ "pressroom",
+ "pressure",
+ "pressurisation",
+ "pressurization",
+ "prestige",
+ "presume",
+ "pretzel",
+ "prevalence",
+ "prevention",
+ "prey",
+ "price",
+ "pricing",
+ "pride",
+ "priest",
+ "priesthood",
+ "primary",
+ "primate",
+ "prince",
+ "princess",
+ "principal",
+ "principle",
+ "print",
+ "printer",
+ "printing",
+ "prior",
+ "priority",
+ "prison",
+ "prisoner",
+ "privacy",
+ "private",
+ "privilege",
+ "prize",
+ "prizefight",
+ "probability",
+ "probation",
+ "probe",
+ "problem",
+ "procedure",
+ "proceedings",
+ "process",
+ "processing",
+ "processor",
+ "proctor",
+ "procurement",
+ "produce",
+ "producer",
+ "product",
+ "production",
+ "productivity",
+ "profession",
+ "professional",
+ "professor",
+ "profile",
+ "profit",
+ "progenitor",
+ "program",
+ "programme",
+ "programming",
+ "progress",
+ "progression",
+ "prohibition",
+ "project",
+ "proliferation",
+ "promenade",
+ "promise",
+ "promotion",
+ "prompt",
+ "pronoun",
+ "pronunciation",
+ "proof",
+ "proof-reader",
+ "propaganda",
+ "propane",
+ "property",
+ "prophet",
+ "proponent",
+ "proportion",
+ "proposal",
+ "proposition",
+ "proprietor",
+ "prose",
+ "prosecution",
+ "prosecutor",
+ "prospect",
+ "prosperity",
+ "prostacyclin",
+ "prostanoid",
+ "prostrate",
+ "protection",
+ "protein",
+ "protest",
+ "protocol",
+ "providence",
+ "provider",
+ "province",
+ "provision",
+ "prow",
+ "proximal",
+ "proximity",
+ "prune",
+ "pruner",
+ "pseudocode",
+ "pseudoscience",
+ "psychiatrist",
+ "psychoanalyst",
+ "psychologist",
+ "psychology",
+ "ptarmigan",
+ "pub",
+ "public",
+ "publication",
+ "publicity",
+ "publisher",
+ "publishing",
+ "pudding",
+ "puddle",
+ "puffin",
+ "pug",
+ "puggle",
+ "pulley",
+ "pulse",
+ "puma",
+ "pump",
+ "pumpernickel",
+ "pumpkin",
+ "pumpkinseed",
+ "pun",
+ "punch",
+ "punctuation",
+ "punishment",
+ "pup",
+ "pupa",
+ "pupil",
+ "puppet",
+ "puppy",
+ "purchase",
+ "puritan",
+ "purity",
+ "purple",
+ "purpose",
+ "purr",
+ "purse",
+ "pursuit",
+ "push",
+ "pusher",
+ "put",
+ "puzzle",
+ "pyramid",
+ "pyridine",
+ "quadrant",
+ "quail",
+ "qualification",
+ "quality",
+ "quantity",
+ "quart",
+ "quarter",
+ "quartet",
+ "quartz",
+ "queen",
+ "query",
+ "quest",
+ "question",
+ "questioner",
+ "questionnaire",
+ "quiche",
+ "quicksand",
+ "quiet",
+ "quill",
+ "quilt",
+ "quince",
+ "quinoa",
+ "quit",
+ "quiver",
+ "quota",
+ "quotation",
+ "quote",
+ "rabbi",
+ "rabbit",
+ "raccoon",
+ "race",
+ "racer",
+ "racing",
+ "racism",
+ "racist",
+ "rack",
+ "radar",
+ "radiator",
+ "radio",
+ "radiosonde",
+ "radish",
+ "raffle",
+ "raft",
+ "rag",
+ "rage",
+ "raid",
+ "rail",
+ "railing",
+ "railroad",
+ "railway",
+ "raiment",
+ "rain",
+ "rainbow",
+ "raincoat",
+ "rainmaker",
+ "rainstorm",
+ "rainy",
+ "raise",
+ "raisin",
+ "rake",
+ "rally",
+ "ram",
+ "rambler",
+ "ramen",
+ "ramie",
+ "ranch",
+ "rancher",
+ "randomisation",
+ "randomization",
+ "range",
+ "ranger",
+ "rank",
+ "rap",
+ "rape",
+ "raspberry",
+ "rat",
+ "rate",
+ "ratepayer",
+ "rating",
+ "ratio",
+ "rationale",
+ "rations",
+ "raven",
+ "ravioli",
+ "rawhide",
+ "ray",
+ "rayon",
+ "razor",
+ "reach",
+ "reactant",
+ "reaction",
+ "read",
+ "reader",
+ "readiness",
+ "reading",
+ "real",
+ "reality",
+ "realization",
+ "realm",
+ "reamer",
+ "rear",
+ "reason",
+ "reasoning",
+ "rebel",
+ "rebellion",
+ "reboot",
+ "recall",
+ "recapitulation",
+ "receipt",
+ "receiver",
+ "reception",
+ "receptor",
+ "recess",
+ "recession",
+ "recipe",
+ "recipient",
+ "reciprocity",
+ "reclamation",
+ "recliner",
+ "recognition",
+ "recollection",
+ "recommendation",
+ "reconsideration",
+ "record",
+ "recorder",
+ "recording",
+ "recovery",
+ "recreation",
+ "recruit",
+ "rectangle",
+ "red",
+ "redesign",
+ "redhead",
+ "redirect",
+ "rediscovery",
+ "reduction",
+ "reef",
+ "refectory",
+ "reference",
+ "referendum",
+ "reflection",
+ "reform",
+ "refreshments",
+ "refrigerator",
+ "refuge",
+ "refund",
+ "refusal",
+ "refuse",
+ "regard",
+ "regime",
+ "region",
+ "regionalism",
+ "register",
+ "registration",
+ "registry",
+ "regret",
+ "regulation",
+ "regulator",
+ "rehospitalisation",
+ "rehospitalization",
+ "reindeer",
+ "reinscription",
+ "reject",
+ "relation",
+ "relationship",
+ "relative",
+ "relaxation",
+ "relay",
+ "release",
+ "reliability",
+ "relief",
+ "religion",
+ "relish",
+ "reluctance",
+ "remains",
+ "remark",
+ "reminder",
+ "remnant",
+ "remote",
+ "removal",
+ "renaissance",
+ "rent",
+ "reorganisation",
+ "reorganization",
+ "repair",
+ "reparation",
+ "repayment",
+ "repeat",
+ "replacement",
+ "replica",
+ "replication",
+ "reply",
+ "report",
+ "reporter",
+ "reporting",
+ "repository",
+ "representation",
+ "representative",
+ "reprocessing",
+ "republic",
+ "republican",
+ "reputation",
+ "request",
+ "requirement",
+ "resale",
+ "rescue",
+ "research",
+ "researcher",
+ "resemblance",
+ "reservation",
+ "reserve",
+ "reservoir",
+ "reset",
+ "residence",
+ "resident",
+ "residue",
+ "resist",
+ "resistance",
+ "resolution",
+ "resolve",
+ "resort",
+ "resource",
+ "respect",
+ "respite",
+ "response",
+ "responsibility",
+ "rest",
+ "restaurant",
+ "restoration",
+ "restriction",
+ "restroom",
+ "restructuring",
+ "result",
+ "resume",
+ "retailer",
+ "retention",
+ "rethinking",
+ "retina",
+ "retirement",
+ "retouching",
+ "retreat",
+ "retrospect",
+ "retrospective",
+ "retrospectivity",
+ "return",
+ "reunion",
+ "revascularisation",
+ "revascularization",
+ "reveal",
+ "revelation",
+ "revenant",
+ "revenge",
+ "revenue",
+ "reversal",
+ "reverse",
+ "review",
+ "revitalisation",
+ "revitalization",
+ "revival",
+ "revolution",
+ "revolver",
+ "reward",
+ "rhetoric",
+ "rheumatism",
+ "rhinoceros",
+ "rhubarb",
+ "rhyme",
+ "rhythm",
+ "rib",
+ "ribbon",
+ "rice",
+ "riddle",
+ "ride",
+ "rider",
+ "ridge",
+ "riding",
+ "rifle",
+ "right",
+ "rim",
+ "ring",
+ "ringworm",
+ "riot",
+ "rip",
+ "ripple",
+ "rise",
+ "riser",
+ "risk",
+ "rite",
+ "ritual",
+ "river",
+ "riverbed",
+ "rivulet",
+ "road",
+ "roadway",
+ "roar",
+ "roast",
+ "robe",
+ "robin",
+ "robot",
+ "robotics",
+ "rock",
+ "rocker",
+ "rocket",
+ "rocket-ship",
+ "rod",
+ "role",
+ "roll",
+ "roller",
+ "romaine",
+ "romance",
+ "roof",
+ "room",
+ "roommate",
+ "rooster",
+ "root",
+ "rope",
+ "rose",
+ "rosemary",
+ "roster",
+ "rostrum",
+ "rotation",
+ "round",
+ "roundabout",
+ "route",
+ "router",
+ "routine",
+ "row",
+ "rowboat",
+ "rowing",
+ "rubber",
+ "rubbish",
+ "rubric",
+ "ruby",
+ "ruckus",
+ "rudiment",
+ "ruffle",
+ "rug",
+ "rugby",
+ "ruin",
+ "rule",
+ "ruler",
+ "ruling",
+ "rum",
+ "rumor",
+ "run",
+ "runaway",
+ "runner",
+ "running",
+ "runway",
+ "rush",
+ "rust",
+ "rutabaga",
+ "rye",
+ "sabre",
+ "sac",
+ "sack",
+ "saddle",
+ "sadness",
+ "safari",
+ "safe",
+ "safeguard",
+ "safety",
+ "saffron",
+ "sage",
+ "sail",
+ "sailboat",
+ "sailing",
+ "sailor",
+ "saint",
+ "sake",
+ "salad",
+ "salami",
+ "salary",
+ "sale",
+ "salesman",
+ "salmon",
+ "salon",
+ "saloon",
+ "salsa",
+ "salt",
+ "salute",
+ "samovar",
+ "sampan",
+ "sample",
+ "samurai",
+ "sanction",
+ "sanctity",
+ "sanctuary",
+ "sand",
+ "sandal",
+ "sandbar",
+ "sandpaper",
+ "sandwich",
+ "sanity",
+ "sardine",
+ "sari",
+ "sarong",
+ "sash",
+ "satellite",
+ "satin",
+ "satire",
+ "satisfaction",
+ "sauce",
+ "saucer",
+ "sauerkraut",
+ "sausage",
+ "savage",
+ "savannah",
+ "saving",
+ "savings",
+ "savior",
+ "saviour",
+ "savory",
+ "saw",
+ "saxophone",
+ "scaffold",
+ "scale",
+ "scallion",
+ "scallops",
+ "scalp",
+ "scam",
+ "scanner",
+ "scarecrow",
+ "scarf",
+ "scarification",
+ "scenario",
+ "scene",
+ "scenery",
+ "scent",
+ "schedule",
+ "scheduling",
+ "schema",
+ "scheme",
+ "schizophrenic",
+ "schnitzel",
+ "scholar",
+ "scholarship",
+ "school",
+ "schoolhouse",
+ "schooner",
+ "science",
+ "scientist",
+ "scimitar",
+ "scissors",
+ "scooter",
+ "scope",
+ "score",
+ "scorn",
+ "scorpion",
+ "scotch",
+ "scout",
+ "scow",
+ "scrambled",
+ "scrap",
+ "scraper",
+ "scratch",
+ "screamer",
+ "screen",
+ "screening",
+ "screenwriting",
+ "screw",
+ "screw-up",
+ "screwdriver",
+ "scrim",
+ "scrip",
+ "script",
+ "scripture",
+ "scrutiny",
+ "sculpting",
+ "sculptural",
+ "sculpture",
+ "sea",
+ "seabass",
+ "seafood",
+ "seagull",
+ "seal",
+ "seaplane",
+ "search",
+ "seashore",
+ "seaside",
+ "season",
+ "seat",
+ "seaweed",
+ "second",
+ "secrecy",
+ "secret",
+ "secretariat",
+ "secretary",
+ "secretion",
+ "section",
+ "sectional",
+ "sector",
+ "security",
+ "sediment",
+ "seed",
+ "seeder",
+ "seeker",
+ "seep",
+ "segment",
+ "seizure",
+ "selection",
+ "self",
+ "self-confidence",
+ "self-control",
+ "self-esteem",
+ "seller",
+ "selling",
+ "semantics",
+ "semester",
+ "semicircle",
+ "semicolon",
+ "semiconductor",
+ "seminar",
+ "senate",
+ "senator",
+ "sender",
+ "senior",
+ "sense",
+ "sensibility",
+ "sensitive",
+ "sensitivity",
+ "sensor",
+ "sentence",
+ "sentencing",
+ "sentiment",
+ "sepal",
+ "separation",
+ "septicaemia",
+ "sequel",
+ "sequence",
+ "serial",
+ "series",
+ "sermon",
+ "serum",
+ "serval",
+ "servant",
+ "server",
+ "service",
+ "servitude",
+ "sesame",
+ "session",
+ "set",
+ "setback",
+ "setting",
+ "settlement",
+ "settler",
+ "severity",
+ "sewer",
+ "sex",
+ "sexuality",
+ "shack",
+ "shackle",
+ "shade",
+ "shadow",
+ "shadowbox",
+ "shakedown",
+ "shaker",
+ "shallot",
+ "shallows",
+ "shame",
+ "shampoo",
+ "shanty",
+ "shape",
+ "share",
+ "shareholder",
+ "shark",
+ "shaw",
+ "shawl",
+ "shear",
+ "shearling",
+ "sheath",
+ "shed",
+ "sheep",
+ "sheet",
+ "shelf",
+ "shell",
+ "shelter",
+ "sherbet",
+ "sherry",
+ "shield",
+ "shift",
+ "shin",
+ "shine",
+ "shingle",
+ "ship",
+ "shipper",
+ "shipping",
+ "shipyard",
+ "shirt",
+ "shirtdress",
+ "shit",
+ "shoat",
+ "shock",
+ "shoe",
+ "shoe-horn",
+ "shoehorn",
+ "shoelace",
+ "shoemaker",
+ "shoes",
+ "shoestring",
+ "shofar",
+ "shoot",
+ "shootdown",
+ "shop",
+ "shopper",
+ "shopping",
+ "shore",
+ "shoreline",
+ "short",
+ "shortage",
+ "shorts",
+ "shortwave",
+ "shot",
+ "shoulder",
+ "shout",
+ "shovel",
+ "show",
+ "show-stopper",
+ "shower",
+ "shred",
+ "shrimp",
+ "shrine",
+ "shutdown",
+ "sibling",
+ "sick",
+ "sickness",
+ "side",
+ "sideboard",
+ "sideburns",
+ "sidecar",
+ "sidestream",
+ "sidewalk",
+ "siding",
+ "siege",
+ "sigh",
+ "sight",
+ "sightseeing",
+ "sign",
+ "signal",
+ "signature",
+ "signet",
+ "significance",
+ "signify",
+ "signup",
+ "silence",
+ "silica",
+ "silicon",
+ "silk",
+ "silkworm",
+ "sill",
+ "silly",
+ "silo",
+ "silver",
+ "similarity",
+ "simple",
+ "simplicity",
+ "simplification",
+ "simvastatin",
+ "sin",
+ "singer",
+ "singing",
+ "singular",
+ "sink",
+ "sinuosity",
+ "sip",
+ "sir",
+ "sister",
+ "sister-in-law",
+ "sitar",
+ "site",
+ "situation",
+ "size",
+ "skate",
+ "skating",
+ "skean",
+ "skeleton",
+ "ski",
+ "skiing",
+ "skill",
+ "skin",
+ "skirt",
+ "skull",
+ "skullcap",
+ "skullduggery",
+ "skunk",
+ "sky",
+ "skylight",
+ "skyline",
+ "skyscraper",
+ "skywalk",
+ "slang",
+ "slapstick",
+ "slash",
+ "slate",
+ "slave",
+ "slavery",
+ "slaw",
+ "sled",
+ "sledge",
+ "sleep",
+ "sleepiness",
+ "sleeping",
+ "sleet",
+ "sleuth",
+ "slice",
+ "slide",
+ "slider",
+ "slime",
+ "slip",
+ "slipper",
+ "slippers",
+ "slope",
+ "slot",
+ "sloth",
+ "slump",
+ "smell",
+ "smelting",
+ "smile",
+ "smith",
+ "smock",
+ "smog",
+ "smoke",
+ "smoking",
+ "smolt",
+ "smuggling",
+ "snack",
+ "snail",
+ "snake",
+ "snakebite",
+ "snap",
+ "snarl",
+ "sneaker",
+ "sneakers",
+ "sneeze",
+ "sniffle",
+ "snob",
+ "snorer",
+ "snow",
+ "snowboarding",
+ "snowflake",
+ "snowman",
+ "snowmobiling",
+ "snowplow",
+ "snowstorm",
+ "snowsuit",
+ "snuck",
+ "snug",
+ "snuggle",
+ "soap",
+ "soccer",
+ "socialism",
+ "socialist",
+ "society",
+ "sociology",
+ "sock",
+ "socks",
+ "soda",
+ "sofa",
+ "softball",
+ "softdrink",
+ "softening",
+ "software",
+ "soil",
+ "soldier",
+ "sole",
+ "solicitation",
+ "solicitor",
+ "solidarity",
+ "solidity",
+ "soliloquy",
+ "solitaire",
+ "solution",
+ "solvency",
+ "sombrero",
+ "somebody",
+ "someone",
+ "someplace",
+ "somersault",
+ "something",
+ "somewhere",
+ "son",
+ "sonar",
+ "sonata",
+ "song",
+ "songbird",
+ "sonnet",
+ "soot",
+ "sophomore",
+ "soprano",
+ "sorbet",
+ "sorghum",
+ "sorrel",
+ "sorrow",
+ "sort",
+ "soul",
+ "soulmate",
+ "sound",
+ "soundness",
+ "soup",
+ "source",
+ "sourwood",
+ "sousaphone",
+ "south",
+ "southeast",
+ "souvenir",
+ "sovereignty",
+ "sow",
+ "soy",
+ "soybean",
+ "space",
+ "spacing",
+ "spade",
+ "spaghetti",
+ "span",
+ "spandex",
+ "spank",
+ "sparerib",
+ "spark",
+ "sparrow",
+ "spasm",
+ "spat",
+ "spatula",
+ "spawn",
+ "speaker",
+ "speakerphone",
+ "speaking",
+ "spear",
+ "spec",
+ "special",
+ "specialist",
+ "specialty",
+ "species",
+ "specification",
+ "spectacle",
+ "spectacles",
+ "spectrograph",
+ "spectrum",
+ "speculation",
+ "speech",
+ "speed",
+ "speedboat",
+ "spell",
+ "spelling",
+ "spelt",
+ "spending",
+ "sphere",
+ "sphynx",
+ "spice",
+ "spider",
+ "spiderling",
+ "spike",
+ "spill",
+ "spinach",
+ "spine",
+ "spiral",
+ "spirit",
+ "spiritual",
+ "spirituality",
+ "spit",
+ "spite",
+ "spleen",
+ "splendor",
+ "split",
+ "spokesman",
+ "spokeswoman",
+ "sponge",
+ "sponsor",
+ "sponsorship",
+ "spool",
+ "spoon",
+ "spork",
+ "sport",
+ "sportsman",
+ "spot",
+ "spotlight",
+ "spouse",
+ "sprag",
+ "sprat",
+ "spray",
+ "spread",
+ "spreadsheet",
+ "spree",
+ "spring",
+ "sprinkles",
+ "sprinter",
+ "sprout",
+ "spruce",
+ "spud",
+ "spume",
+ "spur",
+ "spy",
+ "spyglass",
+ "square",
+ "squash",
+ "squatter",
+ "squeegee",
+ "squid",
+ "squirrel",
+ "stab",
+ "stability",
+ "stable",
+ "stack",
+ "stacking",
+ "stadium",
+ "staff",
+ "stag",
+ "stage",
+ "stain",
+ "stair",
+ "staircase",
+ "stake",
+ "stalk",
+ "stall",
+ "stallion",
+ "stamen",
+ "stamina",
+ "stamp",
+ "stance",
+ "stand",
+ "standard",
+ "standardisation",
+ "standardization",
+ "standing",
+ "standoff",
+ "standpoint",
+ "star",
+ "starboard",
+ "start",
+ "starter",
+ "state",
+ "statement",
+ "statin",
+ "station",
+ "station-wagon",
+ "statistic",
+ "statistics",
+ "statue",
+ "status",
+ "statute",
+ "stay",
+ "steak",
+ "stealth",
+ "steam",
+ "steamroller",
+ "steel",
+ "steeple",
+ "stem",
+ "stench",
+ "stencil",
+ "step",
+ "step-aunt",
+ "step-brother",
+ "step-daughter",
+ "step-father",
+ "step-grandfather",
+ "step-grandmother",
+ "step-mother",
+ "step-sister",
+ "step-son",
+ "step-uncle",
+ "stepdaughter",
+ "stepmother",
+ "stepping-stone",
+ "stepson",
+ "stereo",
+ "stew",
+ "steward",
+ "stick",
+ "sticker",
+ "stiletto",
+ "still",
+ "stimulation",
+ "stimulus",
+ "sting",
+ "stinger",
+ "stir-fry",
+ "stitch",
+ "stitcher",
+ "stock",
+ "stock-in-trade",
+ "stockings",
+ "stole",
+ "stomach",
+ "stone",
+ "stonework",
+ "stool",
+ "stop",
+ "stopsign",
+ "stopwatch",
+ "storage",
+ "store",
+ "storey",
+ "storm",
+ "story",
+ "story-telling",
+ "storyboard",
+ "stot",
+ "stove",
+ "strait",
+ "strand",
+ "stranger",
+ "strap",
+ "strategy",
+ "straw",
+ "strawberry",
+ "strawman",
+ "stream",
+ "street",
+ "streetcar",
+ "strength",
+ "stress",
+ "stretch",
+ "strife",
+ "strike",
+ "string",
+ "strip",
+ "stripe",
+ "strobe",
+ "stroke",
+ "structure",
+ "strudel",
+ "struggle",
+ "stucco",
+ "stud",
+ "student",
+ "studio",
+ "study",
+ "stuff",
+ "stumbling",
+ "stump",
+ "stupidity",
+ "sturgeon",
+ "sty",
+ "style",
+ "styling",
+ "stylus",
+ "sub",
+ "subcomponent",
+ "subconscious",
+ "subcontractor",
+ "subexpression",
+ "subgroup",
+ "subject",
+ "submarine",
+ "submitter",
+ "subprime",
+ "subroutine",
+ "subscription",
+ "subsection",
+ "subset",
+ "subsidence",
+ "subsidiary",
+ "subsidy",
+ "substance",
+ "substitution",
+ "subtitle",
+ "suburb",
+ "subway",
+ "success",
+ "succotash",
+ "suck",
+ "sucker",
+ "suede",
+ "suet",
+ "suffocation",
+ "sugar",
+ "suggestion",
+ "suicide",
+ "suit",
+ "suitcase",
+ "suite",
+ "sulfur",
+ "sultan",
+ "sum",
+ "summary",
+ "summer",
+ "summit",
+ "sun",
+ "sunbeam",
+ "sunbonnet",
+ "sundae",
+ "sunday",
+ "sundial",
+ "sunflower",
+ "sunglasses",
+ "sunlamp",
+ "sunlight",
+ "sunrise",
+ "sunroom",
+ "sunset",
+ "sunshine",
+ "superiority",
+ "supermarket",
+ "supernatural",
+ "supervision",
+ "supervisor",
+ "supper",
+ "supplement",
+ "supplier",
+ "supply",
+ "support",
+ "supporter",
+ "suppression",
+ "supreme",
+ "surface",
+ "surfboard",
+ "surge",
+ "surgeon",
+ "surgery",
+ "surname",
+ "surplus",
+ "surprise",
+ "surround",
+ "surroundings",
+ "surrounds",
+ "survey",
+ "survival",
+ "survivor",
+ "sushi",
+ "suspect",
+ "suspenders",
+ "suspension",
+ "sustainment",
+ "sustenance",
+ "swallow",
+ "swamp",
+ "swan",
+ "swanling",
+ "swath",
+ "sweat",
+ "sweater",
+ "sweatshirt",
+ "sweatshop",
+ "sweatsuit",
+ "sweets",
+ "swell",
+ "swim",
+ "swimming",
+ "swimsuit",
+ "swine",
+ "swing",
+ "switch",
+ "switchboard",
+ "switching",
+ "swivel",
+ "sword",
+ "swordfight",
+ "swordfish",
+ "sycamore",
+ "symbol",
+ "symmetry",
+ "sympathy",
+ "symptom",
+ "syndicate",
+ "syndrome",
+ "synergy",
+ "synod",
+ "synonym",
+ "synthesis",
+ "syrup",
+ "system",
+ "t-shirt",
+ "tab",
+ "tabby",
+ "tabernacle",
+ "table",
+ "tablecloth",
+ "tablet",
+ "tabletop",
+ "tachometer",
+ "tackle",
+ "taco",
+ "tactics",
+ "tactile",
+ "tadpole",
+ "tag",
+ "tail",
+ "tailbud",
+ "tailor",
+ "tailspin",
+ "take-out",
+ "takeover",
+ "tale",
+ "talent",
+ "talk",
+ "talking",
+ "tam-o'-shanter",
+ "tamale",
+ "tambour",
+ "tambourine",
+ "tan",
+ "tandem",
+ "tangerine",
+ "tank",
+ "tank-top",
+ "tanker",
+ "tankful",
+ "tap",
+ "tape",
+ "tapioca",
+ "target",
+ "taro",
+ "tarragon",
+ "tart",
+ "task",
+ "tassel",
+ "taste",
+ "tatami",
+ "tattler",
+ "tattoo",
+ "tavern",
+ "tax",
+ "taxi",
+ "taxicab",
+ "taxpayer",
+ "tea",
+ "teacher",
+ "teaching",
+ "team",
+ "teammate",
+ "teapot",
+ "tear",
+ "tech",
+ "technician",
+ "technique",
+ "technologist",
+ "technology",
+ "tectonics",
+ "teen",
+ "teenager",
+ "teepee",
+ "telephone",
+ "telescreen",
+ "teletype",
+ "television",
+ "tell",
+ "teller",
+ "temp",
+ "temper",
+ "temperature",
+ "temple",
+ "tempo",
+ "temporariness",
+ "temporary",
+ "temptation",
+ "temptress",
+ "tenant",
+ "tendency",
+ "tender",
+ "tenement",
+ "tenet",
+ "tennis",
+ "tenor",
+ "tension",
+ "tensor",
+ "tent",
+ "tentacle",
+ "tenth",
+ "tepee",
+ "teriyaki",
+ "term",
+ "terminal",
+ "termination",
+ "terminology",
+ "termite",
+ "terrace",
+ "terracotta",
+ "terrapin",
+ "terrarium",
+ "territory",
+ "terror",
+ "terrorism",
+ "terrorist",
+ "test",
+ "testament",
+ "testimonial",
+ "testimony",
+ "testing",
+ "text",
+ "textbook",
+ "textual",
+ "texture",
+ "thanks",
+ "thaw",
+ "theater",
+ "theft",
+ "theism",
+ "theme",
+ "theology",
+ "theory",
+ "therapist",
+ "therapy",
+ "thermals",
+ "thermometer",
+ "thermostat",
+ "thesis",
+ "thickness",
+ "thief",
+ "thigh",
+ "thing",
+ "thinking",
+ "thirst",
+ "thistle",
+ "thong",
+ "thongs",
+ "thorn",
+ "thought",
+ "thousand",
+ "thread",
+ "threat",
+ "threshold",
+ "thrift",
+ "thrill",
+ "throat",
+ "throne",
+ "thrush",
+ "thrust",
+ "thug",
+ "thumb",
+ "thump",
+ "thunder",
+ "thunderbolt",
+ "thunderhead",
+ "thunderstorm",
+ "thyme",
+ "tiara",
+ "tic",
+ "tick",
+ "ticket",
+ "tide",
+ "tie",
+ "tiger",
+ "tights",
+ "tile",
+ "till",
+ "tilt",
+ "timbale",
+ "timber",
+ "time",
+ "timeline",
+ "timeout",
+ "timer",
+ "timetable",
+ "timing",
+ "timpani",
+ "tin",
+ "tinderbox",
+ "tinkle",
+ "tintype",
+ "tip",
+ "tire",
+ "tissue",
+ "titanium",
+ "title",
+ "toad",
+ "toast",
+ "toaster",
+ "tobacco",
+ "today",
+ "toe",
+ "toenail",
+ "toffee",
+ "tofu",
+ "tog",
+ "toga",
+ "toilet",
+ "tolerance",
+ "tolerant",
+ "toll",
+ "tom-tom",
+ "tomatillo",
+ "tomato",
+ "tomb",
+ "tomography",
+ "tomorrow",
+ "ton",
+ "tonality",
+ "tone",
+ "tongue",
+ "tonic",
+ "tonight",
+ "tool",
+ "toot",
+ "tooth",
+ "toothbrush",
+ "toothpaste",
+ "toothpick",
+ "top",
+ "top-hat",
+ "topic",
+ "topsail",
+ "toque",
+ "toreador",
+ "tornado",
+ "torso",
+ "torte",
+ "tortellini",
+ "tortilla",
+ "tortoise",
+ "tosser",
+ "total",
+ "tote",
+ "touch",
+ "tough-guy",
+ "tour",
+ "tourism",
+ "tourist",
+ "tournament",
+ "tow-truck",
+ "towel",
+ "tower",
+ "town",
+ "townhouse",
+ "township",
+ "toy",
+ "trace",
+ "trachoma",
+ "track",
+ "tracking",
+ "tracksuit",
+ "tract",
+ "tractor",
+ "trade",
+ "trader",
+ "trading",
+ "tradition",
+ "traditionalism",
+ "traffic",
+ "trafficker",
+ "tragedy",
+ "trail",
+ "trailer",
+ "trailpatrol",
+ "train",
+ "trainer",
+ "training",
+ "trait",
+ "tram",
+ "tramp",
+ "trance",
+ "transaction",
+ "transcript",
+ "transfer",
+ "transformation",
+ "transit",
+ "transition",
+ "translation",
+ "transmission",
+ "transom",
+ "transparency",
+ "transplantation",
+ "transport",
+ "transportation",
+ "trap",
+ "trapdoor",
+ "trapezium",
+ "trapezoid",
+ "trash",
+ "travel",
+ "traveler",
+ "tray",
+ "treasure",
+ "treasury",
+ "treat",
+ "treatment",
+ "treaty",
+ "tree",
+ "trek",
+ "trellis",
+ "tremor",
+ "trench",
+ "trend",
+ "triad",
+ "trial",
+ "triangle",
+ "tribe",
+ "tributary",
+ "trick",
+ "trigger",
+ "trigonometry",
+ "trillion",
+ "trim",
+ "trinket",
+ "trip",
+ "tripod",
+ "tritone",
+ "triumph",
+ "trolley",
+ "trombone",
+ "troop",
+ "trooper",
+ "trophy",
+ "trouble",
+ "trousers",
+ "trout",
+ "trove",
+ "trowel",
+ "truck",
+ "trumpet",
+ "trunk",
+ "trust",
+ "trustee",
+ "truth",
+ "try",
+ "tsunami",
+ "tub",
+ "tuba",
+ "tube",
+ "tuber",
+ "tug",
+ "tugboat",
+ "tuition",
+ "tulip",
+ "tumbler",
+ "tummy",
+ "tuna",
+ "tune",
+ "tune-up",
+ "tunic",
+ "tunnel",
+ "turban",
+ "turf",
+ "turkey",
+ "turmeric",
+ "turn",
+ "turning",
+ "turnip",
+ "turnover",
+ "turnstile",
+ "turret",
+ "turtle",
+ "tusk",
+ "tussle",
+ "tutu",
+ "tuxedo",
+ "tweet",
+ "tweezers",
+ "twig",
+ "twilight",
+ "twine",
+ "twins",
+ "twist",
+ "twister",
+ "twitter",
+ "type",
+ "typeface",
+ "typewriter",
+ "typhoon",
+ "ukulele",
+ "ultimatum",
+ "umbrella",
+ "unblinking",
+ "uncertainty",
+ "uncle",
+ "underclothes",
+ "underestimate",
+ "underground",
+ "underneath",
+ "underpants",
+ "underpass",
+ "undershirt",
+ "understanding",
+ "understatement",
+ "undertaker",
+ "underwear",
+ "underweight",
+ "underwire",
+ "underwriting",
+ "unemployment",
+ "unibody",
+ "uniform",
+ "uniformity",
+ "union",
+ "unique",
+ "unit",
+ "unity",
+ "universe",
+ "university",
+ "update",
+ "upgrade",
+ "uplift",
+ "upper",
+ "upstairs",
+ "upward",
+ "urge",
+ "urgency",
+ "urn",
+ "usage",
+ "use",
+ "user",
+ "usher",
+ "usual",
+ "utensil",
+ "utilisation",
+ "utility",
+ "utilization",
+ "vacation",
+ "vaccine",
+ "vacuum",
+ "vagrant",
+ "valance",
+ "valentine",
+ "validate",
+ "validity",
+ "valley",
+ "valuable",
+ "value",
+ "vampire",
+ "van",
+ "vanadyl",
+ "vane",
+ "vanilla",
+ "vanity",
+ "variability",
+ "variable",
+ "variant",
+ "variation",
+ "variety",
+ "vascular",
+ "vase",
+ "vault",
+ "vaulting",
+ "veal",
+ "vector",
+ "vegetable",
+ "vegetarian",
+ "vegetarianism",
+ "vegetation",
+ "vehicle",
+ "veil",
+ "vein",
+ "veldt",
+ "vellum",
+ "velocity",
+ "velodrome",
+ "velvet",
+ "vendor",
+ "veneer",
+ "vengeance",
+ "venison",
+ "venom",
+ "venti",
+ "venture",
+ "venue",
+ "veranda",
+ "verb",
+ "verdict",
+ "verification",
+ "vermicelli",
+ "vernacular",
+ "verse",
+ "version",
+ "vertigo",
+ "verve",
+ "vessel",
+ "vest",
+ "vestment",
+ "vet",
+ "veteran",
+ "veterinarian",
+ "veto",
+ "viability",
+ "vibe",
+ "vibraphone",
+ "vibration",
+ "vibrissae",
+ "vice",
+ "vicinity",
+ "victim",
+ "victory",
+ "video",
+ "view",
+ "viewer",
+ "vignette",
+ "villa",
+ "village",
+ "vine",
+ "vinegar",
+ "vineyard",
+ "vintage",
+ "vintner",
+ "vinyl",
+ "viola",
+ "violation",
+ "violence",
+ "violet",
+ "violin",
+ "virginal",
+ "virtue",
+ "virus",
+ "visa",
+ "viscose",
+ "vise",
+ "vision",
+ "visit",
+ "visitor",
+ "visor",
+ "vista",
+ "visual",
+ "vitality",
+ "vitamin",
+ "vitro",
+ "vivo",
+ "vixen",
+ "vodka",
+ "vogue",
+ "voice",
+ "void",
+ "vol",
+ "volatility",
+ "volcano",
+ "volleyball",
+ "volume",
+ "volunteer",
+ "volunteering",
+ "vomit",
+ "vote",
+ "voter",
+ "voting",
+ "voyage",
+ "vulture",
+ "wad",
+ "wafer",
+ "waffle",
+ "wage",
+ "wagon",
+ "waist",
+ "waistband",
+ "wait",
+ "waiter",
+ "waiting",
+ "waitress",
+ "waiver",
+ "wake",
+ "walk",
+ "walker",
+ "walking",
+ "walkway",
+ "wall",
+ "wallaby",
+ "wallet",
+ "walnut",
+ "walrus",
+ "wampum",
+ "wannabe",
+ "want",
+ "war",
+ "warden",
+ "wardrobe",
+ "warfare",
+ "warlock",
+ "warlord",
+ "warm-up",
+ "warming",
+ "warmth",
+ "warning",
+ "warrant",
+ "warren",
+ "warrior",
+ "wasabi",
+ "wash",
+ "washbasin",
+ "washcloth",
+ "washer",
+ "washtub",
+ "wasp",
+ "waste",
+ "wastebasket",
+ "wasting",
+ "watch",
+ "watcher",
+ "watchmaker",
+ "water",
+ "waterbed",
+ "watercress",
+ "waterfall",
+ "waterfront",
+ "watermelon",
+ "waterskiing",
+ "waterspout",
+ "waterwheel",
+ "wave",
+ "waveform",
+ "wax",
+ "way",
+ "weakness",
+ "wealth",
+ "weapon",
+ "wear",
+ "weasel",
+ "weather",
+ "web",
+ "webinar",
+ "webmail",
+ "webpage",
+ "website",
+ "wedding",
+ "wedge",
+ "weed",
+ "weeder",
+ "weedkiller",
+ "week",
+ "weekend",
+ "weekender",
+ "weight",
+ "weird",
+ "welcome",
+ "welfare",
+ "well",
+ "well-being",
+ "west",
+ "western",
+ "wet-bar",
+ "wetland",
+ "wetsuit",
+ "whack",
+ "whale",
+ "wharf",
+ "wheat",
+ "wheel",
+ "whelp",
+ "whey",
+ "whip",
+ "whirlpool",
+ "whirlwind",
+ "whisker",
+ "whiskey",
+ "whisper",
+ "whistle",
+ "white",
+ "whole",
+ "wholesale",
+ "wholesaler",
+ "whorl",
+ "wick",
+ "widget",
+ "widow",
+ "width",
+ "wife",
+ "wifi",
+ "wild",
+ "wildebeest",
+ "wilderness",
+ "wildlife",
+ "will",
+ "willingness",
+ "willow",
+ "win",
+ "wind",
+ "wind-chime",
+ "windage",
+ "window",
+ "windscreen",
+ "windshield",
+ "wine",
+ "winery",
+ "wing",
+ "wingman",
+ "wingtip",
+ "wink",
+ "winner",
+ "winter",
+ "wire",
+ "wiretap",
+ "wiring",
+ "wisdom",
+ "wiseguy",
+ "wish",
+ "wisteria",
+ "wit",
+ "witch",
+ "witch-hunt",
+ "withdrawal",
+ "witness",
+ "wok",
+ "wolf",
+ "woman",
+ "wombat",
+ "wonder",
+ "wont",
+ "wood",
+ "woodchuck",
+ "woodland",
+ "woodshed",
+ "woodwind",
+ "wool",
+ "woolens",
+ "word",
+ "wording",
+ "work",
+ "workbench",
+ "worker",
+ "workforce",
+ "workhorse",
+ "working",
+ "workout",
+ "workplace",
+ "workshop",
+ "world",
+ "worm",
+ "worry",
+ "worship",
+ "worshiper",
+ "worth",
+ "wound",
+ "wrap",
+ "wraparound",
+ "wrapper",
+ "wrapping",
+ "wreck",
+ "wrecker",
+ "wren",
+ "wrench",
+ "wrestler",
+ "wriggler",
+ "wrinkle",
+ "wrist",
+ "writer",
+ "writing",
+ "wrong",
+ "xylophone",
+ "yacht",
+ "yahoo",
+ "yak",
+ "yam",
+ "yang",
+ "yard",
+ "yarmulke",
+ "yarn",
+ "yawl",
+ "year",
+ "yeast",
+ "yellow",
+ "yellowjacket",
+ "yesterday",
+ "yew",
+ "yin",
+ "yoga",
+ "yogurt",
+ "yoke",
+ "yolk",
+ "young",
+ "youngster",
+ "yourself",
+ "youth",
+ "yoyo",
+ "yurt",
+ "zampone",
+ "zebra",
+ "zebrafish",
+ "zen",
+ "zephyr",
+ "zero",
+ "ziggurat",
+ "zinc",
+ "zipper",
+ "zither",
+ "zombie",
+ "zone",
+ "zoo",
+ "zoologist",
+ "zoology",
+ "zoot-suit",
+ "zucchini"
+ };
+
+ private static readonly string[] Colours =
+ {
+ "red",
+ "blue",
+ "green",
+ "yellow",
+ "pink",
+ "orange",
+ "blue",
+ "purple",
+ "white",
+ "gold",
+ "silver",
+ "teal",
+ };
+ #endregion
+
+ public NounHelper(Random random)
+ {
+ _random = random;
+ }
+
+ public string GetRandomNoun()
+ {
+ var index = _random.Next(0, Nouns.Length);
+ return Nouns[index];
+ }
+
+ public string GetRandomColour()
+ {
+ var index = _random.Next(0, Colours.Length);
+ return Colours[index];
+ }
+}
\ No newline at end of file
diff --git a/API/Mapping/Full.cs b/API/Mapping/Full.cs
new file mode 100644
index 0000000..0d14ed9
--- /dev/null
+++ b/API/Mapping/Full.cs
@@ -0,0 +1,22 @@
+using AutoMapper;
+using Hesketh.MecatolArchives.DB.Models;
+
+namespace Hesketh.MecatolArchives.API.Mapping;
+
+public sealed class Full : Profile
+{
+ public Full()
+ {
+ CreateMap().ReverseMap();
+ CreateMap().ReverseMap();
+ CreateMap().ReverseMap();
+ CreateMap().ReverseMap();
+ CreateMap().ReverseMap();
+ CreateMap().AfterMap((dbo, dto) =>
+ {
+ dto.Players = new List(dto.Players.OrderBy(x => x.Eliminated).ThenByDescending(x => x.Points));
+ });
+ CreateMap();
+ CreateMap().ReverseMap();
+ }
+}
\ No newline at end of file
diff --git a/API/Mapping/Post.cs b/API/Mapping/Post.cs
new file mode 100644
index 0000000..19edaf9
--- /dev/null
+++ b/API/Mapping/Post.cs
@@ -0,0 +1,18 @@
+using AutoMapper;
+using Hesketh.MecatolArchives.API.Data.Post;
+
+namespace Hesketh.MecatolArchives.API.Mapping;
+
+public class Post : Profile
+{
+ public Post()
+ {
+ CreateMap();
+ CreateMap();
+ CreateMap();
+ CreateMap();
+ CreateMap();
+ CreateMap();
+ CreateMap();
+ }
+}
\ No newline at end of file
diff --git a/API/Mapping/Put.cs b/API/Mapping/Put.cs
new file mode 100644
index 0000000..10653a0
--- /dev/null
+++ b/API/Mapping/Put.cs
@@ -0,0 +1,12 @@
+using AutoMapper;
+using Hesketh.MecatolArchives.API.Data.Put;
+
+namespace Hesketh.MecatolArchives.API.Mapping;
+
+public class Put : Profile
+{
+ public Put()
+ {
+ CreateMap();
+ }
+}
\ No newline at end of file
diff --git a/API/Program.cs b/API/Program.cs
new file mode 100644
index 0000000..36ec6d2
--- /dev/null
+++ b/API/Program.cs
@@ -0,0 +1,81 @@
+using Hesketh.MecatolArchives.API.Auth;
+using Hesketh.MecatolArchives.API.Helpers;
+using Hesketh.MecatolArchives.DB;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.EntityFrameworkCore;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(options => options.CustomSchemaIds(x =>
+{
+ // Could do with something more generic if this becomes an issue in the future
+ // But this is because two Models used in the API have the same name
+ // So this quick work around appends .Post to any models in the Post namespace
+ if (x.FullName!.EndsWith($"Post.{x.Name}")) return $"Post.{x.Name}";
+ if (x.FullName!.EndsWith($"Put.{x.Name}")) return $"Put.{x.Name}";
+ return x.Name;
+}));
+
+builder.Services.Configure(builder.Configuration.GetSection(AdminAccountOptions.SectionName));
+builder.Services.AddTransient();
+builder.Services.AddAuthentication("BasicAuthentication")
+ .AddScheme("BasicAuthentication", null);
+
+builder.Services.AddAutoMapper(typeof(Program).Assembly);
+
+var databaseMode = builder.Configuration.GetValue("DatabaseMode");
+if (databaseMode == "memory")
+{
+ builder.Services.AddDbContext(options
+ => options.UseInMemoryDatabase(nameof(MecatolArchivesDbContext)));
+}
+else if (databaseMode == "sqlite")
+{
+ builder.Services.AddDbContext(options
+ => options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"), b =>
+ {
+ b.MigrationsAssembly("DB.Migrations.Sqlite");
+ }));
+}
+else if (databaseMode == "mssql")
+{
+ builder.Services.AddDbContext(options
+ => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), b =>
+ {
+ b.MigrationsAssembly("DB.Migrations.Mssql");
+ }));
+}
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+if (app.Configuration.GetValue("Https", false))
+ app.UseHttpsRedirection();
+
+app.UseAuthorization();
+app.MapControllers();
+
+using (var scope = app.Services.CreateScope())
+{
+ var db = scope.ServiceProvider.GetRequiredService();
+ if (db.Database.IsRelational() && !db.Database.IsInMemory())
+ {
+ await db.Database.MigrateAsync();
+ }
+ #if DEBUG
+ else if (db.Database.IsInMemory())
+ {
+ await DevelopmentDataSeedingHelper.Seed(db);
+ }
+ #endif
+}
+
+app.Run();
\ No newline at end of file
diff --git a/API/Properties/launchSettings.json b/API/Properties/launchSettings.json
new file mode 100644
index 0000000..78d807e
--- /dev/null
+++ b/API/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:16591",
+ "sslPort": 44319
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5012",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7174;http://localhost:5012",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/API/appsettings.Development.json b/API/appsettings.Development.json
new file mode 100644
index 0000000..f9cd5b9
--- /dev/null
+++ b/API/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "DatabaseMode": "memory"
+}
\ No newline at end of file
diff --git a/API/appsettings.json b/API/appsettings.json
new file mode 100644
index 0000000..ec5c9df
--- /dev/null
+++ b/API/appsettings.json
@@ -0,0 +1,16 @@
+{
+ "Admin": {
+ "Account": {
+ "Username": "admin",
+ "Password": "password123"
+ }
+ },
+ "Https": false,
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
\ No newline at end of file
diff --git a/DB.Migrations.Mssql/DB.Migrations.Mssql.csproj b/DB.Migrations.Mssql/DB.Migrations.Mssql.csproj
new file mode 100644
index 0000000..e571bca
--- /dev/null
+++ b/DB.Migrations.Mssql/DB.Migrations.Mssql.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/DB.Migrations.Mssql/Migrations/20240219200002_Initial.Designer.cs b/DB.Migrations.Mssql/Migrations/20240219200002_Initial.Designer.cs
new file mode 100644
index 0000000..222b90c
--- /dev/null
+++ b/DB.Migrations.Mssql/Migrations/20240219200002_Initial.Designer.cs
@@ -0,0 +1,470 @@
+//
+using System;
+using Hesketh.MecatolArchives.DB;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Hesketh.MecatolArchives.DB.Migrations.Mssql.Migrations
+{
+ [DbContext(typeof(MecatolArchivesDbContext))]
+ [Migration("20240219200002_Initial")]
+ partial class Initial
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Colour", b =>
+ {
+ b.Property("Identifier")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Hex")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Identifier");
+
+ b.ToTable("Colours");
+
+ b.HasData(
+ new
+ {
+ Identifier = new Guid("cbdfdda9-13bf-4a45-be5d-0882f6dcbad8"),
+ Hex = "",
+ Name = "_Unknown_"
+ },
+ new
+ {
+ Identifier = new Guid("564f166e-33cb-45cc-bca6-1b2d16b8bf60"),
+ Hex = "#000000",
+ Name = "Black"
+ },
+ new
+ {
+ Identifier = new Guid("53bf36a1-669c-41ed-9ced-4fa94ba038ee"),
+ Hex = "#FF0000",
+ Name = "Red"
+ },
+ new
+ {
+ Identifier = new Guid("e8ca7b27-00cd-4a2a-bc7f-17105e690e2d"),
+ Hex = "#008000",
+ Name = "Green"
+ },
+ new
+ {
+ Identifier = new Guid("51b6cc96-0e35-48f9-8665-b50bbe3fdb44"),
+ Hex = "#FFFF00",
+ Name = "Yellow"
+ },
+ new
+ {
+ Identifier = new Guid("a9c3b568-d781-452d-91ae-44b0cc8e7020"),
+ Hex = "#800080",
+ Name = "Purple"
+ },
+ new
+ {
+ Identifier = new Guid("43c078a5-0561-40f0-8adc-92afa32eaeb0"),
+ Hex = "#FFA500",
+ Name = "Orange"
+ },
+ new
+ {
+ Identifier = new Guid("daceda53-e450-4fce-82d4-ef1cdd312e38"),
+ Hex = "#FF00FF",
+ Name = "Magenta"
+ },
+ new
+ {
+ Identifier = new Guid("b5616b41-2821-4a27-85dc-fa81b899e578"),
+ Hex = "#0000FF",
+ Name = "Blue"
+ });
+ });
+
+ modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Expansion", b =>
+ {
+ b.Property("Identifier")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PlayIdentifier")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Identifier");
+
+ b.HasIndex("PlayIdentifier");
+
+ b.ToTable("Expansions");
+
+ b.HasData(
+ new
+ {
+ Identifier = new Guid("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"),
+ Name = "Prophecy of Kings"
+ },
+ new
+ {
+ Identifier = new Guid("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"),
+ Name = "Codex I: Ordinian"
+ },
+ new
+ {
+ Identifier = new Guid("9420502f-4ef0-4887-add4-3d8a4941016a"),
+ Name = "Codex II: Affinity"
+ },
+ new
+ {
+ Identifier = new Guid("1eb732ba-74ac-4993-943e-cd6f3650d310"),
+ Name = "Codex III: Vigil"
+ },
+ new
+ {
+ Identifier = new Guid("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"),
+ Name = "Omega Initiative I"
+ },
+ new
+ {
+ Identifier = new Guid("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"),
+ Name = "Omega Initiative II"
+ });
+ });
+
+ modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Faction", b =>
+ {
+ b.Property("Identifier")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Identifier");
+
+ b.ToTable("Factions");
+
+ b.HasData(
+ new
+ {
+ Identifier = new Guid("11ee6931-58d5-4808-b2c5-e3d4b7bf4343"),
+ Name = "The Arborec"
+ },
+ new
+ {
+ Identifier = new Guid("e4d8ad5c-cf62-4001-9b81-f2a31bd87b0d"),
+ Name = "The Barony of Letnev"
+ },
+ new
+ {
+ Identifier = new Guid("b01aaa66-1888-4926-803e-95a864057219"),
+ Name = "The Clan of Saar"
+ },
+ new
+ {
+ Identifier = new Guid("fcae96dd-793a-4393-9f5f-f378ff167b12"),
+ Name = "The Embers of Muaat"
+ },
+ new
+ {
+ Identifier = new Guid("c9a5311a-4798-44c5-8b97-3a5bc4eaa01a"),
+ Name = "The Emirates of Hacan"
+ },
+ new
+ {
+ Identifier = new Guid("a386b3e8-683b-4e7e-9fba-3361bdbbeef1"),
+ Name = "The Federation of Sol"
+ },
+ new
+ {
+ Identifier = new Guid("2e017142-848a-4203-a109-1fa905a5db04"),
+ Name = "The Ghosts of Creuss"
+ },
+ new
+ {
+ Identifier = new Guid("4df36a67-6b3d-4966-ae0c-6ebdb6d0a97d"),
+ Name = "The L1Z1X Mindnet"
+ },
+ new
+ {
+ Identifier = new Guid("c765da52-404b-4073-8c0c-003537169b4c"),
+ Name = "The Mentak Coalition"
+ },
+ new
+ {
+ Identifier = new Guid("2002e0b3-603f-4b66-8268-2ca8ab2bfce4"),
+ Name = "The Naalu Collective"
+ },
+ new
+ {
+ Identifier = new Guid("79c4217c-dc20-46fe-9e0b-44e852dfc64b"),
+ Name = "The Nekro Virus"
+ },
+ new
+ {
+ Identifier = new Guid("1a5777dc-4106-46ec-8168-596c7c9edd36"),
+ Name = "Sardakk N'orr"
+ },
+ new
+ {
+ Identifier = new Guid("0cc14756-19f7-42ef-acd0-156fab6bcd2b"),
+ Name = "The Universities of Jol-Nar"
+ },
+ new
+ {
+ Identifier = new Guid("9b8cf62c-9c94-49aa-b55b-11a8f4f9a7c9"),
+ Name = "The Winnu"
+ },
+ new
+ {
+ Identifier = new Guid("279eefc5-4bd3-41be-93d0-b49bd6e9b7d6"),
+ Name = "The Xxcha Kingdom"
+ },
+ new
+ {
+ Identifier = new Guid("2c637584-cdbb-485e-8c3f-bc713eebaa03"),
+ Name = "The Yin Brotherhood"
+ },
+ new
+ {
+ Identifier = new Guid("5265b1f2-4422-4d0b-8711-582330d9afe4"),
+ Name = "The Yssaril Tribes"
+ },
+ new
+ {
+ Identifier = new Guid("be81d0fd-0ba8-4d55-ac65-4eb66d49028a"),
+ Name = "The Argent Flight"
+ },
+ new
+ {
+ Identifier = new Guid("f39815c4-b651-4ed4-86e0-5b4f5f6128ef"),
+ Name = "The Empyrean"
+ },
+ new
+ {
+ Identifier = new Guid("2fffa08d-40ab-4e29-8e4a-0cda97b664be"),
+ Name = "The Mahact Gene-Sorcerers"
+ },
+ new
+ {
+ Identifier = new Guid("26251032-94c9-4331-8ccf-697755213fad"),
+ Name = "The Naaz-Rokha Alliance"
+ },
+ new
+ {
+ Identifier = new Guid("4210c2bb-f773-4721-907b-1c3e1cd11357"),
+ Name = "The Nomad"
+ },
+ new
+ {
+ Identifier = new Guid("c4e77f65-5a61-404f-8793-b672ccdb29a4"),
+ Name = "The Titans of UL"
+ },
+ new
+ {
+ Identifier = new Guid("91da5e70-a3db-4168-b18d-61ae7b899b48"),
+ Name = "The Vuil'Raith Cabal"
+ },
+ new
+ {
+ Identifier = new Guid("51ee1c82-279b-444c-a6aa-a8cd475612fd"),
+ Name = "The Council Keleres (The Mentak Coalition)"
+ },
+ new
+ {
+ Identifier = new Guid("d819ecf0-3ccd-45d1-9f19-ba045d39fd65"),
+ Name = "The Council Keleres (The Xxcha Kingdoms)"
+ },
+ new
+ {
+ Identifier = new Guid("0747201a-f0ae-4c88-840f-00a3d6c618a0"),
+ Name = "The Council Keleres (The Argent Flight)"
+ },
+ new
+ {
+ Identifier = new Guid("609382d1-c969-4144-916a-ad4c13df1352"),
+ Name = "_Unknown_"
+ });
+ });
+
+ modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Person", b =>
+ {
+ b.Property("Identifier")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Identifier");
+
+ b.ToTable("People");
+ });
+
+ modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b =>
+ {
+ b.Property("Identifier")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Map")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PointGoal")
+ .HasColumnType("bigint");
+
+ b.Property("RulesVersion")
+ .HasColumnType("float");
+
+ b.Property("UtcDate")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Identifier");
+
+ b.ToTable("Plays");
+ });
+
+ modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b =>
+ {
+ b.Property("Identifier")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ColourIdentifier")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Eliminated")
+ .HasColumnType("bit");
+
+ b.Property("FactionIdentifier")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("PersonIdentifier")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("PlayIdentifier")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Points")
+ .HasColumnType("bigint");
+
+ b.Property("Winner")
+ .HasColumnType("bit");
+
+ b.HasKey("Identifier");
+
+ b.HasIndex("ColourIdentifier");
+
+ b.HasIndex("FactionIdentifier");
+
+ b.HasIndex("PersonIdentifier");
+
+ b.HasIndex("PlayIdentifier");
+
+ b.ToTable("Players");
+ });
+
+ modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Variant", b =>
+ {
+ b.Property("Identifier")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PlayIdentifier")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Identifier");
+
+ b.HasIndex("PlayIdentifier");
+
+ b.ToTable("Variants");
+ });
+
+ modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Expansion", b =>
+ {
+ b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null)
+ .WithMany("Expansions")
+ .HasForeignKey("PlayIdentifier");
+ });
+
+ modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b =>
+ {
+ b.HasOne("Hesketh.MecatolArchives.DB.Models.Colour", "Colour")
+ .WithMany()
+ .HasForeignKey("ColourIdentifier")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Hesketh.MecatolArchives.DB.Models.Faction", "Faction")
+ .WithMany()
+ .HasForeignKey("FactionIdentifier")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Hesketh.MecatolArchives.DB.Models.Person", "Person")
+ .WithMany()
+ .HasForeignKey("PersonIdentifier")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", "Play")
+ .WithMany("Players")
+ .HasForeignKey("PlayIdentifier")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Colour");
+
+ b.Navigation("Faction");
+
+ b.Navigation("Person");
+
+ b.Navigation("Play");
+ });
+
+ modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Variant", b =>
+ {
+ b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null)
+ .WithMany("Variants")
+ .HasForeignKey("PlayIdentifier");
+ });
+
+ modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b =>
+ {
+ b.Navigation("Expansions");
+
+ b.Navigation("Players");
+
+ b.Navigation("Variants");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/DB.Migrations.Mssql/Migrations/20240219200002_Initial.cs b/DB.Migrations.Mssql/Migrations/20240219200002_Initial.cs
new file mode 100644
index 0000000..478f83e
--- /dev/null
+++ b/DB.Migrations.Mssql/Migrations/20240219200002_Initial.cs
@@ -0,0 +1,266 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
+
+namespace Hesketh.MecatolArchives.DB.Migrations.Mssql.Migrations
+{
+ ///
+ public partial class Initial : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Colours",
+ columns: table => new
+ {
+ Identifier = table.Column(type: "uniqueidentifier", nullable: false),
+ Hex = table.Column(type: "nvarchar(max)", nullable: false),
+ Name = table.Column(type: "nvarchar(max)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Colours", x => x.Identifier);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Factions",
+ columns: table => new
+ {
+ Identifier = table.Column(type: "uniqueidentifier", nullable: false),
+ Name = table.Column(type: "nvarchar(max)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Factions", x => x.Identifier);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "People",
+ columns: table => new
+ {
+ Identifier = table.Column(type: "uniqueidentifier", nullable: false),
+ Name = table.Column(type: "nvarchar(max)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_People", x => x.Identifier);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Plays",
+ columns: table => new
+ {
+ Identifier = table.Column(type: "uniqueidentifier", nullable: false),
+ UtcDate = table.Column(type: "datetime2", nullable: false),
+ RulesVersion = table.Column(type: "float", nullable: false),
+ PointGoal = table.Column(type: "bigint", nullable: false),
+ Map = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Plays", x => x.Identifier);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Expansions",
+ columns: table => new
+ {
+ Identifier = table.Column(type: "uniqueidentifier", nullable: false),
+ Name = table.Column(type: "nvarchar(max)", nullable: false),
+ PlayIdentifier = table.Column(type: "uniqueidentifier", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Expansions", x => x.Identifier);
+ table.ForeignKey(
+ name: "FK_Expansions_Plays_PlayIdentifier",
+ column: x => x.PlayIdentifier,
+ principalTable: "Plays",
+ principalColumn: "Identifier");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Players",
+ columns: table => new
+ {
+ Identifier = table.Column(type: "uniqueidentifier", nullable: false),
+ Points = table.Column(type: "bigint", nullable: false),
+ Winner = table.Column(type: "bit", nullable: false),
+ Eliminated = table.Column(type: "bit", nullable: false),
+ PersonIdentifier = table.Column(type: "uniqueidentifier", nullable: false),
+ PlayIdentifier = table.Column(type: "uniqueidentifier", nullable: false),
+ FactionIdentifier = table.Column(type: "uniqueidentifier", nullable: false),
+ ColourIdentifier = table.Column(type: "uniqueidentifier", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Players", x => x.Identifier);
+ table.ForeignKey(
+ name: "FK_Players_Colours_ColourIdentifier",
+ column: x => x.ColourIdentifier,
+ principalTable: "Colours",
+ principalColumn: "Identifier",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_Players_Factions_FactionIdentifier",
+ column: x => x.FactionIdentifier,
+ principalTable: "Factions",
+ principalColumn: "Identifier",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_Players_People_PersonIdentifier",
+ column: x => x.PersonIdentifier,
+ principalTable: "People",
+ principalColumn: "Identifier",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_Players_Plays_PlayIdentifier",
+ column: x => x.PlayIdentifier,
+ principalTable: "Plays",
+ principalColumn: "Identifier",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Variants",
+ columns: table => new
+ {
+ Identifier = table.Column(type: "uniqueidentifier", nullable: false),
+ Name = table.Column(type: "nvarchar(max)", nullable: false),
+ PlayIdentifier = table.Column(type: "uniqueidentifier", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Variants", x => x.Identifier);
+ table.ForeignKey(
+ name: "FK_Variants_Plays_PlayIdentifier",
+ column: x => x.PlayIdentifier,
+ principalTable: "Plays",
+ principalColumn: "Identifier");
+ });
+
+ migrationBuilder.InsertData(
+ table: "Colours",
+ columns: new[] { "Identifier", "Hex", "Name" },
+ values: new object[,]
+ {
+ { new Guid("43c078a5-0561-40f0-8adc-92afa32eaeb0"), "#FFA500", "Orange" },
+ { new Guid("51b6cc96-0e35-48f9-8665-b50bbe3fdb44"), "#FFFF00", "Yellow" },
+ { new Guid("53bf36a1-669c-41ed-9ced-4fa94ba038ee"), "#FF0000", "Red" },
+ { new Guid("564f166e-33cb-45cc-bca6-1b2d16b8bf60"), "#000000", "Black" },
+ { new Guid("a9c3b568-d781-452d-91ae-44b0cc8e7020"), "#800080", "Purple" },
+ { new Guid("b5616b41-2821-4a27-85dc-fa81b899e578"), "#0000FF", "Blue" },
+ { new Guid("cbdfdda9-13bf-4a45-be5d-0882f6dcbad8"), "", "_Unknown_" },
+ { new Guid("daceda53-e450-4fce-82d4-ef1cdd312e38"), "#FF00FF", "Magenta" },
+ { new Guid("e8ca7b27-00cd-4a2a-bc7f-17105e690e2d"), "#008000", "Green" }
+ });
+
+ migrationBuilder.InsertData(
+ table: "Expansions",
+ columns: new[] { "Identifier", "Name", "PlayIdentifier" },
+ values: new object[,]
+ {
+ { new Guid("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"), "Omega Initiative II", null },
+ { new Guid("1eb732ba-74ac-4993-943e-cd6f3650d310"), "Codex III: Vigil", null },
+ { new Guid("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"), "Codex I: Ordinian", null },
+ { new Guid("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"), "Omega Initiative I", null },
+ { new Guid("9420502f-4ef0-4887-add4-3d8a4941016a"), "Codex II: Affinity", null },
+ { new Guid("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"), "Prophecy of Kings", null }
+ });
+
+ migrationBuilder.InsertData(
+ table: "Factions",
+ columns: new[] { "Identifier", "Name" },
+ values: new object[,]
+ {
+ { new Guid("0747201a-f0ae-4c88-840f-00a3d6c618a0"), "The Council Keleres (The Argent Flight)" },
+ { new Guid("0cc14756-19f7-42ef-acd0-156fab6bcd2b"), "The Universities of Jol-Nar" },
+ { new Guid("11ee6931-58d5-4808-b2c5-e3d4b7bf4343"), "The Arborec" },
+ { new Guid("1a5777dc-4106-46ec-8168-596c7c9edd36"), "Sardakk N'orr" },
+ { new Guid("2002e0b3-603f-4b66-8268-2ca8ab2bfce4"), "The Naalu Collective" },
+ { new Guid("26251032-94c9-4331-8ccf-697755213fad"), "The Naaz-Rokha Alliance" },
+ { new Guid("279eefc5-4bd3-41be-93d0-b49bd6e9b7d6"), "The Xxcha Kingdom" },
+ { new Guid("2c637584-cdbb-485e-8c3f-bc713eebaa03"), "The Yin Brotherhood" },
+ { new Guid("2e017142-848a-4203-a109-1fa905a5db04"), "The Ghosts of Creuss" },
+ { new Guid("2fffa08d-40ab-4e29-8e4a-0cda97b664be"), "The Mahact Gene-Sorcerers" },
+ { new Guid("4210c2bb-f773-4721-907b-1c3e1cd11357"), "The Nomad" },
+ { new Guid("4df36a67-6b3d-4966-ae0c-6ebdb6d0a97d"), "The L1Z1X Mindnet" },
+ { new Guid("51ee1c82-279b-444c-a6aa-a8cd475612fd"), "The Council Keleres (The Mentak Coalition)" },
+ { new Guid("5265b1f2-4422-4d0b-8711-582330d9afe4"), "The Yssaril Tribes" },
+ { new Guid("609382d1-c969-4144-916a-ad4c13df1352"), "_Unknown_" },
+ { new Guid("79c4217c-dc20-46fe-9e0b-44e852dfc64b"), "The Nekro Virus" },
+ { new Guid("91da5e70-a3db-4168-b18d-61ae7b899b48"), "The Vuil'Raith Cabal" },
+ { new Guid("9b8cf62c-9c94-49aa-b55b-11a8f4f9a7c9"), "The Winnu" },
+ { new Guid("a386b3e8-683b-4e7e-9fba-3361bdbbeef1"), "The Federation of Sol" },
+ { new Guid("b01aaa66-1888-4926-803e-95a864057219"), "The Clan of Saar" },
+ { new Guid("be81d0fd-0ba8-4d55-ac65-4eb66d49028a"), "The Argent Flight" },
+ { new Guid("c4e77f65-5a61-404f-8793-b672ccdb29a4"), "The Titans of UL" },
+ { new Guid("c765da52-404b-4073-8c0c-003537169b4c"), "The Mentak Coalition" },
+ { new Guid("c9a5311a-4798-44c5-8b97-3a5bc4eaa01a"), "The Emirates of Hacan" },
+ { new Guid("d819ecf0-3ccd-45d1-9f19-ba045d39fd65"), "The Council Keleres (The Xxcha Kingdoms)" },
+ { new Guid("e4d8ad5c-cf62-4001-9b81-f2a31bd87b0d"), "The Barony of Letnev" },
+ { new Guid("f39815c4-b651-4ed4-86e0-5b4f5f6128ef"), "The Empyrean" },
+ { new Guid("fcae96dd-793a-4393-9f5f-f378ff167b12"), "The Embers of Muaat" }
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Expansions_PlayIdentifier",
+ table: "Expansions",
+ column: "PlayIdentifier");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Players_ColourIdentifier",
+ table: "Players",
+ column: "ColourIdentifier");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Players_FactionIdentifier",
+ table: "Players",
+ column: "FactionIdentifier");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Players_PersonIdentifier",
+ table: "Players",
+ column: "PersonIdentifier");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Players_PlayIdentifier",
+ table: "Players",
+ column: "PlayIdentifier");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Variants_PlayIdentifier",
+ table: "Variants",
+ column: "PlayIdentifier");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Expansions");
+
+ migrationBuilder.DropTable(
+ name: "Players");
+
+ migrationBuilder.DropTable(
+ name: "Variants");
+
+ migrationBuilder.DropTable(
+ name: "Colours");
+
+ migrationBuilder.DropTable(
+ name: "Factions");
+
+ migrationBuilder.DropTable(
+ name: "People");
+
+ migrationBuilder.DropTable(
+ name: "Plays");
+ }
+ }
+}
diff --git a/DB.Migrations.Mssql/Migrations/20240220210932_VariantsExpansionsManyPlays.Designer.cs b/DB.Migrations.Mssql/Migrations/20240220210932_VariantsExpansionsManyPlays.Designer.cs
new file mode 100644
index 0000000..2ce92a9
--- /dev/null
+++ b/DB.Migrations.Mssql/Migrations/20240220210932_VariantsExpansionsManyPlays.Designer.cs
@@ -0,0 +1,502 @@
+//
+using System;
+using Hesketh.MecatolArchives.DB;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Hesketh.MecatolArchives.DB.Migrations.Mssql.Migrations
+{
+ [DbContext(typeof(MecatolArchivesDbContext))]
+ [Migration("20240220210932_VariantsExpansionsManyPlays")]
+ partial class VariantsExpansionsManyPlays
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("ExpansionPlay", b =>
+ {
+ b.Property("ExpansionsIdentifier")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("PlaysIdentifier")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("ExpansionsIdentifier", "PlaysIdentifier");
+
+ b.HasIndex("PlaysIdentifier");
+
+ b.ToTable("ExpansionPlay");
+ });
+
+ modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Colour", b =>
+ {
+ b.Property("Identifier")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Hex")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property