mirror of
https://github.com/AndrioCelos/TableturfBattleApp.git
synced 2026-03-21 17:34:28 -05:00
Initial commit
This commit is contained in:
commit
1f44ab6d56
225
.editorconfig
Normal file
225
.editorconfig
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
# Remove the line below if you want to inherit .editorconfig settings from higher directories
|
||||
root = true
|
||||
|
||||
# C# files
|
||||
[*.cs]
|
||||
|
||||
#### Core EditorConfig Options ####
|
||||
|
||||
# Indentation and spacing
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
tab_width = 4
|
||||
|
||||
# New line preferences
|
||||
end_of_line = crlf
|
||||
insert_final_newline = false
|
||||
|
||||
#### .NET Coding Conventions ####
|
||||
|
||||
# Organize usings
|
||||
dotnet_separate_import_directive_groups = true
|
||||
dotnet_sort_system_directives_first = true
|
||||
file_header_template = unset
|
||||
|
||||
# this. and Me. preferences
|
||||
dotnet_style_qualification_for_event = true:suggestion
|
||||
dotnet_style_qualification_for_field = true
|
||||
dotnet_style_qualification_for_method = true:suggestion
|
||||
dotnet_style_qualification_for_property = true:suggestion
|
||||
|
||||
# Language keywords vs BCL types preferences
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
# Parentheses preferences
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:suggestion
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
|
||||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
|
||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
|
||||
|
||||
# Modifier preferences
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members
|
||||
|
||||
# Expression-level preferences
|
||||
dotnet_style_coalesce_expression = true
|
||||
dotnet_style_collection_initializer = true
|
||||
dotnet_style_explicit_tuple_names = true
|
||||
dotnet_style_namespace_match_folder = true
|
||||
dotnet_style_null_propagation = true
|
||||
dotnet_style_object_initializer = true
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
dotnet_style_prefer_auto_properties = true:suggestion
|
||||
dotnet_style_prefer_compound_assignment = true
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
|
||||
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true
|
||||
dotnet_style_prefer_inferred_tuple_names = true
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true
|
||||
dotnet_style_prefer_simplified_interpolation = true
|
||||
|
||||
# Field preferences
|
||||
dotnet_style_readonly_field = true
|
||||
|
||||
# Parameter preferences
|
||||
dotnet_code_quality_unused_parameters = non_public:warning
|
||||
|
||||
# Suppression preferences
|
||||
dotnet_remove_unnecessary_suppression_exclusions = none
|
||||
|
||||
# New line preferences
|
||||
dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion
|
||||
dotnet_style_allow_statement_immediately_after_block_experimental = true
|
||||
|
||||
#### C# Coding Conventions ####
|
||||
|
||||
# var preferences
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
csharp_style_var_for_built_in_types = true
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
|
||||
# Expression-bodied members
|
||||
csharp_style_expression_bodied_accessors = true:suggestion
|
||||
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_indexers = true:suggestion
|
||||
csharp_style_expression_bodied_lambdas = true:suggestion
|
||||
csharp_style_expression_bodied_local_functions = true:suggestion
|
||||
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_properties = true:suggestion
|
||||
|
||||
# Pattern matching preferences
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true
|
||||
csharp_style_prefer_extended_property_pattern = true
|
||||
csharp_style_prefer_not_pattern = true
|
||||
csharp_style_prefer_pattern_matching = true:suggestion
|
||||
csharp_style_prefer_switch_expression = true
|
||||
|
||||
# Null-checking preferences
|
||||
csharp_style_conditional_delegate_call = true
|
||||
|
||||
# Modifier preferences
|
||||
csharp_prefer_static_local_function = true
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
|
||||
|
||||
# Code-block preferences
|
||||
csharp_prefer_braces = when_multiline
|
||||
csharp_prefer_simple_using_statement = true
|
||||
csharp_style_namespace_declarations = file_scoped:suggestion
|
||||
csharp_style_prefer_method_group_conversion = true:suggestion
|
||||
csharp_style_prefer_top_level_statements = true
|
||||
|
||||
# Expression-level preferences
|
||||
csharp_prefer_simple_default_expression = true
|
||||
csharp_style_deconstructed_variable_declaration = true
|
||||
csharp_style_implicit_object_creation_when_type_is_apparent = true
|
||||
csharp_style_inlined_variable_declaration = true
|
||||
csharp_style_prefer_index_operator = true
|
||||
csharp_style_prefer_local_over_anonymous_function = true
|
||||
csharp_style_prefer_null_check_over_type_check = true
|
||||
csharp_style_prefer_range_operator = true
|
||||
csharp_style_prefer_tuple_swap = true
|
||||
csharp_style_prefer_utf8_string_literals = true
|
||||
csharp_style_throw_expression = true
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:warning
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable
|
||||
|
||||
# 'using' directive preferences
|
||||
csharp_using_directive_placement = outside_namespace:suggestion
|
||||
|
||||
# New line preferences
|
||||
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:suggestion
|
||||
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion
|
||||
csharp_style_allow_embedded_statements_on_same_line_experimental = true
|
||||
|
||||
#### C# Formatting Rules ####
|
||||
|
||||
# New line preferences
|
||||
csharp_new_line_before_catch = false
|
||||
csharp_new_line_before_else = false
|
||||
csharp_new_line_before_finally = false
|
||||
csharp_new_line_before_members_in_anonymous_types = false
|
||||
csharp_new_line_before_members_in_object_initializers = false
|
||||
csharp_new_line_before_open_brace = none
|
||||
csharp_new_line_between_query_expression_clauses = false
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_case_contents_when_block = false
|
||||
csharp_indent_labels = one_less_than_current
|
||||
csharp_indent_switch_labels = true
|
||||
|
||||
# Space preferences
|
||||
csharp_space_after_cast = true
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_dot = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_semicolon_in_for_statement = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_around_declaration_statements = false
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_before_dot = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_between_square_brackets = false
|
||||
|
||||
# Wrapping preferences
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_preserve_single_line_statements = true
|
||||
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
401
.gitignore
vendored
Normal file
401
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
assets/
|
||||
build/
|
||||
|
||||
## 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/main/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/
|
||||
[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
|
||||
*.tlog
|
||||
*.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 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# 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/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# 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
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
25
TableturfBattle.sln
Normal file
25
TableturfBattle.sln
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.3.32819.101
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TableturfBattleServer", "TableturfBattleServer\TableturfBattleServer.csproj", "{60E18B13-8E94-4E29-9316-4C5FEA019A85}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{60E18B13-8E94-4E29-9316-4C5FEA019A85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{60E18B13-8E94-4E29-9316-4C5FEA019A85}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{60E18B13-8E94-4E29-9316-4C5FEA019A85}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{60E18B13-8E94-4E29-9316-4C5FEA019A85}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {099B14AF-71A1-49B0-8504-F3CD7516414D}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
19
TableturfBattleClient/.vscode/launch.json
vendored
Normal file
19
TableturfBattleClient/.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"name": "Launch application in Firefox",
|
||||
"url": "file:///${workspaceFolder}/index.html",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"preLaunchTask": {
|
||||
"type": "typescript",
|
||||
"tsconfig": "tsconfig.json",
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
27
TableturfBattleClient/.vscode/tasks.json
vendored
Normal file
27
TableturfBattleClient/.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "typescript",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"label": "tsc: build - tsconfig.json"
|
||||
},
|
||||
{
|
||||
"type": "typescript",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"option": "watch",
|
||||
"problemMatcher": [
|
||||
"$tsc-watch"
|
||||
],
|
||||
"group": "build",
|
||||
"label": "tsc: watch - tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
110
TableturfBattleClient/index.html
Normal file
110
TableturfBattleClient/index.html
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Tableturf Battle</title>
|
||||
<link rel="stylesheet" href="tableturf.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="noJSSection">This application requires JavaScript.</div>
|
||||
<div id="loadingSection">Loading game data...</div>
|
||||
<div id="preGameSection" hidden>
|
||||
<p><img title="Tableturf Battle" alt="Tableturf Battle logo" id="logo" src="assets/logo.png"></p>
|
||||
<h1>Tableturf Battle</h1>
|
||||
<p><input type="text" id="nameBox" placeholder="Choose a name"></p>
|
||||
<p><button type="button" id="newGameButton">New game</button></p>
|
||||
<p><input type="text" id="gameIDBox" placeholder="Game link or UUID"> <button type="button" id="joinGameButton">Join game</button></p>
|
||||
<footer>
|
||||
<p>This website is not affiliated with Nintendo. All product names, logos, and brands are property of their respective owners.</p>
|
||||
<p><a href="https://github.com/AndrioCelos/TableturfBattleApp">GitHub</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
<div id="lobbySection" hidden>
|
||||
<p>Waiting for players to join.</p>
|
||||
<ul id="playerList"></ul>
|
||||
</div>
|
||||
<div id="deckSection" hidden>
|
||||
<p>Choose your deck.</p>
|
||||
<p><span id='countLabel'>0</span>/15 cards chosen</p>
|
||||
<p><button type="button" id="submitDeckButton" disabled>Submit</button></p>
|
||||
<p id="cardListLoadingSection">Loading cards...</p>
|
||||
<ul id="cardList"></ul>
|
||||
</div>
|
||||
<div id="gameSection" hidden>
|
||||
<div id="sidebarSection">
|
||||
<div class="playerBar" data-index="1">
|
||||
<div class="result"></div>
|
||||
<div class="name">Player 2</div>
|
||||
<div class="specialPoints"> </div>
|
||||
<div class="playerStats">
|
||||
<div class="statSpecialPoints">
|
||||
<div class="statLabel">Special points</div>
|
||||
<div class="statValue">0</div>
|
||||
</div>
|
||||
<div class="statPasses">
|
||||
<div class="statLabel">Turns passed</div>
|
||||
<div class="statValue">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="midGameContainer">
|
||||
<div id="handContainer"></div>
|
||||
<label for="passButton">
|
||||
<input type="checkbox" id="passButton"/> Pass
|
||||
</label>
|
||||
<label for="specialButton">
|
||||
<input type="checkbox" id="specialButton"/> Special Attack
|
||||
</label>
|
||||
</div>
|
||||
<div id="resultContainer" hidden>
|
||||
<div id="result">Result</div>
|
||||
</div>
|
||||
|
||||
<div class="playerBar" data-index="0">
|
||||
<div class="result"></div>
|
||||
<div class="name">Player 1</div>
|
||||
<div class="specialPoints"> </div>
|
||||
<div class="playerStats">
|
||||
<div class="statSpecialPoints">
|
||||
<div class="statLabel">Special points</div>
|
||||
<div class="statValue">0</div>
|
||||
</div>
|
||||
<div class="statPasses">
|
||||
<div class="statLabel">Turns passed</div>
|
||||
<div class="statValue">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scoreSection">
|
||||
<div id="turnNumberContainer" hidden>
|
||||
<p>Turns left</p>
|
||||
<div id="turnNumberLabel">12</div>
|
||||
</div>
|
||||
<div id="pointsContainers">
|
||||
<div class="pointsContainer" data-index="1"><span class="points">69</span><span class="pointsToContainer">↪ <span class="pointsTo">60</span></span><span class="pointsDelta">+1</span></div>
|
||||
<div class="pointsContainer" data-index="0"><span class="points">69</span><span class="pointsToContainer">↪ <span class="pointsTo">60</span></span><span class="pointsDelta">+1</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="boardSection">
|
||||
<table id="gameBoard"></table>
|
||||
<div id="redrawModal" hidden>
|
||||
<div id="redrawBox">
|
||||
<p>Redraw your starting hand?</p>
|
||||
<p><button type="button" id="redrawNoButton" data-redraw="false">Hold steady</button></p>
|
||||
<p><button type="button" id="redrawYesButton" data-redraw="true">Redraw</button></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="playsSection">
|
||||
<div class="playContainer" data-index="1"></div>
|
||||
<div class="playContainer" data-index="0"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="errorModal" hidden>
|
||||
<div id="errorModalBox">A communication error has occurred.</div>
|
||||
</div>
|
||||
<script src="build/tsbuild.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
203
TableturfBattleClient/src/Board.ts
Normal file
203
TableturfBattleClient/src/Board.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
class Board {
|
||||
table: HTMLTableElement;
|
||||
grid: Space[][] = [ ];
|
||||
cells: HTMLTableCellElement[][] = [ ];
|
||||
|
||||
playerIndex: number | null = 0;
|
||||
cardPlaying: Card | null = null;
|
||||
cardRotation = 0;
|
||||
specialAttack = false;
|
||||
|
||||
autoHighlight = true;
|
||||
|
||||
highlightX = NaN;
|
||||
highlightY = NaN;
|
||||
private highlightedCells: [x: number, y: number][] = [ ];
|
||||
|
||||
onclick: ((x: number, y: number) => void) | null = null;
|
||||
|
||||
constructor(table: HTMLTableElement) {
|
||||
this.table = table;
|
||||
table.addEventListener('mouseleave', _ => {
|
||||
if (this.autoHighlight) this.clearHighlight()
|
||||
});
|
||||
}
|
||||
|
||||
checkMoveLegality(playerIndex: number, card: Card, x: number, y: number, rotation: number, isSpecialAttack: boolean): string | null {
|
||||
let isAnchored = false;
|
||||
for (let dx = 0; dx < 8; dx++) {
|
||||
for (let dy = 0; dy < 8; dy++) {
|
||||
if (card.getSpace(dx, dy, rotation) == Space.Empty)
|
||||
continue;
|
||||
var x2 = x + dx;
|
||||
var y2 = y + dy;
|
||||
if (x2 < 0 || x2 >= this.grid.length || y2 < 0 || y2 >= this.grid[x2].length)
|
||||
return "It cannot overlap a wall or out of bounds."; // Out of bounds.
|
||||
const space = this.grid[x2][y2];
|
||||
if (space == Space.Wall || space == Space.OutOfBounds)
|
||||
return "It cannot overlap a wall or out of bounds.";
|
||||
if (space >= Space.SpecialInactive1)
|
||||
return "It cannot overlap a special space."
|
||||
if (space != Space.Empty && !isSpecialAttack)
|
||||
return "It cannot overlap existing ink without a special attack.";
|
||||
if (!isAnchored) {
|
||||
// A normal play must be adjacent to ink of the player's colour.
|
||||
// A special attack must be adjacent to a special space of theirs.
|
||||
for (let dy2 = -1; dy2 <= 1; dy2++) {
|
||||
for (let dx2 = -1; dx2 <= 1; dx2++) {
|
||||
if (dx2 == 0 && dy2 == 0) continue;
|
||||
var x3 = x2 + dx2;
|
||||
var y3 = y2 + dy2;
|
||||
if (x3 < 0 || x3 >= this.grid.length || y3 < 0 || y3 >= this.grid[x3].length)
|
||||
continue;
|
||||
if (this.grid[x3][y3] >= (isSpecialAttack ? Space.SpecialInactive1 : Space.Ink1)
|
||||
&& ((this.grid[x3][y3]) & 3) == playerIndex) {
|
||||
isAnchored = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return isAnchored ? null : (isSpecialAttack ? "It must be next to one of your special spaces." : "It must be next to your turf.");
|
||||
}
|
||||
|
||||
refreshHighlight() {
|
||||
let legal = this.playerIndex == null || this.cardPlaying == null ? false
|
||||
: this.checkMoveLegality(this.playerIndex, this.cardPlaying, this.highlightX, this.highlightY, this.cardRotation, this.specialAttack) == null;
|
||||
|
||||
this.clearHighlight();
|
||||
if (this.cardPlaying != null && this.playerIndex != null) {
|
||||
for (let dx = 0; dx < 8; dx++) {
|
||||
const x2 = this.highlightX + dx;
|
||||
if (x2 < 0 || x2 >= this.grid.length)
|
||||
continue;
|
||||
for (let dy = 0; dy < 8; dy++) {
|
||||
const y2 = this.highlightY + dy;
|
||||
if (y2 < 0 || y2 >= this.grid[x2].length || this.grid[x2][y2] == Space.OutOfBounds)
|
||||
continue;
|
||||
const space = this.cardPlaying.getSpace(dx, dy, this.cardRotation);
|
||||
if (space != Space.Empty) {
|
||||
const cell = this.cells[x2][y2];
|
||||
cell.classList.add('hover');
|
||||
cell.classList.add(`hover${this.playerIndex + 1}`);
|
||||
if (!legal)
|
||||
cell.classList.add('hoverillegal');
|
||||
if (space == Space.SpecialInactive1)
|
||||
cell.classList.add('hoverspecial');
|
||||
this.highlightedCells.push([x2, y2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearHighlight() {
|
||||
for (const [x, y] of this.highlightedCells) {
|
||||
this.cells[x][y].setAttribute('class', Space[this.grid[x][y]] );
|
||||
}
|
||||
this.highlightedCells.splice(0);
|
||||
}
|
||||
|
||||
resize(grid: Space[][]) {
|
||||
if (grid.length == 0)
|
||||
throw new Error('Grid must not be empty.');
|
||||
|
||||
this.grid = grid || this.grid;
|
||||
|
||||
while (this.table.firstChild) {
|
||||
this.table.removeChild(this.table.firstChild);
|
||||
}
|
||||
this.cells.splice(0);
|
||||
|
||||
const boardWidth = grid.length;
|
||||
const boardHeight = grid[0].length;
|
||||
|
||||
this.table.style.setProperty('--board-width', boardWidth.toString());
|
||||
this.table.style.setProperty('--board-height', boardHeight.toString());
|
||||
|
||||
const trs: HTMLTableRowElement[] = [ ];
|
||||
for (let y = 0; y < boardHeight; y++) {
|
||||
const tr = document.createElement('tr');
|
||||
trs.push(tr);
|
||||
this.table.appendChild(tr);
|
||||
}
|
||||
|
||||
for (let x = 0; x < boardWidth; x++) {
|
||||
const col: HTMLTableCellElement[] = [ ];
|
||||
for (let y = 0; y < grid[x].length; y++) {
|
||||
const td = document.createElement('td');
|
||||
td.dataset.x = x.toString();
|
||||
td.dataset.y = y.toString();
|
||||
trs[y].appendChild(td);
|
||||
col.push(td);
|
||||
td.addEventListener('mousemove', e => {
|
||||
if (this.autoHighlight) {
|
||||
const x = parseInt((e.target as HTMLTableCellElement).dataset.x!) - 3;
|
||||
const y = parseInt((e.target as HTMLTableCellElement).dataset.y!) - 3;
|
||||
if (x != this.highlightX || y != this.highlightY) {
|
||||
this.highlightX = x;
|
||||
this.highlightY = y;
|
||||
this.refreshHighlight();
|
||||
}
|
||||
}
|
||||
});
|
||||
td.addEventListener('click', e => {
|
||||
if (this.autoHighlight && this.onclick) {
|
||||
const x = parseInt((e.target as HTMLTableCellElement).dataset.x!) - 3;
|
||||
const y = parseInt((e.target as HTMLTableCellElement).dataset.y!) - 3;
|
||||
this.onclick(x, y);
|
||||
}
|
||||
});
|
||||
td.addEventListener('wheel', e => {
|
||||
if (this.autoHighlight) {
|
||||
e.preventDefault();
|
||||
if (e.deltaY > 0) this.cardRotation++;
|
||||
else if (e.deltaY < 0) this.cardRotation--;
|
||||
this.refreshHighlight();
|
||||
}
|
||||
});
|
||||
}
|
||||
this.cells.push(col);
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.clearHighlight();
|
||||
for (let x = 0; x < this.grid.length; x++) {
|
||||
for (let y = 0; y < this.grid[x].length; y++) {
|
||||
this.cells[x][y].setAttribute('class', Space[this.grid[x][y]] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getScore(playerIndex: number) {
|
||||
let count = 0;
|
||||
for (let x = 0; x < this.grid.length; x++) {
|
||||
for (let y = 0; y < this.grid[x].length; y++) {
|
||||
const space = this.grid[x][y];
|
||||
if (space >= Space.Ink1 && (space & 3) == playerIndex)
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a value indicating whether a specified player can play the specified card anywhere.
|
||||
*/
|
||||
canPlayCard(playerIndex: number, card: Card, isSpecialAttack: boolean) {
|
||||
for (let rotation = 0; rotation < 3; rotation++) {
|
||||
for (let x = -4; x < this.grid.length - 3; x++) {
|
||||
for (let y = -4; y < this.grid[0].length - 3; y++) {
|
||||
if (this.checkMoveLegality(playerIndex, card, x, y, rotation, isSpecialAttack) == null)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
38
TableturfBattleClient/src/Card.ts
Normal file
38
TableturfBattleClient/src/Card.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
class Card {
|
||||
number: number;
|
||||
name: string;
|
||||
rarity: Rarity;
|
||||
specialCost: number;
|
||||
grid: Space[][];
|
||||
size: number;
|
||||
|
||||
constructor(number: number, name: string, rarity: Rarity, specialCost: number, grid: Space[][]) {
|
||||
this.number = number;
|
||||
this.name = name;
|
||||
this.rarity = rarity;
|
||||
this.specialCost = specialCost;
|
||||
this.grid = grid;
|
||||
|
||||
let size = 0;
|
||||
for (let y = 0; y < 8; y++) {
|
||||
for (let x = 0; x < 8; x++) {
|
||||
if (grid[x][y] != Space.Empty)
|
||||
size++;
|
||||
}
|
||||
}
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
static fromJson(obj: any) {
|
||||
return new Card(obj.number, obj.name, obj.rarity, obj.specialCost, obj.grid);
|
||||
}
|
||||
|
||||
getSpace(x: number, y: number, rotation: number) {
|
||||
switch (rotation & 3) {
|
||||
case 0: return this.grid[x][y];
|
||||
case 1: return this.grid[y][7 - x];
|
||||
case 2: return this.grid[7 - x][7 - y];
|
||||
default: return this.grid[7 - y][x];
|
||||
}
|
||||
}
|
||||
}
|
||||
98
TableturfBattleClient/src/CardButton.ts
Normal file
98
TableturfBattleClient/src/CardButton.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
class CardButton {
|
||||
private static idNumber = 0;
|
||||
|
||||
readonly element: HTMLLabelElement;
|
||||
readonly inputElement: HTMLInputElement;
|
||||
readonly card: Card;
|
||||
|
||||
constructor(type: 'checkbox' | 'radio', card: Card) {
|
||||
this.card = card;
|
||||
|
||||
let el = document.createElement('label');
|
||||
this.element = el;
|
||||
el.setAttribute('for', `card${CardButton.idNumber}`);
|
||||
el.setAttribute('type', 'checkbox')
|
||||
el.classList.add('card');
|
||||
el.classList.add([ 'common', 'rare', 'fresh' ][card.rarity]);
|
||||
|
||||
let size = 0;
|
||||
let table = document.createElement('table');
|
||||
table.classList.add('cardGrid');
|
||||
for (var y = 0; y < 8; y++) {
|
||||
let tr = document.createElement('tr');
|
||||
table.appendChild(tr);
|
||||
for (var x = 0; x < 8; x++) {
|
||||
let td = document.createElement('td');
|
||||
if (card.grid[x][y] == Space.Ink1) {
|
||||
size++;
|
||||
td.classList.add('ink');
|
||||
} else if (card.grid[x][y] == Space.SpecialInactive1) {
|
||||
size++;
|
||||
td.classList.add('special');
|
||||
}
|
||||
tr.appendChild(td);
|
||||
}
|
||||
}
|
||||
|
||||
let row = document.createElement('div');
|
||||
row.className = 'cardHeader';
|
||||
el.appendChild(row);
|
||||
|
||||
let checkBox = document.createElement('input');
|
||||
this.inputElement = checkBox;
|
||||
checkBox.setAttribute('type', type)
|
||||
checkBox.id = `card${CardButton.idNumber}`;
|
||||
checkBox.dataset.number = card.number.toString();
|
||||
checkBox.addEventListener('change', e => {
|
||||
if (checkBox.checked)
|
||||
el.classList.add('checked');
|
||||
else
|
||||
el.classList.remove('checked');
|
||||
});
|
||||
row.appendChild(checkBox);
|
||||
|
||||
let el2 = document.createElement('div');
|
||||
el2.classList.add('cardNumber');
|
||||
el2.innerText = card.number.toString();
|
||||
row.appendChild(el2);
|
||||
|
||||
el2 = document.createElement('div');
|
||||
el2.classList.add('cardName');
|
||||
el2.innerText = card.name;
|
||||
row.appendChild(el2);
|
||||
|
||||
el.appendChild(table);
|
||||
|
||||
row = document.createElement('div');
|
||||
row.className = 'cardFooter';
|
||||
el.appendChild(row);
|
||||
|
||||
|
||||
el2 = document.createElement('div');
|
||||
el2.classList.add('cardSize');
|
||||
el2.innerText = size.toString();
|
||||
row.appendChild(el2);
|
||||
|
||||
el2 = document.createElement('div');
|
||||
el2.classList.add('cardSpecialCost');
|
||||
row.appendChild(el2);
|
||||
|
||||
for (let i = 1; i <= card.specialCost; i++) {
|
||||
const el3 = document.createElement('div');
|
||||
el3.classList.add('cardSpecialPoint');
|
||||
el3.innerText = i.toString();
|
||||
el2.appendChild(el3);
|
||||
}
|
||||
|
||||
CardButton.idNumber++;
|
||||
}
|
||||
|
||||
get enabled() { return !this.inputElement.disabled; }
|
||||
set enabled(value: boolean) {
|
||||
this.inputElement.disabled = !value;
|
||||
if (value)
|
||||
this.element.classList.remove('disabled');
|
||||
else
|
||||
this.element.classList.add('disabled');
|
||||
}
|
||||
}
|
||||
31
TableturfBattleClient/src/CardDatabase.ts
Normal file
31
TableturfBattleClient/src/CardDatabase.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const cardDatabase = {
|
||||
cards: null as Card[] | null,
|
||||
loadAsync() {
|
||||
return new Promise<Card[]>((resolve, reject) => {
|
||||
if (cardDatabase.cards != null) {
|
||||
resolve(cardDatabase.cards);
|
||||
return;
|
||||
}
|
||||
const cardListRequest = new XMLHttpRequest();
|
||||
cardListRequest.open('GET', 'http://localhost:3333/api/cards');
|
||||
cardListRequest.addEventListener('load', e => {
|
||||
const cards = [ ];
|
||||
if (cardListRequest.status == 200) {
|
||||
const s = cardListRequest.responseText;
|
||||
const response = JSON.parse(s) as object[];
|
||||
for (const o of response) {
|
||||
cards.push(Card.fromJson(o));
|
||||
}
|
||||
cardDatabase.cards = cards;
|
||||
resolve(cards);
|
||||
} else {
|
||||
reject(new Error(`Error downloading card database: response was ${cardListRequest.status}`));
|
||||
}
|
||||
});
|
||||
cardListRequest.addEventListener('error', e => {
|
||||
reject(new Error('Error downloading card database: no further information.'))
|
||||
});
|
||||
cardListRequest.send();
|
||||
});
|
||||
}
|
||||
}
|
||||
7
TableturfBattleClient/src/GameState.ts
Normal file
7
TableturfBattleClient/src/GameState.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
enum GameState {
|
||||
WaitingForPlayers,
|
||||
Preparing,
|
||||
Redraw,
|
||||
Ongoing,
|
||||
Ended
|
||||
}
|
||||
23
TableturfBattleClient/src/GameVariables.ts
Normal file
23
TableturfBattleClient/src/GameVariables.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/** A UUID used to identify the client. */
|
||||
let clientToken = window.localStorage.getItem('clientToken') || '';
|
||||
/** The data of the current game, or null if not in a game. */
|
||||
let currentGame: {
|
||||
id: string,
|
||||
/** The list of players in the current game. May have empty slots. */
|
||||
players: (Player | null)[],
|
||||
/** The user's player data, or null if they are spectating. */
|
||||
me: {
|
||||
playerIndex: number,
|
||||
hand: Card[] | null,
|
||||
deck: Card[] | null,
|
||||
cardsUsed: number[]
|
||||
} | null,
|
||||
/** The WebSocket used for receiving game events, or null if not yet connected. */
|
||||
webSocket: WebSocket
|
||||
} | null;
|
||||
|
||||
const playerList = document.getElementById('playerList')!;
|
||||
const playerListItems: HTMLElement[] = [ ];
|
||||
|
||||
const canPlayCard = [ false, false, false, false ];
|
||||
const canPlayCardAsSpecialAttack = [ false, false, false, false ];
|
||||
12
TableturfBattleClient/src/Move.ts
Normal file
12
TableturfBattleClient/src/Move.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
interface Move {
|
||||
card: Card;
|
||||
isPass: boolean;
|
||||
}
|
||||
|
||||
interface PlayMove extends Move {
|
||||
isPass: true;
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
isSpecialAttack: boolean;
|
||||
}
|
||||
314
TableturfBattleClient/src/Pages/GamePage.ts
Normal file
314
TableturfBattleClient/src/Pages/GamePage.ts
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
const board = new Board(document.getElementById('gameBoard') as HTMLTableElement);
|
||||
const turnNumberLabel = new TurnNumberLabel(document.getElementById('turnNumberContainer')!, document.getElementById('turnNumberLabel')!);
|
||||
const handButtons: CardButton[] = [ ];
|
||||
|
||||
const passButton = document.getElementById('passButton') as HTMLInputElement;
|
||||
const specialButton = document.getElementById('specialButton') as HTMLInputElement;
|
||||
|
||||
const handContainer = document.getElementById('handContainer')!;
|
||||
const redrawModal = document.getElementById('redrawModal')!;
|
||||
|
||||
const midGameContainer = document.getElementById('midGameContainer')!;
|
||||
const resultContainer = document.getElementById('resultContainer')!;
|
||||
const resultElement = document.getElementById('result')!;
|
||||
|
||||
const playerBars = Array.from(document.getElementsByClassName('playerBar'), el => new PlayerBar(el as HTMLDivElement));
|
||||
playerBars.sort((a, b) => a.playerIndex - b.playerIndex);
|
||||
|
||||
const playContainers = Array.from(document.getElementsByClassName('playContainer')) as HTMLDivElement[];
|
||||
playContainers.sort((a, b) => parseInt(a.dataset.index || '0') - parseInt(b.dataset.index || '0'));
|
||||
|
||||
let testMode = false;
|
||||
let canPlay = false;
|
||||
|
||||
let cols: Space[][] = [ ];
|
||||
for (let x = 0; x < 9; x++) {
|
||||
const col = [ ];
|
||||
for (let y = 0; y < 26; y++)
|
||||
col.push(Space.Empty);
|
||||
cols.push(col);
|
||||
}
|
||||
cols[4][4] = Space.SpecialInactive2;
|
||||
cols[4][21] = Space.SpecialInactive1;
|
||||
board.resize(cols);
|
||||
|
||||
function loadPlayers(players: (Player | null)[]) {
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
const player = players[i];
|
||||
if (player != null) {
|
||||
currentGame!.players[i] = players[i];
|
||||
playerBars[i].name = player.name;
|
||||
updateStats(i);
|
||||
document.body.style.setProperty(`--primary-colour-${i + 1}`, `rgb(${player.colour.r}, ${player.colour.g}, ${player.colour.b})`);
|
||||
document.body.style.setProperty(`--special-colour-${i + 1}`, `rgb(${player.specialColour.r}, ${player.specialColour.g}, ${player.specialColour.b})`);
|
||||
document.body.style.setProperty(`--special-accent-colour-${i + 1}`, `rgb(${player.specialAccentColour.r}, ${player.specialAccentColour.g}, ${player.specialAccentColour.b})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateStats(playerIndex: number) {
|
||||
if (currentGame == null) return;
|
||||
playerBars[playerIndex].points = board.getScore(playerIndex);
|
||||
playerBars[playerIndex].pointsDelta = 0;
|
||||
playerBars[playerIndex].pointsTo = 0;
|
||||
playerBars[playerIndex].specialPoints = currentGame.players[playerIndex]!.specialPoints;
|
||||
playerBars[playerIndex].statSpecialPointsElement.innerText = currentGame.players[playerIndex]!.totalSpecialPoints.toString();
|
||||
playerBars[playerIndex].statPassesElement.innerText = currentGame.players[playerIndex]!.passes.toString();
|
||||
}
|
||||
|
||||
function showReady(playerIndex: number) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'cardBack';
|
||||
el.innerText = 'Ready';
|
||||
playContainers[playerIndex].appendChild(el);
|
||||
}
|
||||
|
||||
function clearPlayContainers() {
|
||||
for (const container of playContainers) {
|
||||
while (container.firstChild)
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function setupControlsForPlay() {
|
||||
passButton.checked = false;
|
||||
specialButton.checked = false;
|
||||
board.specialAttack = false;
|
||||
board.cardPlaying = null;
|
||||
if (canPlay && currentGame?.me?.hand != null) {
|
||||
passButton.disabled = false;
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
canPlayCard[i] = board.canPlayCard(currentGame.me.playerIndex, currentGame.me.hand[i], false);
|
||||
canPlayCardAsSpecialAttack[i] = currentGame.players[currentGame.me.playerIndex]!.specialPoints >= currentGame.me.hand[i].specialCost
|
||||
&& board.canPlayCard(currentGame.me.playerIndex, currentGame.me.hand[i], true);
|
||||
handButtons[i].inputElement.disabled = !canPlayCard[i];
|
||||
}
|
||||
|
||||
specialButton.disabled = !canPlayCardAsSpecialAttack.includes(true);
|
||||
board.autoHighlight = true;
|
||||
} else {
|
||||
for (const button of handButtons) {
|
||||
button.inputElement.disabled = true;
|
||||
passButton.disabled = true;
|
||||
specialButton.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function playInkAnimations(data: {
|
||||
game: { state: GameState, board: Space[][], turnNumber: number, players: (Player | null)[] },
|
||||
placements: { cards: { playerIndex: number, card: Card }[], spacesAffected: { space: { x: number, y: number }, newState: Space }[] }[],
|
||||
specialSpacesActivated: { x: number, y: number }[]
|
||||
}, anySpecialAttacks: boolean) {
|
||||
const inkPlaced = new Set<number>();
|
||||
const placements = data.placements;
|
||||
board.clearHighlight();
|
||||
canPlay = false;
|
||||
board.autoHighlight = false;
|
||||
await delay(anySpecialAttacks ? 3000 : 1000);
|
||||
for (const placement of placements) {
|
||||
// Skip the delay when cards don't overlap.
|
||||
if (placement.spacesAffected.find(p => inkPlaced.has(p.space.y * 37 + p.space.x))) {
|
||||
inkPlaced.clear();
|
||||
await delay(1000);
|
||||
}
|
||||
|
||||
for (const p of placement.spacesAffected) {
|
||||
inkPlaced.add(p.space.y * 37 + p.space.x);
|
||||
board.grid[p.space.x][p.space.y] = p.newState;
|
||||
}
|
||||
board.refresh();
|
||||
}
|
||||
await delay(1000);
|
||||
|
||||
// Show special spaces.
|
||||
board.grid = data.game.board;
|
||||
board.refresh();
|
||||
if (data.specialSpacesActivated.length > 0)
|
||||
await delay(1000); // Delay if we expect that this changed the board.
|
||||
for (let i = 0; i < playerBars.length; i++) {
|
||||
const player = data.game.players[i];
|
||||
if (player)
|
||||
playerBars[i].specialPoints = player.specialPoints;
|
||||
}
|
||||
for (let i = 0; i < playerBars.length; i++) {
|
||||
playerBars[i].pointsDelta = board.getScore(i) - playerBars[i].points;
|
||||
}
|
||||
await delay(1000);
|
||||
for (let i = 0; i < playerBars.length; i++) {
|
||||
updateStats(i);
|
||||
}
|
||||
await delay(1000);
|
||||
}
|
||||
|
||||
function showResult() {
|
||||
midGameContainer.hidden = true;
|
||||
resultContainer.hidden = false;
|
||||
|
||||
let winners = [ 0 ]; let maxPoints = playerBars[0].points;
|
||||
for (let i = 1; i < playerBars.length; i++) {
|
||||
if (playerBars[i].points > maxPoints) {
|
||||
winners.splice(0);
|
||||
winners.push(i);
|
||||
maxPoints = playerBars[i].points;
|
||||
} else if (playerBars[i].points == maxPoints)
|
||||
winners.push(i);
|
||||
}
|
||||
|
||||
if (currentGame == null) return;
|
||||
for (let i = 0; i < currentGame.players.length; i++) {
|
||||
const el = playerBars[i].resultElement;
|
||||
if (winners.includes(i)) {
|
||||
if (winners.length == 1) {
|
||||
el.classList.add('win');
|
||||
el.classList.remove('lose');
|
||||
el.classList.remove('draw');
|
||||
el.innerText = 'Win!';
|
||||
} else {
|
||||
el.classList.remove('win');
|
||||
el.classList.remove('lose');
|
||||
el.classList.add('draw');
|
||||
el.innerText = 'Draw!';
|
||||
}
|
||||
} else {
|
||||
el.classList.remove('win');
|
||||
el.classList.add('lose');
|
||||
el.classList.remove('draw');
|
||||
el.innerText = 'Lose...';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateHand(cards: any[]) {
|
||||
for (const button of handButtons) {
|
||||
handContainer.removeChild(button.element);
|
||||
}
|
||||
handButtons.splice(0);
|
||||
|
||||
if (!currentGame?.me) return;
|
||||
currentGame.me.hand = cards.map(Card.fromJson);
|
||||
if (cards) {
|
||||
for (const card of currentGame.me.hand) {
|
||||
const button = new CardButton('radio', card);
|
||||
button.inputElement.name = 'handCard';
|
||||
handButtons.push(button);
|
||||
button.inputElement.addEventListener('change', e => {
|
||||
if (button.inputElement.checked) {
|
||||
if (passButton.checked) {
|
||||
if (canPlay) {
|
||||
canPlay = false;
|
||||
// Send the play to the server.
|
||||
let req = new XMLHttpRequest();
|
||||
req.open('POST', `http://localhost:3333/api/games/${currentGame!.id}/play`);
|
||||
req.addEventListener('load', e => {
|
||||
if (req.status == 204) {
|
||||
}
|
||||
});
|
||||
let data = new URLSearchParams();
|
||||
data.append('clientToken', clientToken);
|
||||
data.append('cardNumber', card.number.toString());
|
||||
data.append('isPass', 'true');
|
||||
req.send(data.toString());
|
||||
|
||||
board.autoHighlight = false;
|
||||
}
|
||||
}
|
||||
board.cardPlaying = card;
|
||||
board.cardRotation = 0;
|
||||
}
|
||||
});
|
||||
handContainer.appendChild(button.element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(document.getElementById('redrawNoButton') as HTMLButtonElement).addEventListener('click', redrawButton_click);
|
||||
(document.getElementById('redrawYesButton') as HTMLButtonElement).addEventListener('click', redrawButton_click);
|
||||
function redrawButton_click(e: MouseEvent) {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open('POST', `http://localhost:3333/api/games/${currentGame!.id}/redraw`);
|
||||
let data = new URLSearchParams();
|
||||
data.append('clientToken', clientToken);
|
||||
data.append('redraw', (e.target as HTMLButtonElement).dataset.redraw!);
|
||||
req.send(data.toString());
|
||||
redrawModal.hidden = true;
|
||||
}
|
||||
|
||||
passButton.addEventListener('change', e => {
|
||||
board.autoHighlight = !passButton.checked;
|
||||
if (passButton.checked) {
|
||||
specialButton.checked = false;
|
||||
board.cardPlaying = null;
|
||||
board.specialAttack = false;
|
||||
for (const el of handButtons) {
|
||||
el.inputElement.disabled = false;
|
||||
el.inputElement.checked = false;
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
handButtons[i].inputElement.disabled = !canPlayCard[i];
|
||||
}
|
||||
}
|
||||
});
|
||||
specialButton.addEventListener('change', e => {
|
||||
board.specialAttack = specialButton.checked;
|
||||
if (specialButton.checked) {
|
||||
passButton.checked = false;
|
||||
board.autoHighlight = true;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
handButtons[i].inputElement.disabled = !canPlayCardAsSpecialAttack[i];
|
||||
if (!canPlayCardAsSpecialAttack[i])
|
||||
handButtons[i].inputElement.checked = false;
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
handButtons[i].inputElement.disabled = !canPlayCard[i];
|
||||
if (!canPlayCard[i])
|
||||
handButtons[i].inputElement.checked = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
board.onclick = (x, y) => {
|
||||
if (board.cardPlaying == null || !currentGame?.me)
|
||||
return;
|
||||
const message = board.checkMoveLegality(currentGame.me.playerIndex, board.cardPlaying, x, y, board.cardRotation, board.specialAttack);
|
||||
if (message != null) {
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
if (testMode) {
|
||||
for (let dy = 0; dy < 8; dy++) {
|
||||
for (let dx = 0; dx < 8; dx++) {
|
||||
let space = board.cardPlaying.getSpace(dx, dy, board.cardRotation);
|
||||
if (space != Space.Empty) {
|
||||
board.grid[x + dx][y + dy] = space;
|
||||
}
|
||||
}
|
||||
}
|
||||
board.refresh();
|
||||
} else if (canPlay) {
|
||||
canPlay = false;
|
||||
// Send the play to the server.
|
||||
let req = new XMLHttpRequest();
|
||||
req.open('POST', `http://localhost:3333/api/games/${currentGame.id}/play`);
|
||||
req.addEventListener('load', e => {
|
||||
if (req.status != 204) {
|
||||
alert(req.responseText);
|
||||
board.clearHighlight();
|
||||
board.autoHighlight = true;
|
||||
}
|
||||
});
|
||||
let data = new URLSearchParams();
|
||||
data.append('clientToken', clientToken);
|
||||
data.append('cardNumber', board.cardPlaying.number.toString());
|
||||
data.append('isSpecialAttack', specialButton.checked.toString());
|
||||
data.append('x', board.highlightX.toString());
|
||||
data.append('y', board.highlightY.toString());
|
||||
data.append('r', board.cardRotation.toString());
|
||||
req.send(data.toString());
|
||||
|
||||
board.autoHighlight = false;
|
||||
}
|
||||
};
|
||||
46
TableturfBattleClient/src/Pages/LobbyPage.ts
Normal file
46
TableturfBattleClient/src/Pages/LobbyPage.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
let cardButtons: CardButton[] = [ ];
|
||||
let submitDeckButton = document.getElementById('submitDeckButton') as HTMLButtonElement;
|
||||
|
||||
function clearReady() {
|
||||
if (currentGame == null) return;
|
||||
for (var i = 0; i < currentGame.players.length; i++) {
|
||||
const player = currentGame.players[i];
|
||||
if (player != null) {
|
||||
player.isReady = false;
|
||||
updatePlayerListItem(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updatePlayerListItem(playerIndex: number) {
|
||||
const player = currentGame != null ? currentGame.players[playerIndex] : null;
|
||||
const listItem = playerListItems[playerIndex];
|
||||
if (player != null) {
|
||||
listItem.innerText = player.name;
|
||||
if (player)
|
||||
listItem.innerText += ' (Ready)';
|
||||
} else
|
||||
listItem.innerText = "Waiting...";
|
||||
}
|
||||
|
||||
submitDeckButton.addEventListener('click', e => {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open('POST', `http://localhost:3333/api/games/${currentGame!.id}/chooseDeck`);
|
||||
req.addEventListener('load', e => {
|
||||
if (req.status == 204) {
|
||||
showSection('lobby');
|
||||
}
|
||||
});
|
||||
let data = new URLSearchParams();
|
||||
let cardsString = '';
|
||||
for (var el of cardButtons) {
|
||||
if (el.inputElement.checked) {
|
||||
if (cardsString != '') cardsString += '+';
|
||||
cardsString += el.card.number.toString();
|
||||
}
|
||||
}
|
||||
data.append('clientToken', clientToken);
|
||||
data.append('deckName', 'Deck');
|
||||
data.append('deckCards', cardsString);
|
||||
req.send(data.toString());
|
||||
});
|
||||
201
TableturfBattleClient/src/Pages/PreGamePage.ts
Normal file
201
TableturfBattleClient/src/Pages/PreGamePage.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
const newGameButton = document.getElementById('newGameButton')!;
|
||||
const joinGameButton = document.getElementById('joinGameButton')!;
|
||||
|
||||
newGameButton.addEventListener('click', e => {
|
||||
const name = (document.getElementById('nameBox') as HTMLInputElement).value;
|
||||
window.localStorage.setItem('name', name);
|
||||
|
||||
let request = new XMLHttpRequest();
|
||||
request.open('POST', 'http://localhost:3333/api/games/new');
|
||||
request.addEventListener('load', e => {
|
||||
if (request.status == 200) {
|
||||
let response = JSON.parse(request.responseText);
|
||||
if (!clientToken)
|
||||
setClientToken(response.clientToken);
|
||||
getGameInfo(response.gameID, 0);
|
||||
}
|
||||
});
|
||||
|
||||
let data = new URLSearchParams();
|
||||
data.append('name', name);
|
||||
data.append('clientToken', clientToken);
|
||||
request.send(data.toString());
|
||||
});
|
||||
|
||||
joinGameButton.addEventListener('click', e => {
|
||||
const name = (document.getElementById('nameBox') as HTMLInputElement).value;
|
||||
window.localStorage.setItem('name', name);
|
||||
tryJoinGame(name, (document.getElementById('gameIDBox') as HTMLInputElement).value);
|
||||
});
|
||||
|
||||
function tryJoinGame(name: string, idOrUrl: string) {
|
||||
const gameID = idOrUrl.substring(idOrUrl.lastIndexOf('#') + 1);
|
||||
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(gameID)) {
|
||||
alert("Invalid game ID or link");
|
||||
return;
|
||||
}
|
||||
|
||||
let request = new XMLHttpRequest();
|
||||
request.open('POST', `http://localhost:3333/api/games/${gameID}/join`);
|
||||
request.addEventListener('load', e => {
|
||||
if (request.status == 200) {
|
||||
let response = JSON.parse(request.responseText);
|
||||
if (!clientToken)
|
||||
setClientToken(response.clientToken);
|
||||
getGameInfo(gameID, response.playerIndex);
|
||||
} else if (request.status == 404) {
|
||||
alert("The game was not found.");
|
||||
window.location.hash = '#';
|
||||
} else if (request.status == 410) {
|
||||
alert("The game has already started.");
|
||||
window.location.hash = '#';
|
||||
}
|
||||
});
|
||||
let data = new URLSearchParams();
|
||||
data.append('name', name);
|
||||
data.append('clientToken', clientToken);
|
||||
request.send(data.toString());
|
||||
}
|
||||
|
||||
function getGameInfo(gameID: string, myPlayerIndex: number | null) {
|
||||
board.playerIndex = myPlayerIndex;
|
||||
window.location.hash = `#${gameID}`;
|
||||
|
||||
const webSocket = new WebSocket(`ws://localhost:3333/api/websocket?gameID=${gameID}&clientToken=${clientToken}`);
|
||||
webSocket.addEventListener('open', e => {
|
||||
const request2 = new XMLHttpRequest();
|
||||
request2.open('GET', `http://localhost:3333/api/games/${gameID}/playerData/${clientToken}`);
|
||||
request2.addEventListener('load', e => {
|
||||
if (request2.status == 200) {
|
||||
const response = JSON.parse(request2.responseText);
|
||||
const playerData = response.playerData as PlayerData | null;
|
||||
|
||||
currentGame = {
|
||||
id: gameID,
|
||||
me: playerData != null ? { playerIndex: playerData.playerIndex, hand: playerData.hand?.map(Card.fromJson) || null, deck: playerData.deck?.map(Card.fromJson) || null, cardsUsed: playerData.cardsUsed } : null,
|
||||
players: response.game.players,
|
||||
webSocket: webSocket
|
||||
};
|
||||
|
||||
for (const li of playerListItems)
|
||||
playerList.removeChild(li);
|
||||
playerListItems.splice(0);
|
||||
for (var player of currentGame.players) {
|
||||
var el = document.createElement('li');
|
||||
if (player)
|
||||
el.innerText = player.name;
|
||||
else
|
||||
el.innerText = "Waiting...";
|
||||
playerListItems.push(el);
|
||||
playerList.appendChild(el);
|
||||
}
|
||||
|
||||
onGameStateChange(response.game, playerData);
|
||||
|
||||
for (let i = 0; i < response.game.players.length; i++) {
|
||||
if (response.game.players[i]?.isReady)
|
||||
showReady(i);
|
||||
}
|
||||
|
||||
if (playerData) {
|
||||
myPlayerIndex = playerData.playerIndex;
|
||||
if (playerData.move) {
|
||||
canPlay = false;
|
||||
board.autoHighlight = false;
|
||||
if (!playerData.move.isPass) {
|
||||
const move = playerData.move as PlayMove;
|
||||
board.cardPlaying = Card.fromJson(move.card);
|
||||
board.highlightX = move.x;
|
||||
board.highlightY = move.y;
|
||||
board.cardRotation = move.rotation;
|
||||
board.specialAttack = move.isSpecialAttack;
|
||||
board.refreshHighlight();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
request2.send();
|
||||
});
|
||||
webSocket.addEventListener('message', e => {
|
||||
if (currentGame == null) return;
|
||||
let s = e.data as string;
|
||||
console.log(`>> ${s}`);
|
||||
if (s) {
|
||||
let payload = JSON.parse(s);
|
||||
if (payload.event == 'join') {
|
||||
currentGame.players[payload.data.playerIndex] = payload.data.player;
|
||||
playerListItems[payload.data.playerIndex].innerText = payload.data.player.name;
|
||||
updatePlayerListItem(payload.data.playerIndex);
|
||||
} else if (payload.event == 'playerReady') {
|
||||
currentGame.players[payload.data.playerIndex]!.isReady = true;
|
||||
updatePlayerListItem(payload.data.playerIndex);
|
||||
|
||||
if (playContainers[payload.data.playerIndex].getElementsByTagName('div').length == 0) {
|
||||
showReady(payload.data.playerIndex);
|
||||
}
|
||||
} else if (payload.event == 'stateChange') {
|
||||
clearReady();
|
||||
onGameStateChange(payload.data, payload.playerData);
|
||||
} else if (payload.event == 'turn' || payload.event == 'gameEnd') {
|
||||
clearReady();
|
||||
board.autoHighlight = false;
|
||||
showSection('game');
|
||||
|
||||
(async () => {
|
||||
let anySpecialAttacks = false;
|
||||
// Show the cards that were played.
|
||||
clearPlayContainers();
|
||||
for (let i = 0; i < currentGame.players.length; i++) {
|
||||
const player = currentGame.players[i];
|
||||
if (player != null) {
|
||||
player.specialPoints = payload.data.game.players[i].specialPoints;
|
||||
player.totalSpecialPoints = payload.data.game.players[i].totalSpecialPoints;
|
||||
player.passes = payload.data.game.players[i].passes;
|
||||
|
||||
const move = payload.data.moves[i];
|
||||
const button = new CardButton('checkbox', move.card);
|
||||
if (move.isSpecialAttack) {
|
||||
anySpecialAttacks = true;
|
||||
button.element.classList.add('specialAttack');
|
||||
} else if (move.isPass) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'passLabel';
|
||||
el.innerText = 'Pass';
|
||||
button.element.appendChild(el);
|
||||
}
|
||||
button.inputElement.hidden = true;
|
||||
playContainers[i].append(button.element);
|
||||
}
|
||||
}
|
||||
|
||||
await playInkAnimations(payload.data, anySpecialAttacks);
|
||||
updateHand(payload.playerData.hand);
|
||||
turnNumberLabel.setTurnNumber(payload.data.game.turnNumber);
|
||||
clearPlayContainers();
|
||||
if (payload.event == 'gameEnd') {
|
||||
document.getElementById('gameSection')!.classList.add('gameEnded');
|
||||
showResult();
|
||||
} else {
|
||||
canPlay = myPlayerIndex != null;
|
||||
board.autoHighlight = canPlay;
|
||||
setupControlsForPlay();
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
});
|
||||
webSocket.addEventListener('close', e => {
|
||||
document.getElementById('errorModal')!.hidden = false;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const name = window.localStorage.getItem('name');
|
||||
(document.getElementById('nameBox') as HTMLInputElement).value = name || '';
|
||||
if (window.location.hash != '') {
|
||||
(document.getElementById('gameIDBox') as HTMLInputElement).value = window.location.hash;
|
||||
if (name != null)
|
||||
tryJoinGame(name, window.location.hash)
|
||||
}
|
||||
}
|
||||
16
TableturfBattleClient/src/Player.ts
Normal file
16
TableturfBattleClient/src/Player.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
interface Player {
|
||||
name: string;
|
||||
specialPoints: number;
|
||||
isReady: boolean;
|
||||
colour: Colour;
|
||||
specialColour: Colour;
|
||||
specialAccentColour: Colour;
|
||||
totalSpecialPoints: number;
|
||||
passes: number;
|
||||
}
|
||||
|
||||
interface Colour {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
}
|
||||
88
TableturfBattleClient/src/PlayerBar.ts
Normal file
88
TableturfBattleClient/src/PlayerBar.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
class PlayerBar {
|
||||
readonly playerIndex: number;
|
||||
|
||||
private nameElement: HTMLElement;
|
||||
private specialPointsContainer: HTMLElement;
|
||||
private pointsElement: HTMLElement;
|
||||
private pointsContainer: HTMLElement;
|
||||
private pointsToContainer: HTMLElement;
|
||||
private pointsToElement: HTMLElement;
|
||||
private pointsDeltaElement: HTMLElement;
|
||||
|
||||
resultElement: HTMLElement;
|
||||
statSpecialPointsElement: HTMLElement;
|
||||
statPassesElement: HTMLElement;
|
||||
|
||||
constructor(element: HTMLDivElement) {
|
||||
this.playerIndex = parseInt(element.dataset.index!);
|
||||
if (isNaN(this.playerIndex))
|
||||
throw new Error('Missing player index');
|
||||
this.nameElement = element.getElementsByClassName('name')[0] as HTMLElement;
|
||||
this.specialPointsContainer = element.getElementsByClassName('specialPoints')[0] as HTMLElement;
|
||||
|
||||
let pointsContainer: HTMLElement | null = null;
|
||||
for (const el of document.getElementsByClassName('pointsContainer')) {
|
||||
if ((el as HTMLElement).dataset.index == element.dataset.index) {
|
||||
pointsContainer = el as HTMLElement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pointsContainer == null)
|
||||
throw new Error(`pointsContainer with index ${element.dataset.index} not found`);
|
||||
this.pointsContainer = pointsContainer;
|
||||
|
||||
this.pointsElement = pointsContainer.getElementsByClassName('points')[0] as HTMLElement;
|
||||
this.pointsToContainer = pointsContainer.getElementsByClassName('pointsToContainer')[0] as HTMLElement;
|
||||
this.pointsToElement = pointsContainer.getElementsByClassName('pointsTo')[0] as HTMLElement;
|
||||
this.pointsDeltaElement = pointsContainer.getElementsByClassName('pointsDelta')[0] as HTMLElement;
|
||||
|
||||
this.resultElement = element.getElementsByClassName('result')[0] as HTMLElement;
|
||||
this.statSpecialPointsElement = element.querySelector('.statSpecialPoints .statValue')!;
|
||||
this.statPassesElement = element.querySelector('.statPasses .statValue')!;
|
||||
}
|
||||
|
||||
get name() { return this.nameElement.innerText; }
|
||||
set name(value: string) { this.nameElement.innerText = value; }
|
||||
|
||||
get points() { return parseInt(this.pointsElement.innerText); }
|
||||
set points(value: number) { this.pointsElement.innerText = value.toString(); }
|
||||
|
||||
get specialPoints() { return this.specialPointsContainer.getElementsByClassName('specialPoint').length; }
|
||||
set specialPoints(value: number) {
|
||||
const oldList = this.specialPointsContainer.getElementsByClassName('specialPoint');
|
||||
if (value < oldList.length) {
|
||||
for (let i = oldList.length - 1; i >= value; i--)
|
||||
this.specialPointsContainer.removeChild(oldList[i]);
|
||||
} else if (value > oldList.length) {
|
||||
for (let i = oldList.length; i < value; i++) {
|
||||
const el = document.createElement('div');
|
||||
el.classList.add('specialPoint');
|
||||
el.innerText = `${i + 1}`;
|
||||
this.specialPointsContainer.appendChild(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get pointsTo() { return this.pointsToContainer.hidden ? null : parseInt(this.pointsToElement.innerText); }
|
||||
set pointsTo(value: number | null) {
|
||||
if (value == null || value == 0) // A player can never actually have 0 points because they start with a permanent special space.
|
||||
this.pointsToContainer.hidden = true;
|
||||
else {
|
||||
this.pointsToContainer.hidden = false;
|
||||
this.pointsToElement.innerText = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
get pointsDelta() { return this.pointsDeltaElement.hidden ? null : parseInt(this.pointsDeltaElement.innerText); }
|
||||
set pointsDelta(value: number | null) {
|
||||
if (value == null || value == 0)
|
||||
this.pointsDeltaElement.hidden = true;
|
||||
else {
|
||||
this.pointsDeltaElement.hidden = false;
|
||||
if (value > 0)
|
||||
this.pointsDeltaElement.innerText = `+${value}`;
|
||||
else
|
||||
this.pointsDeltaElement.innerText = value.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
TableturfBattleClient/src/PlayerData.ts
Normal file
7
TableturfBattleClient/src/PlayerData.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
interface PlayerData {
|
||||
playerIndex: number;
|
||||
hand: Card[] | null;
|
||||
deck: Card[] | null;
|
||||
cardsUsed: number[];
|
||||
move: Move | null;
|
||||
}
|
||||
5
TableturfBattleClient/src/Rarity.ts
Normal file
5
TableturfBattleClient/src/Rarity.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
enum Rarity {
|
||||
Common,
|
||||
Rare,
|
||||
Fresh
|
||||
}
|
||||
17
TableturfBattleClient/src/Space.ts
Normal file
17
TableturfBattleClient/src/Space.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
enum Space {
|
||||
Empty = 0,
|
||||
Wall = 1,
|
||||
OutOfBounds = 2,
|
||||
Ink1 = 4,
|
||||
Ink2 = 5,
|
||||
Ink3 = 6,
|
||||
Ink4 = 7,
|
||||
SpecialInactive1 = 8,
|
||||
SpecialInactive2 = 9,
|
||||
SpecialInactive3 = 10,
|
||||
SpecialInactive4 = 11,
|
||||
SpecialActive1 = 12,
|
||||
SpecialActive2 = 13,
|
||||
SpecialActive3 = 14,
|
||||
SpecialActive4 = 15
|
||||
}
|
||||
22
TableturfBattleClient/src/TurnNumberLabel.ts
Normal file
22
TableturfBattleClient/src/TurnNumberLabel.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
class TurnNumberLabel {
|
||||
containerElement: HTMLElement;
|
||||
element: HTMLElement;
|
||||
|
||||
constructor(conatinerElement: HTMLElement, element: HTMLElement) {
|
||||
this.containerElement = conatinerElement;
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
setTurnNumber(value: number | null) {
|
||||
if (value == null)
|
||||
this.containerElement.hidden = true;
|
||||
else {
|
||||
this.containerElement.hidden = false;
|
||||
this.element.innerText = (13 - value).toString();
|
||||
if (value >= 10)
|
||||
this.element.classList.add('nowOrNever');
|
||||
else
|
||||
this.element.classList.remove('nowOrNever');
|
||||
}
|
||||
}
|
||||
}
|
||||
1
TableturfBattleClient/src/_header.ts
Normal file
1
TableturfBattleClient/src/_header.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
// This file was generated using TypeScript.
|
||||
103
TableturfBattleClient/src/app.ts
Normal file
103
TableturfBattleClient/src/app.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/// <reference path="CardDatabase.ts"/>
|
||||
|
||||
function delay(ms: number) { return new Promise(resolve => setTimeout(() => resolve(null), ms)); }
|
||||
|
||||
// Sections
|
||||
const sections = new Map<string, HTMLDivElement>();
|
||||
for (var id of [ 'noJS', 'loading', 'preGame', 'lobby', 'deck', 'game' ]) {
|
||||
let el = document.getElementById(`${id}Section`) as HTMLDivElement;
|
||||
if (!el) throw new EvalError(`Element not found: ${id}Section`);
|
||||
sections.set(id, el);
|
||||
}
|
||||
|
||||
function showSection(key: string) {
|
||||
for (const [key2, el] of sections) {
|
||||
el.hidden = key2 != key;
|
||||
}
|
||||
}
|
||||
|
||||
function setClientToken(token: string) {
|
||||
window.localStorage.setItem('clientToken', token);
|
||||
clientToken = token;
|
||||
}
|
||||
|
||||
function onGameStateChange(game: any, playerData: any) {
|
||||
if (currentGame == null)
|
||||
throw new Error('currentGame is null');
|
||||
clearPlayContainers();
|
||||
board.resize(game.board);
|
||||
board.refresh();
|
||||
loadPlayers(game.players);
|
||||
redrawModal.hidden = true;
|
||||
document.getElementById('gameSection')!.classList.remove('gameEnded');
|
||||
switch (game.state) {
|
||||
case GameState.WaitingForPlayers:
|
||||
showSection('lobby');
|
||||
break;
|
||||
case GameState.Preparing:
|
||||
showSection('deck');
|
||||
for (const button of cardButtons.slice(0, 15)) {
|
||||
button.inputElement.checked = true;
|
||||
}
|
||||
submitDeckButton.disabled = false;
|
||||
break;
|
||||
case GameState.Redraw:
|
||||
case GameState.Ongoing:
|
||||
case GameState.Ended:
|
||||
if (playerData)
|
||||
updateHand(playerData.hand);
|
||||
board.autoHighlight = false;
|
||||
redrawModal.hidden = true;
|
||||
showSection('game');
|
||||
|
||||
switch (game.state) {
|
||||
case GameState.Redraw:
|
||||
redrawModal.hidden = false;
|
||||
turnNumberLabel.setTurnNumber(null);
|
||||
canPlay = false;
|
||||
break;
|
||||
case GameState.Ongoing:
|
||||
turnNumberLabel.setTurnNumber(game.turnNumber);
|
||||
board.autoHighlight = true;
|
||||
canPlay = currentGame.me != null && !currentGame.players[currentGame.me.playerIndex]?.isReady;
|
||||
setupControlsForPlay();
|
||||
break;
|
||||
case GameState.Ended:
|
||||
document.getElementById('gameSection')!.classList.add('gameEnded');
|
||||
turnNumberLabel.setTurnNumber(null);
|
||||
canPlay = false;
|
||||
showResult();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
showSection('preGame');
|
||||
|
||||
cardDatabase.loadAsync().then(cards => {
|
||||
const cardList = document.getElementById('cardList')!;
|
||||
for (var card of cards) {
|
||||
const button = new CardButton('checkbox', card);
|
||||
cardButtons.push(button);
|
||||
button.inputElement.addEventListener('input', e => {
|
||||
var count = 0;
|
||||
for (var el of cardButtons) {
|
||||
if (el.inputElement.checked)
|
||||
count++;
|
||||
}
|
||||
document.getElementById('countLabel')!.innerText = count.toString();
|
||||
submitDeckButton.disabled = (count != 15);
|
||||
});
|
||||
cardList.appendChild(button.element);
|
||||
}
|
||||
document.getElementById('cardListLoadingSection')!.hidden = true;
|
||||
}).catch(e => document.getElementById('errorModal')!.hidden = false);
|
||||
|
||||
function isInternetExplorer() {
|
||||
return !!(window.document as any).documentMode; // This is a non-standard property implemented only by Internet Explorer.
|
||||
}
|
||||
|
||||
if (isInternetExplorer()) {
|
||||
alert("You seem to be using an unsupported browser. Some layout or features of this app may not work correctly.");
|
||||
}
|
||||
519
TableturfBattleClient/tableturf.css
Normal file
519
TableturfBattleClient/tableturf.css
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
/* Fonts */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Splatoon 1';
|
||||
src: url('assets/splatoon1.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Splatoon 2';
|
||||
src: url('assets/splatoon2.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* Body */
|
||||
|
||||
body {
|
||||
color: white;
|
||||
background: black;
|
||||
font-family: 'Splatoon 2', sans-serif;
|
||||
/* Default colours - normally the game data sent from the server will override these */
|
||||
--primary-colour-1 : hsl(63, 99%, 49%);
|
||||
--special-colour-1 : hsl(38, 100%, 49%);
|
||||
--special-accent-colour-1: hsl(60, 95%, 55%);
|
||||
--primary-colour-2 : hsl(234, 97%, 64%);
|
||||
--special-colour-2 : hsl(184, 99%, 50%);
|
||||
--special-accent-colour-2: hsl(180, 17%, 86%);
|
||||
--primary-colour-3 : hsl(306, 95%, 50%);
|
||||
--special-colour-3 : hsl(270, 95%, 50%);
|
||||
--special-accent-colour-3: hsl(285, 95%, 85%);
|
||||
--primary-colour-4 : hsl(155, 95%, 50%);
|
||||
--special-colour-4 : hsl(120, 95%, 50%);
|
||||
--special-accent-colour-4: hsl(135, 95%, 85%);
|
||||
}
|
||||
|
||||
/* Start section */
|
||||
|
||||
#preGameSection {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#logo {
|
||||
height: 8em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Splatoon 1', sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
footer {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
|
||||
.cardBack {
|
||||
width: 10rem;
|
||||
height: 13rem;
|
||||
border: 1px solid grey;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.card {
|
||||
font-family: 'Splatoon 1', 'Arial Black', sans-serif;
|
||||
font-weight: 599;
|
||||
display: inline-block;
|
||||
border: 1px solid;
|
||||
border-radius: 0.5em;
|
||||
width: 10em;
|
||||
margin: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card.common {
|
||||
border-color: #5931FF;
|
||||
}
|
||||
|
||||
.card.rare {
|
||||
border-color: yellow;
|
||||
}
|
||||
|
||||
.card.fresh {
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
height: 2.5em;
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card input {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.cardNumber {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.cardName {
|
||||
text-align: center;
|
||||
line-height: 1.25em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.card.common .cardName {
|
||||
color: rgb(89, 49, 255);
|
||||
}
|
||||
.card.rare .cardName {
|
||||
background: yellow;
|
||||
background: -webkit-linear-gradient(0, rgb(255, 242, 129) 0%, rgb(255, 255, 224) 15%, rgb(231, 180, 39) 50%, rgb(255, 255, 224) 85%, rgb(255, 242, 129) 100%);
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
.card.fresh .cardName {
|
||||
background: white;
|
||||
background: -webkit-linear-gradient(-30deg, rgba(253, 217, 169, 1) 0%, rgba(200, 58, 141, 1) 50%, rgba(55, 233, 207, 1) 100%);
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.cardGrid {
|
||||
margin: 0 auto;
|
||||
}
|
||||
.cardGrid td {
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
.cardGrid td.ink { background: hsl(63, 99%, 49%); }
|
||||
.cardGrid td.special { background: hsl(38, 100%, 49%); }
|
||||
|
||||
.cardFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.5em;
|
||||
}
|
||||
|
||||
.cardSize {
|
||||
display: inline-block;
|
||||
width: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cardSpecialCost {
|
||||
display: inline-flex;
|
||||
width: 6em;
|
||||
flex-wrap: wrap-reverse;
|
||||
row-gap: 4px;
|
||||
}
|
||||
|
||||
.cardSpecialPoint {
|
||||
display: inline-block;
|
||||
color: transparent;
|
||||
background: var(--special-colour-1);
|
||||
width: 1ch;
|
||||
height: 1ch;
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
|
||||
/* Board */
|
||||
|
||||
#gameBoard {
|
||||
height: 98vh;
|
||||
aspect-ratio: var(--board-width)/var(--board-height);
|
||||
table-layout: fixed;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
#gameBoard td { border: 1px solid grey; }
|
||||
#gameBoard td.Wall { background: grey; }
|
||||
#gameBoard td.Ink1 { background: var(--primary-colour-1); }
|
||||
#gameBoard td.Ink2 { background: var(--primary-colour-2); }
|
||||
#gameBoard td.SpecialInactive1 { background: var(--special-colour-1); }
|
||||
#gameBoard td.SpecialInactive2 { background: var(--special-colour-2); }
|
||||
#gameBoard td.SpecialActive1 { background: radial-gradient(circle, var(--special-accent-colour-1) 25%, var(--special-colour-1) 75%); }
|
||||
#gameBoard td.SpecialActive2 { background: radial-gradient(circle, var(--special-accent-colour-2) 25%, var(--special-colour-2) 75%); }
|
||||
|
||||
#gameBoard td.hover {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#gameBoard td.hover::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#gameBoard td.hover1:not(.hoverillegal)::after { background: var(--primary-colour-1); }
|
||||
#gameBoard td.hover2:not(.hoverillegal)::after { background: var(--primary-colour-2); }
|
||||
#gameBoard td.hover3:not(.hoverillegal)::after { background: var(--primary-colour-3); }
|
||||
#gameBoard td.hover4:not(.hoverillegal)::after { background: var(--primary-colour-4); }
|
||||
#gameBoard td.hoverspecial.hover1:not(.hoverillegal)::after { background: var(--special-colour-1); }
|
||||
#gameBoard td.hoverspecial.hover1:not(.hoverillegal)::after { background: var(--special-colour-2); }
|
||||
#gameBoard td.hoverspecial.hover1:not(.hoverillegal)::after { background: var(--special-colour-3); }
|
||||
#gameBoard td.hoverspecial.hover1:not(.hoverillegal)::after { background: var(--special-colour-4); }
|
||||
|
||||
#gameBoard td.hoverillegal::after { background: grey; }
|
||||
|
||||
/* Card list */
|
||||
|
||||
#cardList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Game UI */
|
||||
|
||||
#gameSection:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#sidebarSection {
|
||||
width: 24em;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.playerBar {
|
||||
border-left: 4px solid var(--colour);
|
||||
padding-left: 10px;
|
||||
}
|
||||
.playerBar[data-index="0"] { --colour: var(--primary-colour-1); --special-colour: var(--special-colour-1); }
|
||||
.playerBar[data-index="1"] { --colour: var(--primary-colour-2); --special-colour: var(--special-colour-2); }
|
||||
.playerBar[data-index="2"] { --colour: var(--primary-colour-3); --special-colour: var(--special-colour-3); }
|
||||
.playerBar[data-index="3"] { --colour: var(--primary-colour-4); --special-colour: var(--special-colour-4); }
|
||||
|
||||
.name {
|
||||
font-size: x-large;
|
||||
font-weight: bold;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.specialPoints div {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border: 1px solid;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.playerBar .specialPoint {
|
||||
color: transparent;
|
||||
background: var(--special-colour);
|
||||
}
|
||||
|
||||
.specialPoints div {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.specialPoint:nth-of-type(5n) {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.result {
|
||||
font-family: 'Splatoon 1';
|
||||
color: var(--colour);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.result.win {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.playerStats {
|
||||
margin: 0 1em;
|
||||
background: rgba(64, 64, 64);
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
text-align: center;
|
||||
}
|
||||
#gameSection:not(.gameEnded) .playerStats {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-family: 'Splatoon 1';
|
||||
font-size: x-large;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
#midGameContainer {
|
||||
height: 28em;
|
||||
}
|
||||
|
||||
#resultContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Score bar */
|
||||
|
||||
#scoreSection {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 2fr 1fr;
|
||||
place-items: center;
|
||||
margin: 0 2em;
|
||||
}
|
||||
|
||||
#turnNumberContainer {
|
||||
width: 5em;
|
||||
height: 5em;
|
||||
background: radial-gradient(circle, rgb(128, 128, 128) 0%, rgb(128, 128, 128) 70%, rgba(0, 0, 0, 0) 70%);
|
||||
font-family: 'Splatoon 1', sans-serif;
|
||||
}
|
||||
|
||||
#turnNumberContainer p {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
#turnNumberLabel {
|
||||
text-align: center;
|
||||
font-size: x-large;
|
||||
}
|
||||
#turnNumberLabel.nowOrNever {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#pointsContainers {
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.pointsContainer {
|
||||
width: 5em;
|
||||
height: 5em;
|
||||
}
|
||||
|
||||
.pointsContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
font-family: 'Splatoon 1', sans-serif;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.points {
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
.pointsToContainer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.pointsContainer[data-index="1"] {
|
||||
--colour: var(--primary-colour-2);
|
||||
}
|
||||
|
||||
.pointsContainer::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.5;
|
||||
z-index: -1;
|
||||
background: radial-gradient(circle, var(--colour) 0%, var(--colour) 70%, rgba(0, 0, 0, 0) 70%);
|
||||
}
|
||||
|
||||
.pointsContainer[data-index="0"] {
|
||||
--colour: var(--primary-colour-1);
|
||||
}
|
||||
|
||||
.pointsContainer {
|
||||
color: var(--colour);
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.pointsDelta {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
/* Board section */
|
||||
|
||||
#boardSection {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#redrawModal {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #000000C0;
|
||||
}
|
||||
|
||||
#redrawModel:not([hidden]) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
#redrawBox {
|
||||
/* display: flex; */
|
||||
border: 1px solid grey;
|
||||
width: 15em;
|
||||
height: 10em;
|
||||
background: black;
|
||||
text-align: center;
|
||||
margin-top: 12em;
|
||||
}
|
||||
|
||||
#playsSection {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Error UI */
|
||||
|
||||
#errorModal {
|
||||
background: #00000080;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#errorModal:not([hidden]) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#errorModalBox {
|
||||
width: 20em;
|
||||
background: black;
|
||||
height: 12em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
||||
|
||||
.specialAttack, .specialAttack::after {
|
||||
box-shadow: 0px 0 8px 4px var(--colour);
|
||||
}
|
||||
|
||||
.specialAttack::after {
|
||||
content: 'Special Attack!';
|
||||
position: absolute;
|
||||
left: -2em;
|
||||
top: 5em;
|
||||
right: -2em;
|
||||
height: 2em;
|
||||
background: var(--colour);
|
||||
color: black;
|
||||
text-align: center;
|
||||
font-size: large;
|
||||
border-radius: 0.5em;
|
||||
animation: 3s forwards specialAttackAnimation;
|
||||
}
|
||||
|
||||
@keyframes specialAttackAnimation {
|
||||
from {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
15% {
|
||||
transform: scale(0.9);
|
||||
opacity: 1;
|
||||
}
|
||||
85% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: scale(1.1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.playContainer[data-index="0"] { --colour: var(--primary-colour-1); }
|
||||
.playContainer[data-index="1"] { --colour: var(--primary-colour-2); }
|
||||
.playContainer[data-index="2"] { --colour: var(--primary-colour-3); }
|
||||
.playContainer[data-index="3"] { --colour: var(--primary-colour-4); }
|
||||
|
||||
.passLabel {
|
||||
font-family: 'Splatoon 2', sans-serif;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
background: #00000080;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Score section */
|
||||
14
TableturfBattleClient/tsconfig.json
Normal file
14
TableturfBattleClient/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"files": [ "src/_header.ts" ],
|
||||
"include": [ "src/**/*.ts" ],
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"outFile": "build/tsbuild.js",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
|
||||
}
|
||||
}
|
||||
26
TableturfBattleServer/.vscode/launch.json
vendored
Normal file
26
TableturfBattleServer/.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
||||
"name": ".NET Core Launch (console)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/bin/Debug/net6.0/TableturfBattleServer.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
TableturfBattleServer/.vscode/tasks.json
vendored
Normal file
41
TableturfBattleServer/.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/TableturfBattleServer.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/TableturfBattleServer.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/TableturfBattleServer.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
62
TableturfBattleServer/Card.cs
Normal file
62
TableturfBattleServer/Card.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public class Card {
|
||||
[JsonProperty("number")]
|
||||
public int Number { get; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; }
|
||||
[JsonProperty("rarity")]
|
||||
public Rarity Rarity { get; }
|
||||
[JsonProperty("specialCost")]
|
||||
public int SpecialCost { get; }
|
||||
[JsonIgnore]
|
||||
public int Size { get; }
|
||||
|
||||
[JsonProperty("grid")]
|
||||
private readonly Space[,] grid;
|
||||
|
||||
internal Card(int number, string name, Rarity rarity, int specialCost, Space[,] grid) {
|
||||
this.Number = number;
|
||||
this.Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
this.Rarity = rarity;
|
||||
this.SpecialCost = specialCost;
|
||||
this.grid = grid ?? throw new ArgumentNullException(nameof(grid));
|
||||
|
||||
var size = 0;
|
||||
if (grid.GetUpperBound(0) != 7 || grid.GetUpperBound(1) != 7)
|
||||
throw new ArgumentException("Grid must be 8 × 8.", nameof(grid));
|
||||
for (int y = 0; y < 8; y++) {
|
||||
for (int x = 0; x < 8; x++) {
|
||||
switch (grid[x, y]) {
|
||||
case Space.Empty:
|
||||
break;
|
||||
case Space.Ink1:
|
||||
case Space.SpecialInactive1:
|
||||
size++;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Grid contains invalid values.", nameof(grid));
|
||||
}
|
||||
}
|
||||
}
|
||||
this.Size = size;
|
||||
}
|
||||
|
||||
/// <summary>Returns the space in the specified position on the card grid when rotated in the specified manner.</summary>
|
||||
/// <param name="x">The number of spaces right from the top left corner.</param>
|
||||
/// <param name="y">The number of spaces down from the top left corner.</param>
|
||||
/// <param name="rotation">The number of clockwise rotations.</param>
|
||||
public Space GetSpace(int x, int y, int rotation) {
|
||||
if (x is < 0 or >= 8)
|
||||
throw new ArgumentOutOfRangeException(nameof(x));
|
||||
if (y is < 0 or >= 8)
|
||||
throw new ArgumentOutOfRangeException(nameof(y));
|
||||
return (rotation & 3) switch {
|
||||
0 => this.grid[x, y],
|
||||
1 => this.grid[y, 7 - x],
|
||||
2 => this.grid[7 - x, 7 - y],
|
||||
_ => this.grid[7 - y, x],
|
||||
};
|
||||
}
|
||||
}
|
||||
1646
TableturfBattleServer/CardDatabase.cs
Normal file
1646
TableturfBattleServer/CardDatabase.cs
Normal file
File diff suppressed because it is too large
Load Diff
17
TableturfBattleServer/Colour.cs
Normal file
17
TableturfBattleServer/Colour.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public struct Colour {
|
||||
[JsonProperty("r")]
|
||||
public int R;
|
||||
[JsonProperty("g")]
|
||||
public int G;
|
||||
[JsonProperty("b")]
|
||||
public int B;
|
||||
|
||||
public Colour(int r, int g, int b) {
|
||||
this.R = r;
|
||||
this.G = g;
|
||||
this.B = b;
|
||||
}
|
||||
}
|
||||
20
TableturfBattleServer/Error.cs
Normal file
20
TableturfBattleServer/Error.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public struct Error {
|
||||
[JsonProperty("code")]
|
||||
public string Code { get; }
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; }
|
||||
|
||||
public Error(string code, string description) {
|
||||
this.Code = code ?? throw new ArgumentNullException(nameof(code));
|
||||
this.Description = description ?? throw new ArgumentNullException(nameof(description));
|
||||
}
|
||||
}
|
||||
272
TableturfBattleServer/Game.cs
Normal file
272
TableturfBattleServer/Game.cs
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Drawing;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public class Game {
|
||||
[JsonIgnore]
|
||||
public Guid ID { get; } = Guid.NewGuid();
|
||||
|
||||
[JsonProperty("state")]
|
||||
public GameState State { get; set; }
|
||||
[JsonProperty("turnNumber")]
|
||||
public int TurnNumber { get; set; }
|
||||
[JsonProperty("players")]
|
||||
public List<Player> Players { get; } = new(4);
|
||||
[JsonProperty("board")]
|
||||
public Space[,] Board { get; }
|
||||
|
||||
public Game(int boardWidth, int boardHeight) => this.Board = new Space[boardWidth, boardHeight];
|
||||
|
||||
public bool TryAddPlayer(Player player, out int playerIndex, out Error error) {
|
||||
lock (this.Players) {
|
||||
if (this.State != GameState.WaitingForPlayers) {
|
||||
playerIndex = -1;
|
||||
error = new("GameAlreadyStarted", "The game has already started.");
|
||||
return false;
|
||||
}
|
||||
if (this.Players.Any(p => p.Token == player.Token)) {
|
||||
playerIndex = -1;
|
||||
error = new("PlayerAlreadyJoined", "You're already in the game.");
|
||||
return false;
|
||||
}
|
||||
if (this.Players.Count >= 4) {
|
||||
playerIndex = -1;
|
||||
error = new("GameFull", "The game is full.");
|
||||
return false;
|
||||
}
|
||||
playerIndex = this.Players.Count;
|
||||
this.Players.Add(player);
|
||||
error = default;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool GetPlayer(Guid clientToken, [MaybeNullWhen(false)] out int playerIndex, [MaybeNullWhen(false)] out Player player) {
|
||||
for (var i = 0; i < this.Players.Count; i++) {
|
||||
var player2 = this.Players[i];
|
||||
if (player2.Token == clientToken) {
|
||||
playerIndex = i;
|
||||
player = player2;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
playerIndex = -1;
|
||||
player = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CanPlay(int playerIndex, Card card, int x, int y, int rotation, bool isSpecialAttack) {
|
||||
if (card is null) throw new ArgumentNullException(nameof(card));
|
||||
|
||||
if (isSpecialAttack && (this.Players[playerIndex].SpecialPoints < card.SpecialCost))
|
||||
return false;
|
||||
|
||||
var isAnchored = false;
|
||||
for (int dx = 0; dx < 8; dx++) {
|
||||
for (int dy = 0; dy < 8; dy++) {
|
||||
if (card.GetSpace(dx, dy, rotation) == Space.Empty)
|
||||
continue;
|
||||
var x2 = x + dx;
|
||||
var y2 = y + dy;
|
||||
if (x2 < 0 || x2 > this.Board.GetUpperBound(0)
|
||||
|| y2 < 0 || y2 > this.Board.GetUpperBound(1))
|
||||
return false; // Out of bounds.
|
||||
switch (this.Board[x2, y2]) {
|
||||
case Space.Wall: case Space.OutOfBounds:
|
||||
return false;
|
||||
case >= Space.SpecialInactive1:
|
||||
return false; // Can't overlap special spaces ever.
|
||||
case Space.Empty:
|
||||
break;
|
||||
default:
|
||||
if (!isSpecialAttack) return false; // Can't overlap ink except with a special attack.
|
||||
break;
|
||||
}
|
||||
if (!isAnchored) {
|
||||
// A normal play must be adjacent to ink of the player's colour.
|
||||
// A special attack must be adjacent to a special space of theirs.
|
||||
for (int dy2 = -1; dy2 <= 1; dy2++) {
|
||||
for (int dx2 = -1; dx2 <= 1; dx2++) {
|
||||
if (dx2 == 0 && dy2 == 0) continue;
|
||||
var x3 = x2 + dx2;
|
||||
var y3 = y2 + dy2;
|
||||
if (x3 < 0 || x3 > this.Board.GetUpperBound(0)
|
||||
|| y3 < 0 || y3 > this.Board.GetUpperBound(1))
|
||||
continue;
|
||||
if (this.Board[x3, y3] >= (isSpecialAttack ? Space.SpecialInactive1 : Space.Ink1)
|
||||
&& (((int) this.Board[x3, y3]) & 3) == playerIndex) {
|
||||
isAnchored = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return isAnchored;
|
||||
}
|
||||
|
||||
internal void Tick() {
|
||||
if (this.State == GameState.WaitingForPlayers && this.Players.Count >= 2) {
|
||||
this.Players[0].Colour = new Colour(236, 249, 1);
|
||||
this.Players[0].SpecialColour = new Colour(250, 158, 0);
|
||||
this.Players[0].SpecialAccentColour = new Colour(249, 249, 31);
|
||||
this.Players[1].Colour = new Colour(74, 92, 252);
|
||||
this.Players[1].SpecialColour = new Colour(1, 237, 254);
|
||||
this.Players[1].SpecialAccentColour = new Colour(213, 225, 225);
|
||||
this.State = GameState.Preparing;
|
||||
this.SendEvent("stateChange", this, true);
|
||||
} else if (this.State == GameState.Preparing && this.Players.All(p => p.Deck != null)) {
|
||||
// Draw cards.
|
||||
var random = new Random();
|
||||
foreach (var player in this.Players)
|
||||
player.Shuffle(random);
|
||||
|
||||
this.State = GameState.Redraw;
|
||||
this.SendEvent("stateChange", this, true);
|
||||
} else if (this.State == GameState.Redraw && this.Players.All(p => p.Move != null)) {
|
||||
var random = new Random();
|
||||
foreach (var player in this.Players) {
|
||||
if (player.Move!.IsSpecialAttack) {
|
||||
player.Shuffle(random);
|
||||
}
|
||||
player.Move = null;
|
||||
}
|
||||
|
||||
this.State = GameState.Ongoing;
|
||||
this.TurnNumber = 1;
|
||||
this.SendEvent("stateChange", this, true);
|
||||
} else if (this.State == GameState.Ongoing && this.Players.All(p => p.Move != null)) {
|
||||
var moves = new Move?[this.Players.Count];
|
||||
var placements = new List<Placement>();
|
||||
var specialSpacesActivated = new List<Point>();
|
||||
|
||||
// Place the ink.
|
||||
(Placement placement, int cardSize)? placementData = null;
|
||||
foreach (var i in Enumerable.Range(0, this.Players.Count).Where(i => this.Players[i] != null).OrderByDescending(i => this.Players[i]!.Move!.Card.Size)) {
|
||||
var player = this.Players[i];
|
||||
var move = player.Move!;
|
||||
moves[i] = move;
|
||||
player.CardsUsed.Add(move.Card.Number);
|
||||
|
||||
if (move.IsPass) {
|
||||
player.Passes++;
|
||||
player.SpecialPoints++;
|
||||
player.TotalSpecialPoints++;
|
||||
} else {
|
||||
if (move.IsSpecialAttack)
|
||||
player.SpecialPoints -= move.Card.SpecialCost;
|
||||
if (placementData == null || move.Card.Size != placementData.Value.cardSize) {
|
||||
if (placementData != null)
|
||||
placements.Add(placementData.Value.placement);
|
||||
placementData = (new(), move.Card.Size);
|
||||
}
|
||||
var placement = placementData.Value.placement;
|
||||
placement.Players.Add(i);
|
||||
for (int dy = 0; dy < 8; dy++) {
|
||||
var y = move.Y + dy;
|
||||
for (int dx = 0; dx < 8; dx++) {
|
||||
var x = move.X + dx;
|
||||
var point = new Point(x, y);
|
||||
switch (move.Card.GetSpace(dx, dy, move.Rotation)) {
|
||||
case Space.Ink1:
|
||||
if (placement.SpacesAffected.TryGetValue(point, out var space)) {
|
||||
if (space < Space.SpecialInactive1) {
|
||||
// Two ink spaces overlapped; create a wall there.
|
||||
this.Board[x, y] = placement.SpacesAffected[point] = Space.Wall;
|
||||
}
|
||||
} else {
|
||||
if (this.Board[x, y] < Space.SpecialInactive1) // Ink spaces can't overlap special spaces from larger cards.
|
||||
this.Board[x, y] = placement.SpacesAffected[point] = Space.Ink1 | (Space) i;
|
||||
}
|
||||
break;
|
||||
case Space.SpecialInactive1:
|
||||
if (placement.SpacesAffected.TryGetValue(point, out space) && space >= Space.SpecialInactive1) {
|
||||
// Two special spaces overlapped; create a wall there.
|
||||
this.Board[x, y] = placement.SpacesAffected[point] = Space.Wall;
|
||||
} else {
|
||||
// If a special space overlaps an ink space, overwrite it.
|
||||
this.Board[x, y] = placement.SpacesAffected[point] = Space.SpecialInactive1 | (Space) i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (placementData != null)
|
||||
placements.Add(placementData.Value.placement);
|
||||
|
||||
// Activate special spaces.
|
||||
for (int y = 0; y < this.Board.GetLength(1); y++) {
|
||||
for (int x = 0; x < this.Board.GetLength(0); x++) {
|
||||
if ((this.Board[x, y] & Space.SpecialActive1) == Space.SpecialInactive1) {
|
||||
var anyEmptySpace = false;
|
||||
for (int dy = -1; !anyEmptySpace && dy <= 1; dy++) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
var x2 = x + dx;
|
||||
var y2 = y + dy;
|
||||
if (x2 >= 0 && x2 < this.Board.GetLength(0) && y2 >= 0 && y2 < this.Board.GetLength(1)
|
||||
&& this.Board[x2, y2] == Space.Empty) {
|
||||
anyEmptySpace = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!anyEmptySpace) {
|
||||
var player = this.Players[(int) this.Board[x, y] & 3]!;
|
||||
this.Board[x, y] |= Space.SpecialActive1;
|
||||
player.SpecialPoints++;
|
||||
player.TotalSpecialPoints++;
|
||||
specialSpacesActivated.Add(new(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.TurnNumber == 12) {
|
||||
this.State = GameState.Ended;
|
||||
this.SendEvent("gameEnd", new { game = this, moves, placements, specialSpacesActivated }, true);
|
||||
|
||||
foreach (var player in this.Players) {
|
||||
player.Move = null;
|
||||
}
|
||||
} else {
|
||||
this.TurnNumber++;
|
||||
|
||||
// Draw cards.
|
||||
foreach (var player in this.Players) {
|
||||
var index = player.GetHandIndex(player.Move!.Card.Number);
|
||||
var draw = player.drawOrder![this.TurnNumber + 2];
|
||||
player.Hand![index] = player.Deck![draw];
|
||||
player.Move = null;
|
||||
}
|
||||
this.SendEvent("turn", new { game = this, moves, placements, specialSpacesActivated }, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void SendPlayerReadyEvent(int playerIndex) => this.SendEvent("playerReady", new { playerIndex }, false);
|
||||
|
||||
internal void SendEvent(string eventType, object? data, bool includePlayerData) {
|
||||
foreach (var session in Program.httpServer.WebSocketServices.Hosts.First().Sessions.Sessions) {
|
||||
if (session is TableturfWebSocketBehaviour behaviour && behaviour.GameID == this.ID) {
|
||||
if (includePlayerData) {
|
||||
object? playerData = null;
|
||||
for (int i = 0; i < this.Players.Count; i++) {
|
||||
var player = this.Players[i];
|
||||
if (player.Token == behaviour.ClientToken) {
|
||||
playerData = new { playerIndex = i, hand = player.Hand, deck = player.Deck, move = player.Move, cardsUsed = player.CardsUsed };
|
||||
break;
|
||||
}
|
||||
}
|
||||
behaviour.SendInternal(JsonConvert.SerializeObject(new { @event = eventType, data, playerData }));
|
||||
} else {
|
||||
behaviour.SendInternal(JsonConvert.SerializeObject(new { @event = eventType, data }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
TableturfBattleServer/GameState.cs
Normal file
9
TableturfBattleServer/GameState.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace TableturfBattleServer;
|
||||
|
||||
public enum GameState {
|
||||
WaitingForPlayers,
|
||||
Preparing,
|
||||
Redraw,
|
||||
Ongoing,
|
||||
Ended
|
||||
}
|
||||
37
TableturfBattleServer/Move.cs
Normal file
37
TableturfBattleServer/Move.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public class Move {
|
||||
[JsonProperty("card")]
|
||||
public Card Card { get; }
|
||||
[JsonProperty("isPass")]
|
||||
public bool IsPass { get; }
|
||||
[JsonProperty("x")]
|
||||
public int X { get; }
|
||||
[JsonProperty("y")]
|
||||
public int Y { get; }
|
||||
[JsonProperty("rotation")]
|
||||
public int Rotation { get; }
|
||||
[JsonProperty("isSpecialAttack")]
|
||||
public bool IsSpecialAttack { get; }
|
||||
|
||||
public Move(Card card, bool isPass, int x, int y, int rotation, bool isSpecialAttack) {
|
||||
this.Card = card ?? throw new ArgumentNullException(nameof(card));
|
||||
this.IsPass = isPass;
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
this.Rotation = rotation;
|
||||
this.IsSpecialAttack = isSpecialAttack;
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public bool ShouldSerializeX() => !this.IsPass;
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public bool ShouldSerializeY() => !this.IsPass;
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public bool ShouldSerializeRotation() => !this.IsPass;
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public bool ShouldSerializeIsSpecialAttack() => !this.IsPass;
|
||||
}
|
||||
20
TableturfBattleServer/Placement.cs
Normal file
20
TableturfBattleServer/Placement.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public class Placement {
|
||||
[JsonProperty("players")]
|
||||
public List<int> Players { get; } = new();
|
||||
[JsonProperty("spacesAffected"), JsonConverter(typeof(SpacesAffectedDictionaryConverter))]
|
||||
public Dictionary<Point, Space> SpacesAffected { get; } = new();
|
||||
|
||||
internal class SpacesAffectedDictionaryConverter : JsonConverter<Dictionary<Point, Space>> {
|
||||
public override Dictionary<Point, Space>? ReadJson(JsonReader reader, Type objectType, Dictionary<Point, Space>? existingValue, bool hasExistingValue, JsonSerializer serializer) {
|
||||
var list = serializer.Deserialize<List<(Point space, Space newState)>>(reader);
|
||||
return list?.ToDictionary(o => o.space, o => o.newState);
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, Dictionary<Point, Space>? value, JsonSerializer serializer) {
|
||||
serializer.Serialize(writer, value?.Select(e => new { space = e.Key, newState = e.Value }));
|
||||
}
|
||||
}
|
||||
}
|
||||
68
TableturfBattleServer/Player.cs
Normal file
68
TableturfBattleServer/Player.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public class Player {
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; }
|
||||
[JsonIgnore]
|
||||
public Guid Token { get; }
|
||||
[JsonProperty("colour")]
|
||||
public Colour Colour { get; set; }
|
||||
[JsonProperty("specialColour")]
|
||||
public Colour SpecialColour { get; set; }
|
||||
[JsonProperty("specialAccentColour")]
|
||||
public Colour SpecialAccentColour { get; set; }
|
||||
[JsonProperty("specialPoints")]
|
||||
public int SpecialPoints { get; set; }
|
||||
|
||||
[JsonProperty("totalSpecialPoints")]
|
||||
public int TotalSpecialPoints { get; set; }
|
||||
[JsonProperty("passes")]
|
||||
public int Passes { get; set; }
|
||||
|
||||
[JsonProperty("isReady")]
|
||||
public bool IsReady => this.Move != null || (this.Deck != null && this.Hand == null);
|
||||
|
||||
[JsonIgnore]
|
||||
internal Card[]? Deck;
|
||||
[JsonIgnore]
|
||||
internal readonly List<int> CardsUsed = new(12);
|
||||
[JsonIgnore]
|
||||
internal Card[]? Hand;
|
||||
[JsonIgnore]
|
||||
internal Move? Move;
|
||||
[JsonIgnore]
|
||||
internal int[]? drawOrder;
|
||||
|
||||
public Player(string name, Guid token) {
|
||||
this.Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
this.Token = token;
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(drawOrder))]
|
||||
internal void Shuffle(Random random) {
|
||||
this.drawOrder = new int[15];
|
||||
for (int i = 0; i < 15; i++) this.drawOrder[i] = i;
|
||||
for (int i = 14; i > 0; i--) {
|
||||
var j = random.Next(i);
|
||||
(this.drawOrder[i], this.drawOrder[j]) = (this.drawOrder[j], this.drawOrder[i]);
|
||||
}
|
||||
if (this.Deck != null) {
|
||||
this.Hand = new Card[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
this.Hand[i] = this.Deck[this.drawOrder[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal int GetHandIndex(int cardNumber) {
|
||||
if (this.Hand != null) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (this.Hand[i].Number == cardNumber) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
14
TableturfBattleServer/Point.cs
Normal file
14
TableturfBattleServer/Point.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public struct Point {
|
||||
[JsonProperty("x")]
|
||||
public int X;
|
||||
[JsonProperty("y")]
|
||||
public int Y;
|
||||
|
||||
public Point(int x, int y) {
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
}
|
||||
}
|
||||
374
TableturfBattleServer/Program.cs
Normal file
374
TableturfBattleServer/Program.cs
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Timers;
|
||||
using System.Web;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using WebSocketSharp.Server;
|
||||
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
|
||||
internal class Program {
|
||||
internal static HttpServer httpServer = new(IPAddress.Loopback, 3333);
|
||||
internal static Dictionary<Guid, Game> games = new();
|
||||
internal static readonly Timer timer = new(500);
|
||||
private static readonly List<Guid> gameIdsToRemove = new();
|
||||
|
||||
internal static void Main() {
|
||||
timer.Elapsed += Timer_Elapsed;
|
||||
|
||||
httpServer.AddWebSocketService<TableturfWebSocketBehaviour>("/api/websocket");
|
||||
httpServer.OnGet += HttpServer_OnRequest;
|
||||
httpServer.OnPost += HttpServer_OnRequest;
|
||||
httpServer.Start();
|
||||
Console.WriteLine($"Listening on http://{httpServer.Address}:{httpServer.Port}");
|
||||
|
||||
Thread.Sleep(Timeout.Infinite);
|
||||
}
|
||||
|
||||
private static void Timer_Elapsed(object? sender, ElapsedEventArgs e) {
|
||||
lock (games) {
|
||||
foreach (var (id, game) in games) {
|
||||
lock (game.Players) {
|
||||
game.Tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HttpServer_OnRequest(object? sender, HttpRequestEventArgs e) {
|
||||
e.Response.AppendHeader("Access-Control-Allow-Origin", "*");
|
||||
if (e.Request.RawUrl == "/api/games/new") {
|
||||
if (e.Request.HttpMethod != "POST") {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
|
||||
e.Response.AddHeader("Allow", "POST");
|
||||
} else if (e.Request.ContentLength64 >= 65536) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.RequestEntityTooLarge;
|
||||
} else {
|
||||
try {
|
||||
var d = DecodeFormData(e.Request.InputStream);
|
||||
Guid clientToken;
|
||||
if (!d.TryGetValue("name", out var name)) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.UnprocessableEntity, new("InvalidName", "Missing name."));
|
||||
return;
|
||||
}
|
||||
if (d.TryGetValue("clientToken", out var tokenString) && tokenString != "") {
|
||||
if (!Guid.TryParse(tokenString, out clientToken)) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.UnprocessableEntity, new("InvalidClientToken", "Invalid client token."));
|
||||
return;
|
||||
}
|
||||
} else
|
||||
clientToken = Guid.NewGuid();
|
||||
var game = new Game(9, 26);
|
||||
game.Board[4, 4] = Space.SpecialInactive2;
|
||||
game.Board[4, 21] = Space.SpecialInactive1;
|
||||
game.Players[0] = new(name, clientToken);
|
||||
games.Add(game.ID, game);
|
||||
|
||||
SetResponse(e.Response, (int) HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(new { gameID = game.ID, clientToken }));
|
||||
} catch (ArgumentException) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.BadRequest;
|
||||
}
|
||||
}
|
||||
} else if (e.Request.RawUrl == "/api/cards") {
|
||||
if (e.Request.HttpMethod is not ("GET" or "HEAD")) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
|
||||
e.Response.AddHeader("Allow", "GET, HEAD");
|
||||
return;
|
||||
}
|
||||
e.Response.AppendHeader("Cache-Control", "max-age=86400");
|
||||
e.Response.AppendHeader("ETag", CardDatabase.Version.ToString());
|
||||
if (e.Response.Headers["If-None-Match"] == CardDatabase.Version.ToString()) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.NotModified;
|
||||
} else {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.OK, "application/json", CardDatabase.JSON);
|
||||
}
|
||||
} else {
|
||||
var m = Regex.Match(e.Request.RawUrl, @"^/api/games/([\w-]+)(?:/(\w+)(?:/([\w-]+))?)?$", RegexOptions.Compiled);
|
||||
if (m.Success) {
|
||||
if (!Guid.TryParse(m.Groups[1].Value, out var gameID)) {
|
||||
SetResponse(e.Response, 400, "text/plain", "Invalid game ID");
|
||||
return;
|
||||
}
|
||||
lock (games) {
|
||||
if (!games.TryGetValue(gameID, out var game)) {
|
||||
SetResponse(e.Response, 404, "text/plain", "Game not found");
|
||||
return;
|
||||
}
|
||||
lock (game.Players) {
|
||||
switch (m.Groups[2].Value) {
|
||||
case "": {
|
||||
if (e.Request.HttpMethod is not ("GET" or "HEAD")) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
|
||||
e.Response.AddHeader("Allow", "GET, HEAD");
|
||||
return;
|
||||
}
|
||||
SetResponse(e.Response, (int) HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(game));
|
||||
break;
|
||||
}
|
||||
case "playerData": {
|
||||
if (e.Request.HttpMethod is not ("GET" or "HEAD")) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
|
||||
e.Response.AddHeader("Allow", "GET, HEAD");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(m.Groups[3].Value, out var clientToken))
|
||||
clientToken = Guid.Empty;
|
||||
|
||||
SetResponse(e.Response, (int) HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(new {
|
||||
game,
|
||||
playerData = game.GetPlayer(clientToken, out var playerIndex, out var player)
|
||||
? new { playerIndex, hand = player.Hand, deck = player.Deck, cardsUsed = player.CardsUsed, move = player.Move }
|
||||
: null
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case "join": {
|
||||
if (e.Request.HttpMethod != "POST") {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
|
||||
e.Response.AddHeader("Allow", "POST");
|
||||
} else if (e.Request.ContentLength64 >= 65536) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.RequestEntityTooLarge;
|
||||
} else {
|
||||
try {
|
||||
var d = DecodeFormData(e.Request.InputStream);
|
||||
Guid clientToken;
|
||||
if (!d.TryGetValue("name", out var name)) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.UnprocessableEntity, new("InvalidName", "Missing name."));
|
||||
return;
|
||||
}
|
||||
if (d.TryGetValue("clientToken", out var tokenString) && tokenString != "") {
|
||||
if (!Guid.TryParse(tokenString, out clientToken)) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.BadRequest, new("InvalidClientToken", "Invalid client token."));
|
||||
return;
|
||||
}
|
||||
} else
|
||||
clientToken = Guid.NewGuid();
|
||||
|
||||
if (!game.GetPlayer(clientToken, out var playerIndex, out var player)) {
|
||||
if (game.State != GameState.WaitingForPlayers) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.Gone, new("GameAlreadyStarted", "The game has already started."));
|
||||
return;
|
||||
}
|
||||
|
||||
player = new Player(name, clientToken);
|
||||
if (!game.TryAddPlayer(player, out playerIndex, out var error)) {
|
||||
SetErrorResponse(e.Response, error.Code == "GameAlreadyStarted" ? (int) HttpStatusCode.Gone : 422, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If they're already in the game, resend the original join response instead of an error.
|
||||
SetResponse(e.Response, (int) HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(new { playerIndex, clientToken }));
|
||||
game.SendEvent("join", new { playerIndex, player }, false);
|
||||
timer.Start();
|
||||
} catch (ArgumentException) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.BadRequest;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "chooseDeck": {
|
||||
if (e.Request.HttpMethod != "POST") {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
|
||||
e.Response.AddHeader("Allow", "POST");
|
||||
} else if (e.Request.ContentLength64 >= 65536) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.RequestEntityTooLarge;
|
||||
} else {
|
||||
try {
|
||||
var d = DecodeFormData(e.Request.InputStream);
|
||||
if (!d.TryGetValue("clientToken", out var tokenString) || !Guid.TryParse(tokenString, out var clientToken)) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.BadRequest, new("InvalidClientToken", "Invalid client token."));
|
||||
return;
|
||||
}
|
||||
if (!game.GetPlayer(clientToken, out var playerIndex, out var player)) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.UnprocessableEntity, new("NotInGame", "You're not in the game."));
|
||||
return;
|
||||
}
|
||||
if (player.Deck != null) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.Conflict, new("DeckAlreadyChosen", "You've already chosen a deck."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!d.TryGetValue("deckName", out var deckName)) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.BadRequest, new("InvalidDeckName", "Missing deck name."));
|
||||
return;
|
||||
}
|
||||
if (!d.TryGetValue("deckCards", out var deckString)) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.BadRequest, new("InvalidDeckCards", "Missing deck cards."));
|
||||
return;
|
||||
}
|
||||
var array = deckString.Split(new[] { ',', '+', ' ' }, 15);
|
||||
if (array.Length != 15) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.UnprocessableEntity, new("InvalidDeckCards", "Invalid deck list."));
|
||||
return;
|
||||
}
|
||||
var cards = new int[15];
|
||||
for (int i = 0; i < 15; i++) {
|
||||
if (!int.TryParse(array[i], out var cardNumber) || cardNumber < 0 || cardNumber > CardDatabase.Cards.Count) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.UnprocessableEntity, new("InvalidDeckCards", "Invalid deck list."));
|
||||
return;
|
||||
}
|
||||
if (Array.IndexOf(cards, cardNumber, 0, i) >= 0) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.UnprocessableEntity, new("InvalidDeckCards", "Deck cannot have duplicates."));
|
||||
return;
|
||||
}
|
||||
cards[i] = cardNumber;
|
||||
}
|
||||
|
||||
player.Deck = cards.Select(CardDatabase.GetCard).ToArray();
|
||||
e.Response.StatusCode = (int) HttpStatusCode.NoContent;
|
||||
game.SendPlayerReadyEvent(playerIndex);
|
||||
timer.Start();
|
||||
} catch (ArgumentException) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.BadRequest;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "play": {
|
||||
if (e.Request.HttpMethod != "POST") {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
|
||||
e.Response.AddHeader("Allow", "POST");
|
||||
} else if (e.Request.ContentLength64 >= 65536) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.RequestEntityTooLarge;
|
||||
} else {
|
||||
try {
|
||||
var d = DecodeFormData(e.Request.InputStream);
|
||||
Guid clientToken;
|
||||
if (!d.TryGetValue("clientToken", out var tokenString) || !Guid.TryParse(tokenString, out clientToken)) {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.BadRequest, "text/plain", "Invalid client token");
|
||||
return;
|
||||
}
|
||||
if (game.State != GameState.Ongoing) {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.Gone, "text/plain", "You can't do that in this game state.");
|
||||
return;
|
||||
}
|
||||
if (!game.GetPlayer(clientToken, out var playerIndex, out var player)) {
|
||||
SetErrorResponse(e.Response, (int) HttpStatusCode.UnprocessableEntity, new("NotInGame", "You're not in the game."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (player!.Move != null) {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.Conflict, "text/plain", "You've already chosen a move.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!d.TryGetValue("cardNumber", out var cardNumberStr) || !int.TryParse(cardNumberStr, out var cardNumber)) {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.UnprocessableEntity, "text/plain", "Missing or invalid card number");
|
||||
return;
|
||||
}
|
||||
|
||||
var handIndex = player.GetHandIndex(cardNumber);
|
||||
if (handIndex < 0) {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.UnprocessableEntity, "text/plain", "You don't have that card");
|
||||
return;
|
||||
}
|
||||
|
||||
var card = player.Hand![handIndex];
|
||||
if (d.TryGetValue("isPass", out var isPassStr) && isPassStr.ToLower() is not ("false" or "0")) {
|
||||
player.Move = new(card, true, 0, 0, 0, false);
|
||||
} else {
|
||||
var isSpecialAttack = d.TryGetValue("isSpecialAttack", out var isSpecialAttackStr) && isSpecialAttackStr.ToLower() is not ("false" or "0");
|
||||
if (!d.TryGetValue("x", out var xs) || !int.TryParse(xs, out var x)
|
||||
|| !d.TryGetValue("y", out var ys) || !int.TryParse(ys, out var y)
|
||||
|| !d.TryGetValue("r", out var rs) || !int.TryParse(rs, out var r)) {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.BadRequest, "text/plain", "Missing or invalid coordinates");
|
||||
return;
|
||||
}
|
||||
r &= 3;
|
||||
if (!game.CanPlay(playerIndex, card, x, y, r, isSpecialAttack)) {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.UnprocessableEntity, "text/plain", "Illegal move");
|
||||
return;
|
||||
}
|
||||
player.Move = new(card, false, x, y, r, isSpecialAttack);
|
||||
}
|
||||
e.Response.StatusCode = (int) HttpStatusCode.NoContent;
|
||||
game.SendPlayerReadyEvent(playerIndex);
|
||||
timer.Start();
|
||||
} catch (ArgumentException) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.BadRequest;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "redraw": {
|
||||
if (e.Request.HttpMethod != "POST") {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
|
||||
e.Response.AddHeader("Allow", "POST");
|
||||
} else if (e.Request.ContentLength64 >= 65536) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.RequestEntityTooLarge;
|
||||
} else {
|
||||
try {
|
||||
if (game.State != GameState.Redraw) {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.Gone, "text/plain", "You can't do that in this game state.");
|
||||
return;
|
||||
}
|
||||
|
||||
var d = DecodeFormData(e.Request.InputStream);
|
||||
if (!d.TryGetValue("clientToken", out var tokenString) || !Guid.TryParse(tokenString, out var clientToken)) {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.BadRequest, "text/plain", "Invalid client token");
|
||||
return;
|
||||
}
|
||||
if (!game.GetPlayer(clientToken, out var playerIndex, out var player)) {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.UnprocessableEntity, "text/plain", "You're not in the game.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.Move != null) {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.Conflict, "text/plain", "You've already chosen a move.");
|
||||
return;
|
||||
}
|
||||
|
||||
var redraw = d.TryGetValue("redraw", out var redrawStr) && redrawStr.ToLower() is not ("false" or "0");
|
||||
player.Move = new(player.Hand![0], false, 0, 0, 0, redraw);
|
||||
e.Response.StatusCode = (int) HttpStatusCode.NoContent;
|
||||
game.SendPlayerReadyEvent(playerIndex);
|
||||
timer.Start();
|
||||
} catch (ArgumentException) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.BadRequest;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
SetResponse(e.Response, (int) HttpStatusCode.NotFound, "text/plain", "Endpoint not found");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SetResponse(e.Response, (int) HttpStatusCode.NotFound, "text/plain", "Endpoint not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetErrorResponse(WebSocketSharp.Net.HttpListenerResponse response, int statusCode, Error error) {
|
||||
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(error));
|
||||
SetResponse(response, statusCode, "application/json", bytes);
|
||||
}
|
||||
private static void SetResponse(WebSocketSharp.Net.HttpListenerResponse response, int statusCode, string contentType, string content) {
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
SetResponse(response, statusCode, contentType, bytes);
|
||||
}
|
||||
private static void SetResponse(WebSocketSharp.Net.HttpListenerResponse response, int statusCode, string contentType, byte[] content) {
|
||||
response.StatusCode = statusCode;
|
||||
response.ContentType = contentType;
|
||||
response.ContentLength64 = content.Length;
|
||||
response.Close(content, true);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> DecodeFormData(Stream stream) {
|
||||
using var reader = new StreamReader(stream);
|
||||
var s = reader.ReadToEnd();
|
||||
return s != ""
|
||||
? s.Split(new[] { '&' }).Select(s => s.Split('=')).Select(a => a.Length == 2 ? a : throw new ArgumentException("Invalid form data"))
|
||||
.ToDictionary(a => HttpUtility.UrlDecode(a[0]), a => HttpUtility.UrlDecode(a[1]))
|
||||
: new();
|
||||
}
|
||||
}
|
||||
7
TableturfBattleServer/Rarity.cs
Normal file
7
TableturfBattleServer/Rarity.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace TableturfBattleServer;
|
||||
|
||||
public enum Rarity {
|
||||
Common,
|
||||
Rare,
|
||||
Fresh
|
||||
}
|
||||
18
TableturfBattleServer/Space.cs
Normal file
18
TableturfBattleServer/Space.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
namespace TableturfBattleServer;
|
||||
public enum Space {
|
||||
Empty = 0,
|
||||
Wall = 1,
|
||||
OutOfBounds = 2,
|
||||
Ink1 = 4,
|
||||
Ink2 = 5,
|
||||
Ink3 = 6,
|
||||
Ink4 = 7,
|
||||
SpecialInactive1 = 8,
|
||||
SpecialInactive2 = 9,
|
||||
SpecialInactive3 = 10,
|
||||
SpecialInactive4 = 11,
|
||||
SpecialActive1 = 12,
|
||||
SpecialActive2 = 13,
|
||||
SpecialActive3 = 14,
|
||||
SpecialActive4 = 15
|
||||
}
|
||||
18
TableturfBattleServer/TableturfBattleServer.csproj
Normal file
18
TableturfBattleServer/TableturfBattleServer.csproj
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageProjectUrl>https://github.com/AndrioCelos/TableturfBattleApp</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/AndrioCelos/TableturfBattleApp</RepositoryUrl>
|
||||
<Version>0.0.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
19
TableturfBattleServer/TableturfWebSocketBehaviour.cs
Normal file
19
TableturfBattleServer/TableturfWebSocketBehaviour.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System.Web;
|
||||
|
||||
using WebSocketSharp.Server;
|
||||
|
||||
internal class TableturfWebSocketBehaviour : WebSocketBehavior {
|
||||
public Guid GameID { get; set; }
|
||||
public Guid ClientToken { get; set; }
|
||||
|
||||
protected override void OnOpen() {
|
||||
var args = this.Context.RequestUri.Query[1..].Split('&').Select(s => s.Split('=', 2)).Where(a => a.Length == 2)
|
||||
.ToDictionary(a => HttpUtility.UrlDecode(a[0]), a => HttpUtility.UrlDecode(a[1]));
|
||||
if (args.TryGetValue("gameID", out var gameIDString) && Guid.TryParse(gameIDString, out var gameID))
|
||||
this.GameID = gameID;
|
||||
if (args.TryGetValue("clientToken", out var clientTokenString) && Guid.TryParse(clientTokenString, out var clientToken))
|
||||
this.ClientToken = clientToken;
|
||||
}
|
||||
|
||||
internal void SendInternal(string data) => this.Send(data);
|
||||
}
|
||||
21
license.md
Normal file
21
license.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Andrea Giannone
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
7
readme.md
Normal file
7
readme.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Tableturf Battle
|
||||
|
||||
This is a Web application and backend that simulates the [Tableturf Battle](https://splatoonwiki.org/wiki/Tableturf_Battle) minigame from [_Splatoon 3_](https://splatoon.nintendo.com/).
|
||||
|
||||
This application is under development and should be considered an early access release.
|
||||
|
||||
_Splatoon_ is © Nintendo. This is a fan project and is not affiliated with Nintendo. All product names, logos, and brands are property of their respective owners.
|
||||
Loading…
Reference in New Issue
Block a user