Browse Source

[Card Game] Add LAN Networking and Extra Polish (#55)

* Allow up to 7 players (6 AI).
* Store Game and Player Settings (move this to StorageContainers when that gets merged)
* Add support for Italian, French, Spanish, Russian, Chinese and Japanese
* Use NetworkStateMachine samples as basis for LAN Networking
* User Environment.UserName for currently logged in user, fall back to 'You' if none returned.
* Add MagicTrick and GinRummy Tutorials.
Dominique Louis 3 weeks ago
parent
commit
dbb5e87cc3
87 changed files with 16297 additions and 446 deletions
  1. 20 0
      CardsStarterKit/Core/Content/Content.mgcb
  2. 13 2
      CardsStarterKit/Core/Content/Fonts/Bold.spritefont
  3. 241 0
      CardsStarterKit/Core/Content/Fonts/Bold_CJK.spritefont
  4. 14 3
      CardsStarterKit/Core/Content/Fonts/MenuFont.spritefont
  5. 241 0
      CardsStarterKit/Core/Content/Fonts/MenuFont_CJK.spritefont
  6. 14 3
      CardsStarterKit/Core/Content/Fonts/Regular.spritefont
  7. 241 0
      CardsStarterKit/Core/Content/Fonts/Regular_CJK.spritefont
  8. BIN
      CardsStarterKit/Core/Content/Images/UI/gradient.png
  9. 2 1
      CardsStarterKit/Core/Game/Blackjack.Core.csproj
  10. 556 117
      CardsStarterKit/Core/Game/Blackjack/Game/BlackjackCardGame.cs
  11. 24 0
      CardsStarterKit/Core/Game/Blackjack/Game/BlackjackConstants.cs
  12. 655 56
      CardsStarterKit/Core/Game/Blackjack/Misc/BetGameComponent.cs
  13. 44 0
      CardsStarterKit/Core/Game/Blackjack/Networking/NetworkSerializationExtensions.cs
  14. 34 0
      CardsStarterKit/Core/Game/Blackjack/Networking/PacketTypes.cs
  15. 277 0
      CardsStarterKit/Core/Game/Blackjack/Networking/Packets.cs
  16. 47 3
      CardsStarterKit/Core/Game/Blackjack/UI/BlackJackAnimatedPlayerHandComponent.cs
  17. 7 1
      CardsStarterKit/Core/Game/Blackjack/UI/BlackJackTable.cs
  18. 15 1
      CardsStarterKit/Core/Game/Blackjack/UI/BlackjackAnimatedDealerHandComponent.cs
  19. 1 1
      CardsStarterKit/Core/Game/Blackjack/UI/Button.cs
  20. 227 12
      CardsStarterKit/Core/Game/Blackjack/UI/UIConstants.cs
  21. 3 0
      CardsStarterKit/Core/Game/Misc/AudioManager.cs
  22. 301 0
      CardsStarterKit/Core/Game/Misc/GameSettings.cs
  23. 1116 0
      CardsStarterKit/Core/Game/Resources.Designer.cs
  24. 353 0
      CardsStarterKit/Core/Game/Resources.es.resx
  25. 353 0
      CardsStarterKit/Core/Game/Resources.fr.resx
  26. 353 0
      CardsStarterKit/Core/Game/Resources.it.resx
  27. 352 0
      CardsStarterKit/Core/Game/Resources.ja.resx
  28. BIN
      CardsStarterKit/Core/Game/Resources.resources
  29. 412 0
      CardsStarterKit/Core/Game/Resources.resx
  30. 352 0
      CardsStarterKit/Core/Game/Resources.ru.resx
  31. 352 0
      CardsStarterKit/Core/Game/Resources.zh.resx
  32. 42 16
      CardsStarterKit/Core/Game/ScreenManager/MenuEntry.cs
  33. 13 2
      CardsStarterKit/Core/Game/ScreenManager/MenuScreen.cs
  34. 89 2
      CardsStarterKit/Core/Game/ScreenManager/ScreenManager.cs
  35. 85 0
      CardsStarterKit/Core/Game/Screens/AvailableSessionMenuEntry.cs
  36. 196 0
      CardsStarterKit/Core/Game/Screens/BlackjackLobbyScreen.cs
  37. 14 0
      CardsStarterKit/Core/Game/Screens/CardsStarterKit.code-workspace
  38. 536 33
      CardsStarterKit/Core/Game/Screens/GameplayScreen.cs
  39. 1 1
      CardsStarterKit/Core/Game/Screens/InstructionScreen.cs
  40. 41 14
      CardsStarterKit/Core/Game/Screens/MainMenuScreen.cs
  41. 57 0
      CardsStarterKit/Core/Game/Screens/MessageBoxScreen.cs
  42. 196 0
      CardsStarterKit/Core/Game/Screens/NetworkBusyScreen.cs
  43. 130 0
      CardsStarterKit/Core/Game/Screens/NetworkSessionComponent.cs
  44. 44 0
      CardsStarterKit/Core/Game/Screens/OperationCompletedEventArgs.cs
  45. 0 120
      CardsStarterKit/Core/Game/Screens/OptionsMenu.cs
  46. 3 3
      CardsStarterKit/Core/Game/Screens/PauseScreen.cs
  47. 301 0
      CardsStarterKit/Core/Game/Screens/SessionBrowserScreen.cs
  48. 739 0
      CardsStarterKit/Core/Game/Screens/SettingsScreen.cs
  49. 20 1
      CardsStarterKit/Framework/Cards/CardPacket.cs
  50. 9 1
      CardsStarterKit/Framework/Cards/TraditionalCard.cs
  51. 114 0
      CardsStarterKit/Framework/UI/DeckDisplayComponent.cs
  52. 9 0
      CardsStarterKit/Framework/UI/GameTable.cs
  53. 139 0
      CardsStarterKit/Framework/UI/RiffleShuffleAnimation.cs
  54. 138 0
      CardsStarterKit/Framework/UI/ShuffleAnimation.cs
  55. 165 0
      CardsStarterKit/Framework/UI/ShuffleAnimationComponent.cs
  56. 18 9
      CardsStarterKit/Framework/UI/TransitionGameComponentAnimation.cs
  57. 42 2
      CardsStarterKit/Framework/Utils/UIUtility.cs
  58. 1 0
      CardsStarterKit/Platforms/Android/AndroidManifest.xml
  59. 1 1
      CardsStarterKit/Platforms/Android/BlackJack.Android.csproj
  60. 1 1
      CardsStarterKit/Platforms/iOS/BlackJack.iOS.csproj
  61. 2 0
      CardsStarterKit/Platforms/iOS/Info.plist
  62. 159 0
      CardsStarterKit/Tools/CJK_TRANSLATION_SUMMARY.md
  63. 148 0
      CardsStarterKit/Tools/ExtractCJKCharacters.py
  64. 366 0
      CardsStarterKit/Tools/chinese_character_regions.xml
  65. 1 0
      CardsStarterKit/Tools/chinese_characters.txt
  66. 570 0
      CardsStarterKit/Tools/cjk_character_regions.xml
  67. 1 0
      CardsStarterKit/Tools/cjk_characters.txt
  68. 294 0
      CardsStarterKit/Tools/japanese_character_regions.xml
  69. 1 0
      CardsStarterKit/Tools/japanese_characters.txt
  70. 1392 0
      CardsStarterKit/Tutorials/01_MagicTrick_NineCardTrick.md
  71. 2345 0
      CardsStarterKit/Tutorials/02_GinRummy_Implementation.md
  72. 245 0
      MonoGame.Xna.Framework.Net/Net/ConnectionHealthMonitor.cs
  73. 180 0
      MonoGame.Xna.Framework.Net/Net/Discovery.cs
  74. 11 1
      MonoGame.Xna.Framework.Net/Net/Enums/NetworkSessionJoinError.cs
  75. 5 0
      MonoGame.Xna.Framework.Net/Net/Enums/NetworkSessionState.cs
  76. 45 0
      MonoGame.Xna.Framework.Net/Net/GamerLeavingMessage.cs
  77. 48 0
      MonoGame.Xna.Framework.Net/Net/HeartbeatMessage.cs
  78. 43 0
      MonoGame.Xna.Framework.Net/Net/HeartbeatReplyMessage.cs
  79. 58 0
      MonoGame.Xna.Framework.Net/Net/INetworkLogger.cs
  80. 36 0
      MonoGame.Xna.Framework.Net/Net/JoinRejectedMessage.cs
  81. 5 0
      MonoGame.Xna.Framework.Net/Net/JoinRequestMessage.cs
  82. 108 0
      MonoGame.Xna.Framework.Net/Net/NetworkDiagnostics.cs
  83. 5 0
      MonoGame.Xna.Framework.Net/Net/NetworkMessageRegistry.cs
  84. 328 19
      MonoGame.Xna.Framework.Net/Net/NetworkSession.cs
  85. 53 0
      MonoGame.Xna.Framework.Net/Net/SessionStateMessage.cs
  86. 66 17
      MonoGame.Xna.Framework.Net/Net/SystemLinkSessionManager.cs
  87. 18 3
      Peer2Peer/Core/PeerToPeerGame.cs

+ 20 - 0
CardsStarterKit/Core/Content/Content.mgcb

@@ -29,6 +29,21 @@
 /processor:FontDescriptionProcessor
 /build:Fonts/Regular.spritefont
 
+#begin Fonts/Bold_CJK.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/build:Fonts/Bold_CJK.spritefont
+
+#begin Fonts/MenuFont_CJK.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/build:Fonts/MenuFont_CJK.spritefont
+
+#begin Fonts/Regular_CJK.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/build:Fonts/Regular_CJK.spritefont
+
 #begin Images/blank.png
 /importer:TextureImporter
 /processor:TextureProcessor
@@ -429,6 +444,11 @@
 /processor:TextureProcessor
 /build:Images/UI/win.png
 
+#begin Images/UI/gradient.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/build:Images/UI/gradient.png
+
 #begin Images/youLose.png
 /importer:TextureImporter
 /processor:TextureProcessor

+ 13 - 2
CardsStarterKit/Core/Content/Fonts/Bold.spritefont

@@ -10,8 +10,9 @@ with.
 
     <!--
     Modify this string to change the font that will be imported.
+    Using Arial Unicode for better currency symbol support
     -->
-    <FontName>Arial</FontName>
+    <FontName>Arial Unicode</FontName>
 
     <!--
     Size is a float value, measured in points. Modify this value to change
@@ -53,7 +54,17 @@ with.
     <CharacterRegions>
       <CharacterRegion>
         <Start>&#32;</Start>
-        <End>&#126;</End>
+        <End>&#255;</End>
+      </CharacterRegion>
+      <!-- Cyrillic characters for Russian support -->
+      <CharacterRegion>
+        <Start>&#x0400;</Start>
+        <End>&#x04FF;</End>
+      </CharacterRegion>
+      <!-- Currency symbols (includes €, £, ¥, ₽, etc.) -->
+      <CharacterRegion>
+        <Start>&#x20A0;</Start>
+        <End>&#x20CF;</End>
       </CharacterRegion>
     </CharacterRegions>
   </Asset>

+ 241 - 0
CardsStarterKit/Core/Content/Fonts/Bold_CJK.spritefont

@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+CJK (Chinese, Japanese, Korean) font variant using Arial Unicode
+Includes optimized character ranges for Japanese and Chinese translations
+-->
+<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
+  <Asset Type="Graphics:FontDescription">
+
+    <!--
+    Modify this string to change the font that will be imported.
+    Using Arial Unicode for better currency symbol support
+    -->
+    <FontName>Arial Unicode</FontName>
+    
+    <!--
+    Size is a float value, measured in points. Modify this value to change
+    the size of the font.
+    -->
+    <Size>20</Size>
+
+    <!--
+    Spacing is a float value, measured in pixels. Modify this value to change
+    the amount of spacing in between characters.
+    -->
+    <Spacing>0</Spacing>
+
+    <!--
+    UseKerning controls the layout of the font. If this value is true, kerning information
+    will be used when placing characters.
+    -->
+    <UseKerning>true</UseKerning>
+
+    <!--
+    Style controls the style of the font. Valid entries are "Regular", "Bold", "Italic",
+    and "Bold, Italic", and are case sensitive.
+    -->
+    <Style>Bold</Style>
+
+    <!--
+    If you uncomment this line, the default character will be substituted if you draw
+    or measure text that contains characters which were not included in the font.
+    -->
+    <!-- <DefaultCharacter>*</DefaultCharacter> -->
+
+    <!--
+    CharacterRegions control what letters are available in the font. Every
+    character from Start to End will be built and made available for drawing. The
+    default range is from 32, (ASCII space), to 126, ('~'), covering the basic Latin
+    character set. The characters are ordered according to the Unicode standard.
+    See the documentation for more information.
+    -->
+    <CharacterRegions>
+      <!-- Basic Latin and Extended Latin -->
+      <CharacterRegion>
+        <Start>&#32;</Start>
+        <End>&#255;</End>
+      </CharacterRegion>
+      <!-- Currency symbols -->
+      <CharacterRegion>
+        <Start>&#x20A0;</Start>
+        <End>&#x20CF;</End>
+      </CharacterRegion>
+      <!-- Hiragana (Japanese) -->
+      <CharacterRegion>
+        <Start>&#x3040;</Start>
+        <End>&#x309F;</End>
+      </CharacterRegion>
+      <!-- Katakana (Japanese) -->
+      <CharacterRegion>
+        <Start>&#x30A0;</Start>
+        <End>&#x30FF;</End>
+      </CharacterRegion>
+      <!-- CJK Unified Ideographs - Common subset for our translations -->
+      <!-- Range covers the most frequently used characters -->
+      <CharacterRegion>
+        <Start>&#x4E00;</Start>
+        <End>&#x4FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5000;</Start>
+        <End>&#x51FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5200;</Start>
+        <End>&#x53FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5400;</Start>
+        <End>&#x55FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5600;</Start>
+        <End>&#x57FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5800;</Start>
+        <End>&#x59FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5A00;</Start>
+        <End>&#x5BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5C00;</Start>
+        <End>&#x5DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5E00;</Start>
+        <End>&#x5FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6000;</Start>
+        <End>&#x61FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6200;</Start>
+        <End>&#x63FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6400;</Start>
+        <End>&#x65FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6600;</Start>
+        <End>&#x67FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6800;</Start>
+        <End>&#x69FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6A00;</Start>
+        <End>&#x6BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6C00;</Start>
+        <End>&#x6DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6E00;</Start>
+        <End>&#x6FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7000;</Start>
+        <End>&#x71FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7200;</Start>
+        <End>&#x73FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7400;</Start>
+        <End>&#x75FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7600;</Start>
+        <End>&#x77FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7800;</Start>
+        <End>&#x79FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7A00;</Start>
+        <End>&#x7BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7C00;</Start>
+        <End>&#x7DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7E00;</Start>
+        <End>&#x7FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8000;</Start>
+        <End>&#x81FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8200;</Start>
+        <End>&#x83FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8400;</Start>
+        <End>&#x85FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8600;</Start>
+        <End>&#x87FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8800;</Start>
+        <End>&#x89FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8A00;</Start>
+        <End>&#x8BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8C00;</Start>
+        <End>&#x8DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8E00;</Start>
+        <End>&#x8FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9000;</Start>
+        <End>&#x91FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9200;</Start>
+        <End>&#x93FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9400;</Start>
+        <End>&#x95FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9600;</Start>
+        <End>&#x97FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9800;</Start>
+        <End>&#x99FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9A00;</Start>
+        <End>&#x9BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9C00;</Start>
+        <End>&#x9DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9E00;</Start>
+        <End>&#x9FFF;</End>
+      </CharacterRegion>
+    </CharacterRegions>
+  </Asset>
+</XnaContent>

+ 14 - 3
CardsStarterKit/Core/Content/Fonts/MenuFont.spritefont

@@ -10,9 +10,10 @@ with.
 
     <!--
     Modify this string to change the font that will be imported.
+    Using Arial Unicode for better currency symbol support
     -->
-    <FontName>Arial</FontName>
-
+    <FontName>Arial Unicode</FontName>
+    
     <!--
     Size is a float value, measured in points. Modify this value to change
     the size of the font.
@@ -53,7 +54,17 @@ with.
     <CharacterRegions>
       <CharacterRegion>
         <Start>&#32;</Start>
-        <End>&#126;</End>
+        <End>&#255;</End>
+      </CharacterRegion>
+      <!-- Cyrillic characters for Russian support -->
+      <CharacterRegion>
+        <Start>&#x0400;</Start>
+        <End>&#x04FF;</End>
+      </CharacterRegion>
+      <!-- Currency symbols (includes €, £, ¥, ₽, etc.) -->
+      <CharacterRegion>
+        <Start>&#x20A0;</Start>
+        <End>&#x20CF;</End>
       </CharacterRegion>
     </CharacterRegions>
   </Asset>

+ 241 - 0
CardsStarterKit/Core/Content/Fonts/MenuFont_CJK.spritefont

@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+CJK (Chinese, Japanese, Korean) font variant using Arial Unicode
+Includes optimized character ranges for Japanese and Chinese translations
+-->
+<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
+  <Asset Type="Graphics:FontDescription">
+
+    <!--
+    Modify this string to change the font that will be imported.
+    Using Arial Unicode for better currency symbol support
+    -->
+    <FontName>Arial Unicode</FontName>
+    
+    <!--
+    Size is a float value, measured in points. Modify this value to change
+    the size of the font.
+    -->
+    <Size>24</Size>
+
+    <!--
+    Spacing is a float value, measured in pixels. Modify this value to change
+    the amount of spacing in between characters.
+    -->
+    <Spacing>0</Spacing>
+
+    <!--
+    UseKerning controls the layout of the font. If this value is true, kerning information
+    will be used when placing characters.
+    -->
+    <UseKerning>true</UseKerning>
+
+    <!--
+    Style controls the style of the font. Valid entries are "Regular", "Bold", "Italic",
+    and "Bold, Italic", and are case sensitive.
+    -->
+    <Style>Bold</Style>
+
+    <!--
+    If you uncomment this line, the default character will be substituted if you draw
+    or measure text that contains characters which were not included in the font.
+    -->
+    <!-- <DefaultCharacter>*</DefaultCharacter> -->
+
+    <!--
+    CharacterRegions control what letters are available in the font. Every
+    character from Start to End will be built and made available for drawing. The
+    default range is from 32, (ASCII space), to 126, ('~'), covering the basic Latin
+    character set. The characters are ordered according to the Unicode standard.
+    See the documentation for more information.
+    -->
+    <CharacterRegions>
+      <!-- Basic Latin and Extended Latin -->
+      <CharacterRegion>
+        <Start>&#32;</Start>
+        <End>&#255;</End>
+      </CharacterRegion>
+      <!-- Currency symbols -->
+      <CharacterRegion>
+        <Start>&#x20A0;</Start>
+        <End>&#x20CF;</End>
+      </CharacterRegion>
+      <!-- Hiragana (Japanese) -->
+      <CharacterRegion>
+        <Start>&#x3040;</Start>
+        <End>&#x309F;</End>
+      </CharacterRegion>
+      <!-- Katakana (Japanese) -->
+      <CharacterRegion>
+        <Start>&#x30A0;</Start>
+        <End>&#x30FF;</End>
+      </CharacterRegion>
+      <!-- CJK Unified Ideographs - Common subset for our translations -->
+      <!-- Range covers the most frequently used characters -->
+      <CharacterRegion>
+        <Start>&#x4E00;</Start>
+        <End>&#x4FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5000;</Start>
+        <End>&#x51FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5200;</Start>
+        <End>&#x53FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5400;</Start>
+        <End>&#x55FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5600;</Start>
+        <End>&#x57FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5800;</Start>
+        <End>&#x59FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5A00;</Start>
+        <End>&#x5BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5C00;</Start>
+        <End>&#x5DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5E00;</Start>
+        <End>&#x5FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6000;</Start>
+        <End>&#x61FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6200;</Start>
+        <End>&#x63FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6400;</Start>
+        <End>&#x65FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6600;</Start>
+        <End>&#x67FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6800;</Start>
+        <End>&#x69FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6A00;</Start>
+        <End>&#x6BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6C00;</Start>
+        <End>&#x6DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6E00;</Start>
+        <End>&#x6FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7000;</Start>
+        <End>&#x71FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7200;</Start>
+        <End>&#x73FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7400;</Start>
+        <End>&#x75FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7600;</Start>
+        <End>&#x77FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7800;</Start>
+        <End>&#x79FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7A00;</Start>
+        <End>&#x7BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7C00;</Start>
+        <End>&#x7DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7E00;</Start>
+        <End>&#x7FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8000;</Start>
+        <End>&#x81FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8200;</Start>
+        <End>&#x83FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8400;</Start>
+        <End>&#x85FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8600;</Start>
+        <End>&#x87FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8800;</Start>
+        <End>&#x89FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8A00;</Start>
+        <End>&#x8BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8C00;</Start>
+        <End>&#x8DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8E00;</Start>
+        <End>&#x8FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9000;</Start>
+        <End>&#x91FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9200;</Start>
+        <End>&#x93FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9400;</Start>
+        <End>&#x95FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9600;</Start>
+        <End>&#x97FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9800;</Start>
+        <End>&#x99FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9A00;</Start>
+        <End>&#x9BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9C00;</Start>
+        <End>&#x9DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9E00;</Start>
+        <End>&#x9FFF;</End>
+      </CharacterRegion>
+    </CharacterRegions>
+  </Asset>
+</XnaContent>

+ 14 - 3
CardsStarterKit/Core/Content/Fonts/Regular.spritefont

@@ -10,9 +10,10 @@ with.
 
     <!--
     Modify this string to change the font that will be imported.
+    Using Arial Unicode for better currency symbol support
     -->
-    <FontName>Arial</FontName>
-
+    <FontName>Arial Unicode</FontName>
+    
     <!--
     Size is a float value, measured in points. Modify this value to change
     the size of the font.
@@ -53,7 +54,17 @@ with.
     <CharacterRegions>
       <CharacterRegion>
         <Start>&#32;</Start>
-        <End>&#126;</End>
+        <End>&#255;</End>
+      </CharacterRegion>
+      <!-- Cyrillic characters for Russian support -->
+      <CharacterRegion>
+        <Start>&#x0400;</Start>
+        <End>&#x04FF;</End>
+      </CharacterRegion>
+      <!-- Currency symbols (includes €, £, ¥, ₽, etc.) -->
+      <CharacterRegion>
+        <Start>&#x20A0;</Start>
+        <End>&#x20CF;</End>
       </CharacterRegion>
     </CharacterRegions>
   </Asset>

+ 241 - 0
CardsStarterKit/Core/Content/Fonts/Regular_CJK.spritefont

@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+CJK (Chinese, Japanese, Korean) font variant using Arial Unicode
+Includes optimized character ranges for Japanese and Chinese translations
+-->
+<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
+  <Asset Type="Graphics:FontDescription">
+
+    <!--
+    Modify this string to change the font that will be imported.
+    Using Arial Unicode for better currency symbol support
+    -->
+    <FontName>Arial Unicode</FontName>
+    
+    <!--
+    Size is a float value, measured in points. Modify this value to change
+    the size of the font.
+    -->
+    <Size>20</Size>
+
+    <!--
+    Spacing is a float value, measured in pixels. Modify this value to change
+    the amount of spacing in between characters.
+    -->
+    <Spacing>0</Spacing>
+
+    <!--
+    UseKerning controls the layout of the font. If this value is true, kerning information
+    will be used when placing characters.
+    -->
+    <UseKerning>true</UseKerning>
+
+    <!--
+    Style controls the style of the font. Valid entries are "Regular", "Bold", "Italic",
+    and "Bold, Italic", and are case sensitive.
+    -->
+    <Style>Regular</Style>
+
+    <!--
+    If you uncomment this line, the default character will be substituted if you draw
+    or measure text that contains characters which were not included in the font.
+    -->
+    <!-- <DefaultCharacter>*</DefaultCharacter> -->
+
+    <!--
+    CharacterRegions control what letters are available in the font. Every
+    character from Start to End will be built and made available for drawing. The
+    default range is from 32, (ASCII space), to 126, ('~'), covering the basic Latin
+    character set. The characters are ordered according to the Unicode standard.
+    See the documentation for more information.
+    -->
+    <CharacterRegions>
+      <!-- Basic Latin and Extended Latin -->
+      <CharacterRegion>
+        <Start>&#32;</Start>
+        <End>&#255;</End>
+      </CharacterRegion>
+      <!-- Currency symbols -->
+      <CharacterRegion>
+        <Start>&#x20A0;</Start>
+        <End>&#x20CF;</End>
+      </CharacterRegion>
+      <!-- Hiragana (Japanese) -->
+      <CharacterRegion>
+        <Start>&#x3040;</Start>
+        <End>&#x309F;</End>
+      </CharacterRegion>
+      <!-- Katakana (Japanese) -->
+      <CharacterRegion>
+        <Start>&#x30A0;</Start>
+        <End>&#x30FF;</End>
+      </CharacterRegion>
+      <!-- CJK Unified Ideographs - Common subset for our translations -->
+      <!-- Range covers the most frequently used characters -->
+      <CharacterRegion>
+        <Start>&#x4E00;</Start>
+        <End>&#x4FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5000;</Start>
+        <End>&#x51FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5200;</Start>
+        <End>&#x53FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5400;</Start>
+        <End>&#x55FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5600;</Start>
+        <End>&#x57FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5800;</Start>
+        <End>&#x59FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5A00;</Start>
+        <End>&#x5BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5C00;</Start>
+        <End>&#x5DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x5E00;</Start>
+        <End>&#x5FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6000;</Start>
+        <End>&#x61FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6200;</Start>
+        <End>&#x63FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6400;</Start>
+        <End>&#x65FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6600;</Start>
+        <End>&#x67FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6800;</Start>
+        <End>&#x69FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6A00;</Start>
+        <End>&#x6BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6C00;</Start>
+        <End>&#x6DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x6E00;</Start>
+        <End>&#x6FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7000;</Start>
+        <End>&#x71FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7200;</Start>
+        <End>&#x73FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7400;</Start>
+        <End>&#x75FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7600;</Start>
+        <End>&#x77FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7800;</Start>
+        <End>&#x79FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7A00;</Start>
+        <End>&#x7BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7C00;</Start>
+        <End>&#x7DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x7E00;</Start>
+        <End>&#x7FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8000;</Start>
+        <End>&#x81FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8200;</Start>
+        <End>&#x83FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8400;</Start>
+        <End>&#x85FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8600;</Start>
+        <End>&#x87FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8800;</Start>
+        <End>&#x89FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8A00;</Start>
+        <End>&#x8BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8C00;</Start>
+        <End>&#x8DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x8E00;</Start>
+        <End>&#x8FFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9000;</Start>
+        <End>&#x91FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9200;</Start>
+        <End>&#x93FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9400;</Start>
+        <End>&#x95FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9600;</Start>
+        <End>&#x97FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9800;</Start>
+        <End>&#x99FF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9A00;</Start>
+        <End>&#x9BFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9C00;</Start>
+        <End>&#x9DFF;</End>
+      </CharacterRegion>
+      <CharacterRegion>
+        <Start>&#x9E00;</Start>
+        <End>&#x9FFF;</End>
+      </CharacterRegion>
+    </CharacterRegions>
+  </Asset>
+</XnaContent>

BIN
CardsStarterKit/Core/Content/Images/UI/gradient.png


+ 2 - 1
CardsStarterKit/Core/Game/Blackjack.Core.csproj

@@ -14,9 +14,10 @@
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.*" PrivateAssets="all"/>
+    <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.*" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\Framework\Cards.Framework.csproj" />
+    <ProjectReference Include="..\..\..\MonoGame.Xna.Framework.Net\MonoGame.Xna.Framework.Net.csproj" />
   </ItemGroup>
 </Project>

File diff suppressed because it is too large
+ 556 - 117
CardsStarterKit/Core/Game/Blackjack/Game/BlackjackCardGame.cs


+ 24 - 0
CardsStarterKit/Core/Game/Blackjack/Game/BlackjackConstants.cs

@@ -0,0 +1,24 @@
+namespace Blackjack
+{
+    public static class BlackjackConstants
+    {
+        public const int MaxPlayers = 7;
+        public const int MinPlayers = 1;
+
+        /// <summary>
+        /// Default AI player names used to fill empty player slots.
+        /// Now uses localized names from Resources.
+        /// </summary>
+        public static string[] DefaultAINames => new string[]
+        {
+            Resources.AIPlayer1,
+            Resources.AIPlayer2,
+            Resources.AIPlayer3,
+            Resources.AIPlayer4,
+            Resources.AIPlayer5,
+            Resources.AIPlayer6
+        };
+
+        // Add other game logic constants here as needed
+    }
+}

+ 655 - 56
CardsStarterKit/Core/Game/Blackjack/Misc/BetGameComponent.cs

@@ -33,14 +33,24 @@ namespace Blackjack
 
         bool isKeyDown = false;
 
+        // In network games, this specifies which player index the local user controls
+        public int LocalPlayerIndex { get; set; } = -1;
+
+        /// <summary>
+        /// Gets the height of the chip texture (once loaded)
+        /// </summary>
+        public int ChipHeight => blankChip?.Height ?? 50; // Default to 50 if not loaded yet
+
         Button bet;
         Button clear;
 
         Vector2 ChipOffset { get; set; }
-        static float insuranceYPosition = 120;
-        static Vector2 secondHandOffset = new Vector2(25, 30);
+        float insuranceYPosition;
+        Vector2 secondHandOffset;
 
         List<AnimatedGameComponent> currentChipComponent = new List<AnimatedGameComponent>();
+        // Track all chip components for each player (for win/loss animations)
+        Dictionary<int, List<AnimatedGameComponent>> playerChipComponents = new Dictionary<int, List<AnimatedGameComponent>>();
         int currentBet = 0;
         InputState input;
         InputHelper inputHelper;
@@ -66,6 +76,22 @@ namespace Blackjack
             this.spriteBatch = spriteBatch;
             this.globalTransformation = globalTransformation;
             chipsAssets = new Dictionary<int, Texture2D>();
+
+            // Calculate proportional values based on screen size
+            var blackjackGame = cardGame as BlackjackCardGame;
+            if (blackjackGame != null)
+            {
+                int screenWidth = blackjackGame.ScreenManager.SafeArea.Width;
+                int screenHeight = blackjackGame.ScreenManager.SafeArea.Height;
+                secondHandOffset = UIConstants.GetBetSecondHandOffset(screenWidth, screenHeight);
+                insuranceYPosition = UIConstants.GetInsuranceYPosition(screenHeight);
+            }
+            else
+            {
+                // Fallback to default values
+                secondHandOffset = new Vector2(25, 30);
+                insuranceYPosition = 120;
+            }
         }
 
 
@@ -101,40 +127,102 @@ namespace Blackjack
             int buttonHeight = UIConstants.GetButtonHeight(bounds.Height);
             int buttonSpacing = UIConstants.GetButtonSpacing(bounds.Width);
 
-            // Position chip buttons higher to avoid overlap with Deal/Clear buttons
-            // Place them above the buttons with extra spacing
-            int chipAreaBottomMargin = buttonHeight + (smallPadding * 3); // Space for buttons + gap
-            positions[chipsAssets.Count - 1] = new Vector2(bounds.Left + smallPadding,
-                bounds.Bottom - size.Height - chipSpacing - chipAreaBottomMargin);
-            for (int chipIndex = 2; chipIndex <= chipsAssets.Count; chipIndex++)
+            // Calculate total width of all chips side by side
+            int totalChipWidth = 0;
+            for (int i = 0; i < assetNames.Length; i++)
+            {
+                totalChipWidth += chipsAssets[assetNames[i]].Bounds.Width;
+            }
+            totalChipWidth += smallPadding * (assetNames.Length - 1); // Spacing between chips
+
+            // Position chips horizontally centered at bottom
+            int startX = (bounds.Width - totalChipWidth) / 2;
+            int chipY = bounds.Bottom - size.Height - smallPadding;
+
+            positions[0] = new Vector2(startX, chipY);
+            for (int chipIndex = 1; chipIndex < chipsAssets.Count; chipIndex++)
             {
-                size = chipsAssets[assetNames[chipsAssets.Count - chipIndex]].Bounds;
-                positions[chipsAssets.Count - chipIndex] = positions[chipsAssets.Count - (chipIndex - 1)] -
-                    new Vector2(0, size.Height + smallPadding);
+                int prevChipWidth = chipsAssets[assetNames[chipIndex - 1]].Bounds.Width;
+                positions[chipIndex] = positions[chipIndex - 1] + new Vector2(prevChipWidth + smallPadding, 0);
             }
 
-            // Initialize bet button
+            // Calculate button Y position using shared helper for consistency
+            int buttonY = UIConstants.GetGameplayButtonYPosition(size.Height, bounds.Width, bounds.Height);
+
+            // Calculate total width of both buttons side by side
+            int totalButtonWidth = (buttonWidth * 2) + smallPadding;
+            int buttonStartX = (bounds.Width - totalButtonWidth) / 2;
+
+            // Initialize bet button (centered above chips)
             bet = new Button("ButtonRegular", "ButtonPressed", input, cardGame, spriteBatch, globalTransformation)
             {
-                Bounds = new Rectangle(bounds.Left + smallPadding, bounds.Bottom - buttonHeight - smallPadding, buttonWidth, buttonHeight),
+                Bounds = new Rectangle(buttonStartX, buttonY, buttonWidth, buttonHeight),
                 Font = cardGame.Font,
-                Text = "Deal",
+                Text = Resources.Deal,
             };
             bet.Click += Bet_Click;
             Game.Components.Add(bet);
 
-            // Initialize clear button
+            // Initialize clear button (to the right of bet button)
             clear = new Button("ButtonRegular", "ButtonPressed", input, cardGame, spriteBatch, globalTransformation)
             {
-                Bounds = new Rectangle(bounds.Left + smallPadding + buttonWidth + smallPadding, bounds.Bottom - buttonHeight - smallPadding, buttonWidth, buttonHeight),
+                Bounds = new Rectangle(buttonStartX + buttonWidth + smallPadding, buttonY, buttonWidth, buttonHeight),
                 Font = cardGame.Font,
-                Text = "Clear",
+                Text = Resources.Clear,
             };
             clear.Click += Clear_Click;
             Game.Components.Add(clear);
             ShowAndEnableButtons(false);
         }
 
+        /// <summary>
+        /// Updates button text and fonts after language change to match current Resources
+        /// Also resizes buttons to fit the new text
+        /// </summary>
+        public void UpdateButtonText()
+        {
+            int screenWidth = ScreenManager.BASE_BUFFER_WIDTH;
+            int minButtonWidth = UIConstants.GetButtonWidth(screenWidth);
+
+            if (bet != null)
+            {
+                bet.Text = Resources.Deal;
+                bet.Font = cardGame.Font;
+
+                // Calculate new width based on text using centralized UIConstants method
+                int newWidth = UIConstants.CalculateButtonWidth(Resources.Deal, cardGame.Font, minButtonWidth, screenWidth);
+                bet.Bounds = new Rectangle(bet.Bounds.X, bet.Bounds.Y, newWidth, bet.Bounds.Height);
+            }
+            if (clear != null)
+            {
+                clear.Text = Resources.Clear;
+                clear.Font = cardGame.Font;
+
+                // Calculate new width based on text using centralized UIConstants method
+                int newWidth = UIConstants.CalculateButtonWidth(Resources.Clear, cardGame.Font, minButtonWidth, screenWidth);
+                clear.Bounds = new Rectangle(clear.Bounds.X, clear.Bounds.Y, newWidth, clear.Bounds.Height);
+            }
+
+            // Reposition buttons to maintain proper spacing
+            RepositionBetButtons();
+        }
+
+        /// <summary>
+        /// Repositions bet buttons to maintain proper spacing after width changes
+        /// </summary>
+        private void RepositionBetButtons()
+        {
+            if (bet == null || clear == null)
+                return;
+
+            int smallPadding = UIConstants.GetSmallPadding(ScreenManager.BASE_BUFFER_WIDTH);
+            int totalWidth = bet.Bounds.Width + clear.Bounds.Width + smallPadding;
+            int startX = (ScreenManager.BASE_BUFFER_WIDTH - totalWidth) / 2;
+
+            bet.Bounds = new Rectangle(startX, bet.Bounds.Y, bet.Bounds.Width, bet.Bounds.Height);
+            clear.Bounds = new Rectangle(startX + bet.Bounds.Width + smallPadding, clear.Bounds.Y, clear.Bounds.Width, clear.Bounds.Height);
+        }
+
         /// <summary>
         /// Load component content.
         /// </summary>
@@ -176,14 +264,30 @@ namespace Blackjack
                     {
                         ShowAndEnableButtons(false);
                         int bet = ((BlackjackAIPlayer)player).AIBet();
-                        if (bet == 0)
+
+                        BlackjackCardGame blackjackGame = cardGame as BlackjackCardGame;
+
+                        if (bet > 0)
                         {
-                            Bet_Click(this, EventArgs.Empty);
+                            AddChip(playerIndex, bet, false);
                         }
                         else
                         {
-                            AddChip(playerIndex, bet, false);
+                            // Show that the player has passed on this round
+                            blackjackGame?.ShowPlayerPass(playerIndex);
                         }
+
+                        // Mark AI player as done betting
+                        player.IsDoneBetting = true;
+
+                        // Broadcast the AI's bet to network
+                        if (blackjackGame != null && blackjackGame.IsNetworkGame && blackjackGame.IsHost)
+                        {
+                            blackjackGame.BroadcastBetPlaced((byte)playerIndex, bet);
+                        }
+
+                        currentChipComponent.Clear();
+                        currentBet = 0;
                     }
                     else
                     {
@@ -237,40 +341,50 @@ namespace Blackjack
         /// <param name="mouseState">Mouse input information.</param>
         private void HandleInput()
         {
-            bool isPressed = false;
+            bool isClicked = false;
             Vector2 position = Vector2.Zero;
 
-            // Check for tap gestures
+            // Check for tap gestures (touch release)
             if (input.Gestures.Count > 0 && input.Gestures[0].GestureType == GestureType.Tap)
             {
-                isPressed = true;
+                isClicked = true;
                 position = input.Gestures[0].Position;
             }
 
-            // Check for mouse input
-            if (input.CurrentMouseState.LeftButton == ButtonState.Pressed)
+            // Check for mouse click (button was pressed last frame, released this frame)
+            bool wasPressed = input.LastMouseState.LeftButton == ButtonState.Pressed;
+            bool isReleased = input.CurrentMouseState.LeftButton == ButtonState.Released;
+
+            if (wasPressed && isReleased)
             {
-                isPressed = true;
+                isClicked = true;
                 position = new Vector2(input.CurrentMouseState.X, input.CurrentMouseState.Y);
             }
 
-            // Handle chip interaction logic
-            if (isPressed)
+            // Handle chip interaction logic only on click/tap completion
+            if (isClicked)
             {
-                if (!isKeyDown)
+                int chipValue = GetIntersectingChipValue(position);
+                if (chipValue != 0)
                 {
-                    int chipValue = GetIntersectingChipValue(position);
-                    if (chipValue != 0)
+                    // Determine which player should receive the chip
+                    int targetPlayerIndex;
+                    BlackjackCardGame blackjackGame = cardGame as BlackjackCardGame;
+
+                    if (blackjackGame != null && blackjackGame.IsNetworkGame && LocalPlayerIndex >= 0)
+                    {
+                        // In network games, always bet for the local player
+                        targetPlayerIndex = LocalPlayerIndex;
+                    }
+                    else
                     {
-                        AddChip(GetCurrentPlayer(), chipValue, false);
+                        // In single-player, bet for the current player
+                        targetPlayerIndex = GetCurrentPlayer();
                     }
-                    isKeyDown = true;
+
+                    AddChip(targetPlayerIndex, chipValue, false);
                 }
             }
-            else
-            {
-                isKeyDown = false;
-            }
         }
 
         /// <summary>
@@ -318,17 +432,55 @@ namespace Blackjack
 
             BlackjackPlayer player;
 
-            // Draws the player balance and bet amount
+            // Draws the player balance, bet amount, and names below chip circles
             for (int playerIndex = 0; playerIndex < players.Count; playerIndex++)
             {
                 BlackJackTable table = (BlackJackTable)cardGame.GameTable;
-                Vector2 position = table[playerIndex] + table.RingOffset +
-                    new Vector2(table.RingTexture.Bounds.Width, 0);
+
+                // Account for scaled ring texture
+                float ringScale = UIConstants.GetChipScale();
+                float scaledRingHeight = table.RingTexture.Bounds.Height * ringScale;
+
+                // Position text below the chip circle center
+                // RingOffset puts us at the circle center, so add half the scaled height to get to bottom
+                Vector2 basePosition = table[playerIndex] + table.RingOffset +
+                    new Vector2(0, scaledRingHeight / 2f + 10); // 10px padding below circle
+
                 player = (BlackjackPlayer)players[playerIndex];
-                spriteBatch.DrawString(cardGame.Font, "$" + player.BetAmount.ToString(),
-                    position, Color.White);
-                spriteBatch.DrawString(cardGame.Font, "$" + player.Balance.ToString(),
-                    position + new Vector2(0, 30), Color.White);
+
+                // Draw bet amount (top line)
+                spriteBatch.DrawString(cardGame.Font, GameSettings.Instance.Currency + player.BetAmount.ToString(),
+                    basePosition, Color.White, 0f, Vector2.Zero, 0.75f, SpriteEffects.None, 0f);
+
+                // Draw balance (second line)
+                spriteBatch.DrawString(cardGame.Font, GameSettings.Instance.Currency + player.Balance.ToString(),
+                    basePosition + new Vector2(0, 20), Color.White, 0f, Vector2.Zero, 0.75f, SpriteEffects.None, 0f);
+
+                // Draw player name with AI indicator (bottom line)
+                string playerName = player.Name;
+                bool isAI = player is BlackjackAIPlayer;
+                Color nameColor = isAI ? Color.Yellow : Color.Cyan; // Yellow for AI, Cyan for human
+
+                // Strip GUID suffix from human player names for display (but keep full name for network identification)
+                string displayName = CardsFramework.UIUtility.StripGuidSuffix(playerName);
+
+                // Add (AI) suffix for AI players
+                if (isAI)
+                {
+                    displayName = $"{displayName} (AI)";
+                }
+                // Add (Host) suffix for the first human player in network games
+                else if (cardGame is BlackjackCardGame blackjackGame &&
+                         blackjackGame.IsNetworkGame &&
+                         blackjackGame.NetworkSession != null &&
+                         playerIndex == 0)
+                {
+                    displayName = $"{displayName} (Host)";
+                    nameColor = Color.LightGreen; // Distinct color for host
+                }
+
+                spriteBatch.DrawString(cardGame.Font, displayName,
+                    basePosition + new Vector2(0, 40), nameColor, 0f, Vector2.Zero, 0.70f, SpriteEffects.None, 0f);
             }
 
             spriteBatch.End();
@@ -344,7 +496,9 @@ namespace Blackjack
         /// <param name="chipValue">The value on the chip to add.</param>
         /// <param name="secondHand">True if this chip is added to the chip pile
         /// belonging to the player's second hand.</param>
-        public void AddChip(int playerIndex, int chipValue, bool secondHand)
+        /// <param name="sendToNetwork">Whether to send this chip addition over the network. 
+        /// Set to false when receiving chip additions from the network to avoid loops.</param>
+        public void AddChip(int playerIndex, int chipValue, bool secondHand, bool sendToNetwork = true)
         {
             // Only add the chip if the bet is successfully performed
             if (((BlackjackPlayer)players[playerIndex]).Bet(chipValue))
@@ -364,8 +518,10 @@ namespace Blackjack
                 // Get the proper offset according to the platform (pc, phone, xbox)
                 Vector2 offset = GetChipOffset(playerIndex, secondHand);
 
+                // Stack chips slightly offset but keep them centered in the ring
+                // Use smaller offsets to prevent drifting from center
                 position = cardGame.GameTable[playerIndex] + offset +
-                    new Vector2(-currentChipComponent.Count * 2, currentChipComponent.Count * 1);
+                    new Vector2(-currentChipComponent.Count * 1, currentChipComponent.Count * 0.5f);
 
 
                 // Find the index of the chip
@@ -397,6 +553,32 @@ namespace Blackjack
                 });
 
                 currentChipComponent.Add(chipComponent);
+
+                // Track chip for this player (for win/loss animations)
+                if (!playerChipComponents.ContainsKey(playerIndex))
+                {
+                    playerChipComponents[playerIndex] = new List<AnimatedGameComponent>();
+                }
+                playerChipComponents[playerIndex].Add(chipComponent);
+
+                // Send chip addition to network in real-time (if enabled)
+                if (sendToNetwork)
+                {
+                    BlackjackCardGame blackjackGame = cardGame as BlackjackCardGame;
+                    if (blackjackGame != null && blackjackGame.IsNetworkGame)
+                    {
+                        if (blackjackGame.IsHost)
+                        {
+                            // Host broadcasts the chip addition to all clients
+                            blackjackGame.BroadcastChipAdded((byte)playerIndex, chipValue);
+                        }
+                        else
+                        {
+                            // Client sends their chip addition to the host
+                            blackjackGame.SendChipAdded((byte)playerIndex, chipValue);
+                        }
+                    }
+                }
             }
         }
 
@@ -450,7 +632,96 @@ namespace Blackjack
 
         /// <summary>
         /// Updates the balance of all players in light of their bets and the dealer's
-        /// hand.
+        /// hand. WITH ANIMATIONS - triggers chip animations sequentially for each player.
+        /// </summary>
+        /// <param name="dealerPlayer">Player object representing the dealer.</param>
+        /// <param name="completionCallback">Called when all animations complete.</param>
+        public void CalculateBalanceWithAnimations(BlackjackPlayer dealerPlayer, Action completionCallback = null)
+        {
+            // Process players sequentially with animations
+            ProcessPlayerAnimationsSequentially(dealerPlayer, 0, completionCallback);
+        }
+
+        /// <summary>
+        /// Recursively processes player animations one at a time.
+        /// </summary>
+        private void ProcessPlayerAnimationsSequentially(BlackjackPlayer dealerPlayer, int playerIndex, Action completionCallback)
+        {
+            if (playerIndex >= players.Count)
+            {
+                // All players processed
+                completionCallback?.Invoke();
+                return;
+            }
+
+            BlackjackPlayer player = (BlackjackPlayer)players[playerIndex];
+
+            // Calculate factors for this player
+            float factor = CalculateFactorForHand(dealerPlayer, player, HandTypes.First);
+            float totalWinAmount = 0;
+
+            if (player.IsSplit)
+            {
+                float factor2 = CalculateFactorForHand(dealerPlayer, player, HandTypes.Second);
+                float initialBet = player.BetAmount / ((player.Double ? 2f : 1f) + (player.SecondDouble ? 2f : 1f));
+                float bet1 = initialBet * (player.Double ? 2f : 1f);
+                float bet2 = initialBet * (player.SecondDouble ? 2f : 1f);
+
+                totalWinAmount = bet1 * factor + bet2 * factor2;
+
+                if (player.IsInsurance && dealerPlayer.BlackJack)
+                {
+                    totalWinAmount += initialBet;
+                }
+            }
+            else
+            {
+                if (player.IsInsurance && dealerPlayer.BlackJack)
+                {
+                    totalWinAmount = player.BetAmount + (player.BetAmount * factor);
+                }
+                else
+                {
+                    totalWinAmount = player.BetAmount * factor;
+                }
+            }
+
+            // Determine animation type based on result
+            if (factor > 0)
+            {
+                // Win - chips return to selector and balance increases
+                AnimateWinningChips(playerIndex, totalWinAmount, () =>
+                {
+                    player.ClearBet();
+                    // Move to next player
+                    ProcessPlayerAnimationsSequentially(dealerPlayer, playerIndex + 1, completionCallback);
+                });
+            }
+            else if (factor < 0)
+            {
+                // Loss - chips go to dealer and balance decreases (totalWinAmount is negative)
+                AnimateLosingChips(playerIndex, totalWinAmount, () =>
+                {
+                    player.ClearBet();
+                    // Move to next player
+                    ProcessPlayerAnimationsSequentially(dealerPlayer, playerIndex + 1, completionCallback);
+                });
+            }
+            else
+            {
+                // Push - chips return to selector (no balance change)
+                AnimateWinningChips(playerIndex, 0, () =>
+                {
+                    player.ClearBet();
+                    // Move to next player
+                    ProcessPlayerAnimationsSequentially(dealerPlayer, playerIndex + 1, completionCallback);
+                });
+            }
+        }
+
+        /// <summary>
+        /// Updates the balance of all players in light of their bets and the dealer's
+        /// hand. (LEGACY - no animations, instant update)
         /// </summary>
         /// <param name="dealerPlayer">Player object representing the dealer.</param>
         public void CalculateBalance(BlackjackPlayer dealerPlayer)
@@ -501,6 +772,291 @@ namespace Blackjack
             }
         }
 
+        /// <summary>
+        /// Animates winning chips returning to the chip selector and updates player balance incrementally.
+        /// For local players, chips fly to selector. For remote/AI players, chips fly to name text.
+        /// </summary>
+        /// <param name="playerIndex">Index of the player who won.</param>
+        /// <param name="winAmount">Amount won (used to determine which chips to animate).</param>
+        /// <param name="callback">Callback to invoke when all animations complete.</param>
+        public void AnimateWinningChips(int playerIndex, float winAmount, Action callback = null)
+        {
+            if (!playerChipComponents.ContainsKey(playerIndex) || playerChipComponents[playerIndex].Count == 0)
+            {
+                callback?.Invoke();
+                return;
+            }
+
+            BlackjackPlayer player = (BlackjackPlayer)players[playerIndex];
+            bool isLocal = IsLocalPlayer(playerIndex);
+
+            if (isLocal)
+            {
+                // Local player: chips fly to selector positions
+                AnimateChipsToSelector(playerIndex, winAmount, callback);
+            }
+            else
+            {
+                // Remote/AI player: chips fly to name text and disappear
+                AnimateChipsToNameText(playerIndex, winAmount, callback);
+            }
+        }
+
+        /// <summary>
+        /// Animates chips to the chip selector (for local players).
+        /// </summary>
+        private void AnimateChipsToSelector(int playerIndex, float winAmount, Action callback = null)
+        {
+            BlackjackPlayer player = (BlackjackPlayer)players[playerIndex];
+            var chips = playerChipComponents[playerIndex];
+
+            // Animate chips one by one back to their selector positions
+            TimeSpan delayBetweenChips = TimeSpan.FromMilliseconds(100);
+            DateTime startTime = DateTime.Now;
+
+            for (int i = 0; i < chips.Count; i++)
+            {
+                var chip = chips[i];
+
+                // Determine which chip value this is based on texture
+                int chipValue = GetChipValueFromComponent(chip);
+                int chipIndex = GetChipSelectorIndex(chipValue);
+
+                if (chipIndex >= 0 && chipIndex < positions.Length)
+                {
+                    bool isLastChip = (i == chips.Count - 1);
+
+                    // Animate chip back to selector position
+                    chip.AddAnimation(new TransitionGameComponentAnimation(
+                        chip.CurrentPosition, positions[chipIndex])
+                    {
+                        Duration = TimeSpan.FromSeconds(0.5),
+                        StartTime = startTime + (delayBetweenChips * i),
+                        PerformWhenDone = isLastChip ? (object obj) =>
+                        {
+                            PlayBetSound(obj);
+                            // Update balance after last chip arrives
+                            player.Balance += winAmount;
+                            SavePlayerBalanceIfNeeded(playerIndex);
+                            // Clean up all chips
+                            CleanupPlayerChips(playerIndex);
+                            callback?.Invoke();
+                        }
+                        : PlayBetSound
+                    });
+                }
+            }
+        }
+
+        /// <summary>
+        /// Animates chips to the player's name text position with shrink/fade effect (for remote/AI players).
+        /// </summary>
+        private void AnimateChipsToNameText(int playerIndex, float winAmount, Action callback = null)
+        {
+            BlackjackPlayer player = (BlackjackPlayer)players[playerIndex];
+            var chips = playerChipComponents[playerIndex];
+
+            Vector2 targetPosition = GetPlayerNameTextPosition(playerIndex);
+
+            // Animate chips one by one to name text
+            TimeSpan delayBetweenChips = TimeSpan.FromMilliseconds(100);
+            DateTime startTime = DateTime.Now;
+
+            for (int i = 0; i < chips.Count; i++)
+            {
+                var chip = chips[i];
+                bool isLastChip = (i == chips.Count - 1);
+
+                // Animate chip to name position with fade/shrink
+                chip.AddAnimation(new TransitionGameComponentAnimation(
+                    chip.CurrentPosition, targetPosition)
+                {
+                    Duration = TimeSpan.FromSeconds(0.5),
+                    StartTime = startTime + (delayBetweenChips * i),
+                    PerformWhenDone = (object obj) =>
+                    {
+                        PlayBetSound(obj);
+
+                        if (isLastChip)
+                        {
+                            // Update balance when last chip arrives
+                            player.Balance += winAmount;
+                            SavePlayerBalanceIfNeeded(playerIndex);
+                            // Clean up all chips
+                            CleanupPlayerChips(playerIndex);
+                            callback?.Invoke();
+                        }
+                    }
+                });
+
+                // Add scale animation to shrink chip as it arrives (from 1.0 to 0.0)
+                chip.AddAnimation(new ScaleGameComponentAnimation(1.0f, 0.0f)
+                {
+                    Duration = TimeSpan.FromSeconds(0.5),
+                    StartTime = startTime + (delayBetweenChips * i)
+                });
+            }
+        }
+
+        /// <summary>
+        /// Animates losing chips moving to the dealer position and deducts the loss from player balance.
+        /// </summary>
+        /// <param name="playerIndex">Index of the player who lost.</param>
+        /// <param name="lossAmount">The negative amount to subtract from balance (e.g., -50 means lose $50).</param>
+        /// <param name="callback">Callback to invoke when all animations complete.</param>
+        public void AnimateLosingChips(int playerIndex, float lossAmount, Action callback = null)
+        {
+            if (!playerChipComponents.ContainsKey(playerIndex) || playerChipComponents[playerIndex].Count == 0)
+            {
+                callback?.Invoke();
+                return;
+            }
+
+            BlackjackPlayer player = (BlackjackPlayer)players[playerIndex];
+            var chips = playerChipComponents[playerIndex];
+
+            // Get dealer position (center top of screen)
+            Vector2 dealerPosition = cardGame.GameTable.DealerPosition;
+
+            // Animate all chips simultaneously to dealer
+            TimeSpan delayBetweenChips = TimeSpan.FromMilliseconds(50);
+            DateTime startTime = DateTime.Now;
+
+            for (int i = 0; i < chips.Count; i++)
+            {
+                var chip = chips[i];
+                bool isLastChip = (i == chips.Count - 1);
+
+                chip.AddAnimation(new TransitionGameComponentAnimation(
+                    chip.CurrentPosition, dealerPosition)
+                {
+                    Duration = TimeSpan.FromSeconds(0.6),
+                    StartTime = startTime + (delayBetweenChips * i),
+                    PerformWhenDone = isLastChip ? (object obj) =>
+                    {
+                        // Deduct loss from balance when last chip reaches dealer (lossAmount is negative)
+                        player.Balance += lossAmount;
+                        SavePlayerBalanceIfNeeded(playerIndex);
+                        // Clean up all chips after last one reaches dealer
+                        CleanupPlayerChips(playerIndex);
+                        callback?.Invoke();
+                    }
+                    : null
+                });
+            }
+        }
+
+        /// <summary>
+        /// Helper method to get chip value from its texture.
+        /// </summary>
+        private int GetChipValueFromTexture(Texture2D texture)
+        {
+            foreach (var kvp in chipsAssets)
+            {
+                if (kvp.Value == texture)
+                    return kvp.Key;
+            }
+            return 0;
+        }
+
+        /// <summary>
+        /// Helper method to get chip value from an AnimatedGameComponent.
+        /// </summary>
+        private int GetChipValueFromComponent(AnimatedGameComponent chipComponent)
+        {
+            return GetChipValueFromTexture(chipComponent.CurrentFrame);
+        }
+
+        /// <summary>
+        /// Helper method to get the index in the positions array for a chip value.
+        /// </summary>
+        private int GetChipSelectorIndex(int chipValue)
+        {
+            for (int i = 0; i < assetNames.Length; i++)
+            {
+                if (assetNames[i] == chipValue)
+                    return i;
+            }
+            return -1;
+        }
+
+        /// <summary>
+        /// Cleans up chip components for a specific player.
+        /// </summary>
+        private void CleanupPlayerChips(int playerIndex)
+        {
+            if (playerChipComponents.ContainsKey(playerIndex))
+            {
+                foreach (var chip in playerChipComponents[playerIndex])
+                {
+                    Game.Components.Remove(chip);
+                }
+                playerChipComponents[playerIndex].Clear();
+                playerChipComponents.Remove(playerIndex);
+            }
+        }
+
+        /// <summary>
+        /// Determines if a player is the local player (controlled by this machine).
+        /// </summary>
+        private bool IsLocalPlayer(int playerIndex)
+        {
+            BlackjackCardGame blackjackGame = cardGame as BlackjackCardGame;
+
+            if (blackjackGame != null && blackjackGame.IsNetworkGame && LocalPlayerIndex >= 0)
+            {
+                // In network games, only the LocalPlayerIndex player is local
+                return playerIndex == LocalPlayerIndex;
+            }
+            else
+            {
+                // In single-player games, player 0 is the local player
+                return playerIndex == 0;
+            }
+        }
+
+        /// <summary>
+        /// Saves the player balance to settings if persistent winnings is enabled
+        /// and this is a single-player (non-network) game.
+        /// </summary>
+        private void SavePlayerBalanceIfNeeded(int playerIndex)
+        {
+            BlackjackCardGame blackjackGame = cardGame as BlackjackCardGame;
+
+            // Only save in single-player games (not network games)
+            if (blackjackGame != null && !blackjackGame.IsNetworkGame &&
+                GameSettings.Instance.PersistWinnings && playerIndex == 0)
+            {
+                var player = players[playerIndex] as BlackjackPlayer;
+                if (player != null)
+                {
+                    GameSettings.Instance.SavedPlayerBalance = player.Balance;
+                    GameSettings.Save();
+                    System.Console.WriteLine($"[PersistWinnings] Saved balance: {player.Balance}");
+                }
+            }
+        }
+
+        /// <summary>
+        /// Calculates the position where player name text is rendered.
+        /// This is used as the target for chip animations for remote/AI players.
+        /// </summary>
+        private Vector2 GetPlayerNameTextPosition(int playerIndex)
+        {
+            BlackJackTable table = (BlackJackTable)cardGame.GameTable;
+
+            // Account for scaled ring texture
+            float ringScale = UIConstants.GetChipScale();
+            float scaledRingHeight = table.RingTexture.Bounds.Height * ringScale;
+
+            // Position text below the chip circle center (same as Draw method)
+            Vector2 basePosition = table[playerIndex] + table.RingOffset +
+                new Vector2(0, scaledRingHeight / 2f + 10); // 10px padding below circle
+
+            // Name is on the third line (40px below base position)
+            return basePosition + new Vector2(0, 40);
+        }
+
         /// <summary>
         /// Adds chips to a specified player in order to reach a specified bet amount.
         /// </summary>
@@ -595,9 +1151,14 @@ namespace Blackjack
             Vector2 offset = Vector2.Zero;
 
             BlackJackTable table = ((BlackJackTable)cardGame.GameTable);
-            offset = table.RingOffset +
-                new Vector2(table.RingTexture.Bounds.Width - blankChip.Bounds.Width,
-                    table.RingTexture.Bounds.Height - blankChip.Bounds.Height) / 2f;
+
+            // The ring is drawn with center origin, so we need to account for that
+            // Ring position is at the CENTER of the scaled ring texture
+            float ringScale = UIConstants.GetChipScale();
+
+            // Since ring is drawn from center, the offset should just center the chip
+            // within the ring (no need to calculate top-left position)
+            offset = table.RingOffset - new Vector2(blankChip.Bounds.Width / 2f, blankChip.Bounds.Height / 2f);
 
             if (secondHand == true)
             {
@@ -727,13 +1288,19 @@ namespace Blackjack
         void Clear_Click(object sender, EventArgs e)
         {
             // Clear current player chips from screen and resets his bet
+            int playerIndex = GetCurrentPlayer();
+            BlackjackPlayer player = (BlackjackPlayer)players[playerIndex];
+
             currentBet = 0;
-            ((BlackjackPlayer)players[GetCurrentPlayer()]).ClearBet();
-            for (int chipComponentIndex = 0; chipComponentIndex < currentChipComponent.Count; chipComponentIndex++)
+
+            // Animate chips returning to selector positions (reuse winning chip animation)
+            // Pass 0 for winAmount since we're just returning the bet, not adding winnings
+            AnimateChipsToSelector(playerIndex, 0, () =>
             {
-                Game.Components.Remove(currentChipComponent[chipComponentIndex]);
-            }
-            currentChipComponent.Clear();
+                // Clear the bet after animation completes
+                player.ClearBet();
+                currentChipComponent.Clear();
+            });
         }
 
         /// <summary>
@@ -745,13 +1312,45 @@ namespace Blackjack
         void Bet_Click(object sender, EventArgs e)
         {
             // Finish the bet
-            int playerIndex = GetCurrentPlayer();
+            int playerIndex;
+            BlackjackCardGame blackjackGame = cardGame as BlackjackCardGame;
+
+            if (blackjackGame != null && blackjackGame.IsNetworkGame && LocalPlayerIndex >= 0)
+            {
+                // In network games, mark the LOCAL player as done betting
+                playerIndex = LocalPlayerIndex;
+            }
+            else
+            {
+                // In single-player, use current player
+                playerIndex = GetCurrentPlayer();
+            }
+
+            int finalBetAmount = currentBet;
+
             // If the player did not bet, show that he has passed on this round
             if (currentBet == 0)
             {
                 ((BlackjackCardGame)cardGame).ShowPlayerPass(playerIndex);
             }
+
             ((BlackjackPlayer)players[playerIndex]).IsDoneBetting = true;
+
+            // Send/broadcast bet to network
+            if (blackjackGame != null && blackjackGame.IsNetworkGame)
+            {
+                if (blackjackGame.IsHost)
+                {
+                    // Host broadcasts the bet to all clients
+                    blackjackGame.BroadcastBetPlaced((byte)playerIndex, finalBetAmount);
+                }
+                else
+                {
+                    // Client sends their bet to the host
+                    blackjackGame.SendBetPlaced((byte)playerIndex, finalBetAmount);
+                }
+            }
+
             currentChipComponent.Clear();
             currentBet = 0;
         }

+ 44 - 0
CardsStarterKit/Core/Game/Blackjack/Networking/NetworkSerializationExtensions.cs

@@ -0,0 +1,44 @@
+using Microsoft.Xna.Framework.Net;
+using CardsFramework;
+
+namespace Blackjack.Networking
+{
+    public static class NetworkSerializationExtensions
+    {
+        public static void Write(this PacketWriter writer, TraditionalCard card)
+        {
+            writer.Write((byte)card.Type); // Suit (fits in byte - max 0x08)
+            writer.Write((ushort)card.Value); // Value (needs ushort - max 0x4000)
+        }
+
+        public static TraditionalCard ReadCard(this PacketReader reader)
+        {
+            var suit = (CardSuit)reader.ReadByte();
+            var value = (CardValue)reader.ReadUInt16();
+            return TraditionalCard.Create(suit, value);
+        }
+
+        public static void Write(this PacketWriter writer, Hand hand)
+        {
+            writer.Write(hand.Count);
+            for (int i = 0; i < hand.Count; i++)
+            {
+                writer.Write(hand[i]);
+            }
+        }
+
+        public static Hand ReadHand(this PacketReader reader)
+        {
+            var hand = new Hand();
+            int count = reader.ReadInt32();
+            for (int i = 0; i < count; i++)
+            {
+                // Add is internal, so this may need to be called from within the same assembly
+                var card = reader.ReadCard();
+                hand.GetType().GetMethod("Add", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
+                    .Invoke(hand, new object[] { card });
+            }
+            return hand;
+        }
+    }
+}

+ 34 - 0
CardsStarterKit/Core/Game/Blackjack/Networking/PacketTypes.cs

@@ -0,0 +1,34 @@
+namespace Blackjack.Networking
+{
+    public enum PacketType : byte
+    {
+        PlayerAction = 1,
+        BetPlaced = 2,
+        PlayerListSync = 3,
+        ChipAdded = 4,
+        GameStateUpdate = 10,
+        CardDealt = 11,
+        ShuffleSeed = 12,
+        RoundStarted = 13,
+        RoundEnded = 14,
+        TurnChanged = 15,
+        BalanceUpdate = 16,
+        BettingPhaseStarted = 17,
+        // Phase 5: Gameplay actions
+        HitAction = 20,
+        StandAction = 21,
+        DoubleAction = 22,
+        SplitAction = 23,
+        InsuranceAction = 24,
+    }
+
+    public enum BlackjackAction : byte
+    {
+        Hit = 1,
+        Stand = 2,
+        Double = 3,
+        Split = 4,
+        Insurance = 5,
+        Pass = 6
+    }
+}

+ 277 - 0
CardsStarterKit/Core/Game/Blackjack/Networking/Packets.cs

@@ -0,0 +1,277 @@
+using Microsoft.Xna.Framework.Net;
+using CardsFramework;
+using System.Collections.Generic;
+
+namespace Blackjack.Networking
+{
+    public class PlayerInfo
+    {
+        public string Name { get; set; }
+        public bool IsAI { get; set; }
+    }
+    
+    public class PlayerListSyncPacket
+    {
+        public List<PlayerInfo> Players { get; set; }
+        
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write((byte)Players.Count);
+            foreach (var player in Players)
+            {
+                writer.Write(player.Name);
+                writer.Write(player.IsAI);
+            }
+        }
+        
+        public static PlayerListSyncPacket Deserialize(PacketReader reader)
+        {
+            var packet = new PlayerListSyncPacket { Players = new List<PlayerInfo>() };
+            byte count = reader.ReadByte();
+            for (int i = 0; i < count; i++)
+            {
+                packet.Players.Add(new PlayerInfo
+                {
+                    Name = reader.ReadString(),
+                    IsAI = reader.ReadBoolean()
+                });
+            }
+            return packet;
+        }
+    }
+
+    public class PlayerActionPacket
+    {
+        public BlackjackAction Action { get; set; }
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write((byte)Action);
+        }
+        public static PlayerActionPacket Deserialize(PacketReader reader)
+        {
+            return new PlayerActionPacket
+            {
+                Action = (BlackjackAction)reader.ReadByte()
+            };
+        }
+    }
+
+    public class BetPlacedPacket
+    {
+        public byte PlayerIndex { get; set; }
+        public int BetAmount { get; set; }
+        
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(PlayerIndex);
+            writer.Write(BetAmount);
+        }
+        
+        public static BetPlacedPacket Deserialize(PacketReader reader)
+        {
+            return new BetPlacedPacket
+            {
+                PlayerIndex = reader.ReadByte(),
+                BetAmount = reader.ReadInt32()
+            };
+        }
+    }
+
+    public class ChipAddedPacket
+    {
+        public byte PlayerIndex { get; set; }
+        public int ChipValue { get; set; }
+        
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(PlayerIndex);
+            writer.Write(ChipValue);
+        }
+        
+        public static ChipAddedPacket Deserialize(PacketReader reader)
+        {
+            return new ChipAddedPacket
+            {
+                PlayerIndex = reader.ReadByte(),
+                ChipValue = reader.ReadInt32()
+            };
+        }
+    }
+
+    public class CardDealtPacket
+    {
+        public byte PlayerIndex { get; set; }
+        public TraditionalCard Card { get; set; }
+        public bool FaceDown { get; set; }
+        public HandTypes HandType { get; set; }
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(PlayerIndex);
+            writer.Write(Card);
+            writer.Write(FaceDown);
+            writer.Write((byte)HandType);
+        }
+        public static CardDealtPacket Deserialize(PacketReader reader)
+        {
+            try
+            {
+                var playerIndex = reader.ReadByte();
+                System.Console.WriteLine($"[CardDealtPacket] PlayerIndex: {playerIndex}");
+                
+                var card = reader.ReadCard();
+                System.Console.WriteLine($"[CardDealtPacket] Card: {card.Type} {card.Value}");
+                
+                var faceDown = reader.ReadBoolean();
+                System.Console.WriteLine($"[CardDealtPacket] FaceDown: {faceDown}");
+                
+                var handType = (HandTypes)reader.ReadByte();
+                System.Console.WriteLine($"[CardDealtPacket] HandType: {handType}");
+                
+                return new CardDealtPacket
+                {
+                    PlayerIndex = playerIndex,
+                    Card = card,
+                    FaceDown = faceDown,
+                    HandType = handType
+                };
+            }
+            catch (System.Exception ex)
+            {
+                System.Console.WriteLine($"[CardDealtPacket ERROR] Failed to deserialize: {ex.Message}");
+                throw;
+            }
+        }
+    }
+
+    public class ShuffleSeedPacket
+    {
+        public int Seed { get; set; }
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(Seed);
+        }
+        public static ShuffleSeedPacket Deserialize(PacketReader reader)
+        {
+            return new ShuffleSeedPacket
+            {
+                Seed = reader.ReadInt32()
+            };
+        }
+    }
+
+    public class TurnChangedPacket
+    {
+        public byte CurrentPlayerIndex { get; set; }
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(CurrentPlayerIndex);
+        }
+        public static TurnChangedPacket Deserialize(PacketReader reader)
+        {
+            return new TurnChangedPacket
+            {
+                CurrentPlayerIndex = reader.ReadByte()
+            };
+        }
+    }
+
+    public class BalanceUpdatePacket
+    {
+        public byte PlayerIndex { get; set; }
+        public float NewBalance { get; set; }
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(PlayerIndex);
+            writer.Write(NewBalance);
+        }
+        public static BalanceUpdatePacket Deserialize(PacketReader reader)
+        {
+            return new BalanceUpdatePacket
+            {
+                PlayerIndex = reader.ReadByte(),
+                NewBalance = reader.ReadSingle()
+            };
+        }
+    }
+
+    // Phase 5: Gameplay Action Packets
+    public class HitActionPacket
+    {
+        public byte PlayerIndex { get; set; }
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(PlayerIndex);
+        }
+        public static HitActionPacket Deserialize(PacketReader reader)
+        {
+            return new HitActionPacket
+            {
+                PlayerIndex = reader.ReadByte()
+            };
+        }
+    }
+
+    public class StandActionPacket
+    {
+        public byte PlayerIndex { get; set; }
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(PlayerIndex);
+        }
+        public static StandActionPacket Deserialize(PacketReader reader)
+        {
+            return new StandActionPacket
+            {
+                PlayerIndex = reader.ReadByte()
+            };
+        }
+    }
+
+    public class DoubleActionPacket
+    {
+        public byte PlayerIndex { get; set; }
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(PlayerIndex);
+        }
+        public static DoubleActionPacket Deserialize(PacketReader reader)
+        {
+            return new DoubleActionPacket
+            {
+                PlayerIndex = reader.ReadByte()
+            };
+        }
+    }
+
+    public class SplitActionPacket
+    {
+        public byte PlayerIndex { get; set; }
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(PlayerIndex);
+        }
+        public static SplitActionPacket Deserialize(PacketReader reader)
+        {
+            return new SplitActionPacket
+            {
+                PlayerIndex = reader.ReadByte()
+            };
+        }
+    }
+
+    public class InsuranceActionPacket
+    {
+        public byte PlayerIndex { get; set; }
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(PlayerIndex);
+        }
+        public static InsuranceActionPacket Deserialize(PacketReader reader)
+        {
+            return new InsuranceActionPacket
+            {
+                PlayerIndex = reader.ReadByte()
+            };
+        }
+    }
+}

+ 47 - 3
CardsStarterKit/Core/Game/Blackjack/UI/BlackJackAnimatedPlayerHandComponent.cs

@@ -16,6 +16,8 @@ namespace Blackjack
     public class BlackjackAnimatedPlayerHandComponent : AnimatedHandGameComponent
     {
         Vector2 offset;
+        private int horizontalSpacing;
+        private int verticalSpacing;
 
         /// <summary>
         /// Creates a new instance of the 
@@ -30,7 +32,17 @@ namespace Blackjack
             CardsGame cardGame, Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch, Microsoft.Xna.Framework.Matrix globalTransformation)
             : base(place, hand, cardGame, spriteBatch, globalTransformation)
         {
-            this.offset = Vector2.Zero;
+            // Move cards up above the chip circle by a card height + padding
+            if (cardGame is BlackjackCardGame blackjackGame)
+            {
+                int cardHeight = UIConstants.GetCardHeight(blackjackGame.ScreenManager.SafeArea.Height);
+                this.offset = new Vector2(0, -(cardHeight / 2)); // Above chip circle
+            }
+            else
+            {
+                this.offset = new Vector2(0, -120); // Fallback offset
+            }
+            InitializeSpacing(cardGame);
         }
 
         /// <summary>
@@ -47,7 +59,39 @@ namespace Blackjack
             Hand hand, CardsGame cardGame, Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch, Microsoft.Xna.Framework.Matrix globalTransformation)
             : base(place, hand, cardGame, spriteBatch, globalTransformation)
         {
-            this.offset = offset;
+            // Apply additional Y offset to move cards above chip circle, keeping provided X offset
+            var blackjackGame = cardGame as BlackjackCardGame;
+            if (blackjackGame != null)
+            {
+                int cardHeight = UIConstants.GetCardHeight(blackjackGame.ScreenManager.SafeArea.Height);
+                this.offset = new Vector2(offset.X, offset.Y - cardHeight - 15); // Above chip circle
+            }
+            else
+            {
+                this.offset = new Vector2(offset.X, offset.Y - 120); // Fallback offset
+            }
+            InitializeSpacing(cardGame);
+        }
+
+        /// <summary>
+        /// Initialize card spacing based on screen dimensions
+        /// </summary>
+        private void InitializeSpacing(CardsGame cardGame)
+        {
+            var blackjackGame = cardGame as BlackjackCardGame;
+            if (blackjackGame != null)
+            {
+                int screenWidth = blackjackGame.ScreenManager.SafeArea.Width;
+                int screenHeight = blackjackGame.ScreenManager.SafeArea.Height;
+                horizontalSpacing = UIConstants.GetPlayerCardHorizontalSpacing(screenWidth);
+                verticalSpacing = UIConstants.GetPlayerCardVerticalSpacing(screenHeight);
+            }
+            else
+            {
+                // Fallback to reasonable defaults
+                horizontalSpacing = 25;
+                verticalSpacing = 30;
+            }
         }
 
         /// <summary>
@@ -60,7 +104,7 @@ namespace Blackjack
         /// rendered.</returns>
         public override Vector2 GetCardRelativePosition(int cardLocationInHand)
         {
-            return new Vector2(25 * cardLocationInHand, -30 * cardLocationInHand) +
+            return new Vector2(horizontalSpacing * cardLocationInHand, -verticalSpacing * cardLocationInHand) +
                 offset;
         }
     }

+ 7 - 1
CardsStarterKit/Core/Game/Blackjack/UI/BlackJackTable.cs

@@ -49,9 +49,15 @@ namespace Blackjack
 
             SpriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, globalTransformation);
 
+            // Scale rings down for 7 players
+            float ringScale = UIConstants.ChipScaleRatio;
+            
             for (int placeIndex = 0; placeIndex < Places; placeIndex++)
             {
-                SpriteBatch.Draw(RingTexture, PlaceOrder(placeIndex) + RingOffset, Color.White);
+                Vector2 position = PlaceOrder(placeIndex) + RingOffset;
+                Vector2 origin = new Vector2(RingTexture.Width / 2f, RingTexture.Height / 2f);
+                
+                SpriteBatch.Draw(RingTexture, position, null, Color.White, 0f, origin, ringScale, SpriteEffects.None, 0f);
             }
 
             SpriteBatch.End();

+ 15 - 1
CardsStarterKit/Core/Game/Blackjack/UI/BlackjackAnimatedDealerHandComponent.cs

@@ -15,6 +15,8 @@ namespace Blackjack
 {
     public class BlackjackAnimatedDealerHandComponent : AnimatedHandGameComponent
     {
+        private int cardSpacing;
+
         /// <summary>
         /// Creates a new instance of the 
         /// <see cref="BlackjackAnimatedDealerHandComponent"/> class.
@@ -27,6 +29,18 @@ namespace Blackjack
             CardsGame cardGame, Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch, Microsoft.Xna.Framework.Matrix globalTransformation)
             : base(place, hand, cardGame, spriteBatch, globalTransformation)
         {
+            // Calculate card spacing based on screen width
+            var blackjackGame = cardGame as BlackjackCardGame;
+            if (blackjackGame != null)
+            {
+                int screenWidth = blackjackGame.ScreenManager.SafeArea.Width;
+                cardSpacing = UIConstants.GetDealerCardSpacing(screenWidth);
+            }
+            else
+            {
+                // Fallback to a reasonable default
+                cardSpacing = 30;
+            }
         }
 
         /// <summary>
@@ -39,7 +53,7 @@ namespace Blackjack
         /// rendered.</returns>
         public override Vector2 GetCardRelativePosition(int cardLocationInHand)
         {
-            return new Vector2(30 * cardLocationInHand, 0);
+            return new Vector2(cardSpacing * cardLocationInHand, 0);
         }
     }
 }

+ 1 - 1
CardsStarterKit/Core/Game/Blackjack/UI/Button.cs

@@ -201,7 +201,7 @@ namespace Blackjack
             spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, globalTransformation);
 
             spriteBatch.Draw(isPressed ? PressedTexture : RegularTexture, Bounds, Color.White);
-            if (Font != null)
+            if (Font != null && !string.IsNullOrEmpty(Text))
             {
                 Vector2 textPosition = Font.MeasureString(Text);
                 textPosition = new Vector2(Bounds.Width - textPosition.X,

+ 227 - 12
CardsStarterKit/Core/Game/Blackjack/UI/UIConstants.cs

@@ -4,6 +4,10 @@
 // UI scaling constants for resolution-independent rendering in Blackjack
 //-----------------------------------------------------------------------------
 
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
 namespace Blackjack
 {
     /// <summary>
@@ -16,17 +20,46 @@ namespace Blackjack
         public const float ButtonWidthRatio = 0.078f;         // ~100px at 1280px width
         public const float ButtonHeightRatio = 0.069f;        // ~50px at 720px height
         public const float WideButtonWidthRatio = 0.156f;     // ~200px at 1280px width
-        
+
         // Spacing and padding (as percentage of screen dimensions)
         public const float SmallPaddingRatio = 0.008f;        // ~10px at 1280px
         public const float MediumPaddingRatio = 0.047f;       // ~60px at 1280px
         public const float ButtonSpacingRatio = 0.086f;       // ~110px at 1280px
         public const float ChipSpacingRatio = 0.063f;         // ~80px at 1280px
-        
+
+        // Card and chip scaling for 7 players (scaled down from 3 players)
+        public const float CardScaleRatio = 0.65f;            // 65% of original size to fit 7 players
+        public const float ChipCircleRadiusRatio = 0.055f;    // ~40px radius at 720px height (was ~70px for 3 players)
+        public const float ChipScaleRatio = 0.7f;             // 70% of original chip size
+        public const float RingOffsetYRatio = 0.153f;         // ~110px at 720px height - distance below card position for chip circle
+
+        // Card dimensions and spacing (based on original card size 71x96)
+        public const float CardWidthRatio = 0.055f;           // ~71px at 1280px width
+        public const float CardHeightRatio = 0.133f;          // ~96px at 720px height
+        public const float DealerCardSpacingRatio = 0.023f;   // ~30px at 1280px width
+        public const float PlayerCardHorizontalSpacingRatio = 0.020f; // ~25px at 1280px width
+        public const float PlayerCardVerticalSpacingRatio = 0.042f;   // ~30px at 720px height
+        public const float SecondHandOffsetXRatio = 0.078f;   // ~100px at 1280px width
+        public const float SecondHandOffsetYRatio = 0.035f;   // ~25px at 720px height
+        public const float BetSecondHandOffsetXRatio = 0.020f; // ~25px at 1280px width
+        public const float BetSecondHandOffsetYRatio = 0.042f; // ~30px at 720px height
+
+        // Shuffle animation parameters
+        public const float ShuffleSplitDistanceRatio = 0.117f; // ~150px at 1280px width
+        public const float ShuffleCascadeHeightRatio = 0.111f; // ~80px at 720px height
+        public const float DeckLayerOffsetRatio = 0.0012f;     // ~1.5px at 1280px width
+
+        // Frame size for UI elements
+        public const float FrameWidthRatio = 0.141f;          // ~180px at 1280px width
+        public const float FrameHeightRatio = 0.250f;         // ~180px at 720px height
+
+        // Insurance position
+        public const float InsuranceYPositionRatio = 0.167f;  // ~120px at 720px height
+
         // Text scaling
         public const float RegularTextScale = 0.6f;
         public const float SmallTextScale = 0.5f;
-        
+
         /// <summary>
         /// Calculate actual pixel value from screen width ratio
         /// </summary>
@@ -34,7 +67,7 @@ namespace Blackjack
         {
             return (int)(screenWidth * ratio);
         }
-        
+
         /// <summary>
         /// Calculate actual pixel value from screen height ratio
         /// </summary>
@@ -42,7 +75,7 @@ namespace Blackjack
         {
             return (int)(screenHeight * ratio);
         }
-        
+
         /// <summary>
         /// Get standard button width
         /// </summary>
@@ -50,7 +83,7 @@ namespace Blackjack
         {
             return GetWidthScaled(screenWidth, ButtonWidthRatio);
         }
-        
+
         /// <summary>
         /// Get wide button width (for insurance, new game, etc.)
         /// </summary>
@@ -58,7 +91,7 @@ namespace Blackjack
         {
             return GetWidthScaled(screenWidth, WideButtonWidthRatio);
         }
-        
+
         /// <summary>
         /// Get button height
         /// </summary>
@@ -66,7 +99,7 @@ namespace Blackjack
         {
             return GetHeightScaled(screenHeight, ButtonHeightRatio);
         }
-        
+
         /// <summary>
         /// Get small padding (10px equivalent)
         /// </summary>
@@ -74,7 +107,7 @@ namespace Blackjack
         {
             return GetWidthScaled(screenWidth, SmallPaddingRatio);
         }
-        
+
         /// <summary>
         /// Get medium padding (60px equivalent)
         /// </summary>
@@ -82,7 +115,7 @@ namespace Blackjack
         {
             return GetHeightScaled(screenHeight, MediumPaddingRatio);
         }
-        
+
         /// <summary>
         /// Get button spacing (110px equivalent)
         /// </summary>
@@ -90,7 +123,7 @@ namespace Blackjack
         {
             return GetWidthScaled(screenWidth, ButtonSpacingRatio);
         }
-        
+
         /// <summary>
         /// Get chip spacing (80px equivalent)
         /// </summary>
@@ -98,5 +131,187 @@ namespace Blackjack
         {
             return GetHeightScaled(screenHeight, ChipSpacingRatio);
         }
+
+        /// <summary>
+        /// Get chip circle radius for player positions
+        /// </summary>
+        public static int GetChipCircleRadius(int screenHeight)
+        {
+            return GetHeightScaled(screenHeight, ChipCircleRadiusRatio);
+        }
+
+        /// <summary>
+        /// Get card scale for 7 players
+        /// </summary>
+        public static float GetCardScale()
+        {
+            return CardScaleRatio;
+        }
+
+        /// <summary>
+        /// Get chip scale for 7 players
+        /// </summary>
+        public static float GetChipScale()
+        {
+            return ChipScaleRatio;
+        }
+
+        /// <summary>
+        /// Get ring offset (distance below card position for chip circle)
+        /// </summary>
+        public static Vector2 GetRingOffset(int screenHeight)
+        {
+            return new Vector2(0, GetHeightScaled(screenHeight, RingOffsetYRatio));
+        }
+
+        /// <summary>
+        /// Get card width based on screen width
+        /// </summary>
+        public static int GetCardWidth(int screenWidth)
+        {
+            return GetWidthScaled(screenWidth, CardWidthRatio);
+        }
+
+        /// <summary>
+        /// Get card height based on screen height
+        /// </summary>
+        public static int GetCardHeight(int screenHeight)
+        {
+            return GetHeightScaled(screenHeight, CardHeightRatio);
+        }
+
+        /// <summary>
+        /// Get card size as Vector2
+        /// </summary>
+        public static Vector2 GetCardSize(int screenWidth, int screenHeight)
+        {
+            return new Vector2(GetCardWidth(screenWidth), GetCardHeight(screenHeight));
+        }
+
+        /// <summary>
+        /// Get dealer card spacing (horizontal spacing between cards)
+        /// </summary>
+        public static int GetDealerCardSpacing(int screenWidth)
+        {
+            return GetWidthScaled(screenWidth, DealerCardSpacingRatio);
+        }
+
+        /// <summary>
+        /// Get player card horizontal spacing
+        /// </summary>
+        public static int GetPlayerCardHorizontalSpacing(int screenWidth)
+        {
+            return GetWidthScaled(screenWidth, PlayerCardHorizontalSpacingRatio);
+        }
+
+        /// <summary>
+        /// Get player card vertical spacing
+        /// </summary>
+        public static int GetPlayerCardVerticalSpacing(int screenHeight)
+        {
+            return GetHeightScaled(screenHeight, PlayerCardVerticalSpacingRatio);
+        }
+
+        /// <summary>
+        /// Get second hand offset for split hands
+        /// </summary>
+        public static Vector2 GetSecondHandOffset(int screenWidth, int screenHeight)
+        {
+            return new Vector2(
+                GetWidthScaled(screenWidth, SecondHandOffsetXRatio),
+                GetHeightScaled(screenHeight, SecondHandOffsetYRatio));
+        }
+
+        /// <summary>
+        /// Get second hand offset for betting component
+        /// </summary>
+        public static Vector2 GetBetSecondHandOffset(int screenWidth, int screenHeight)
+        {
+            return new Vector2(
+                GetWidthScaled(screenWidth, BetSecondHandOffsetXRatio),
+                GetHeightScaled(screenHeight, BetSecondHandOffsetYRatio));
+        }
+
+        /// <summary>
+        /// Get shuffle split distance
+        /// </summary>
+        public static float GetShuffleSplitDistance(int screenWidth)
+        {
+            return GetWidthScaled(screenWidth, ShuffleSplitDistanceRatio);
+        }
+
+        /// <summary>
+        /// Get shuffle cascade height
+        /// </summary>
+        public static float GetShuffleCascadeHeight(int screenHeight)
+        {
+            return GetHeightScaled(screenHeight, ShuffleCascadeHeightRatio);
+        }
+
+        /// <summary>
+        /// Get deck layer offset for stacked deck display
+        /// </summary>
+        public static Vector2 GetDeckLayerOffset(int screenWidth)
+        {
+            float offset = GetWidthScaled(screenWidth, DeckLayerOffsetRatio);
+            return new Vector2(offset, offset);
+        }
+
+        /// <summary>
+        /// Get frame size for UI elements
+        /// </summary>
+        public static Vector2 GetFrameSize(int screenWidth, int screenHeight)
+        {
+            return new Vector2(
+                GetWidthScaled(screenWidth, FrameWidthRatio),
+                GetHeightScaled(screenHeight, FrameHeightRatio));
+        }
+
+        /// <summary>
+        /// Get insurance Y position
+        /// </summary>
+        public static float GetInsuranceYPosition(int screenHeight)
+        {
+            return GetHeightScaled(screenHeight, InsuranceYPositionRatio);
+        }
+
+        /// <summary>
+        /// Calculate the Y position for gameplay buttons (Deal, Clear, Hit, Stand, etc.)
+        /// This ensures consistent button positioning across betting and gameplay phases.
+        /// </summary>
+        /// <param name="chipTextureHeight">The actual height of the chip texture in pixels</param>
+        /// <param name="screenWidth">The screen width (for calculating padding)</param>
+        /// <param name="screenHeight">The screen height (for calculating button height)</param>
+        /// <returns>The Y coordinate where buttons should be positioned</returns>
+        public static int GetGameplayButtonYPosition(int chipTextureHeight, int screenWidth, int screenHeight)
+        {
+            int smallPadding = GetSmallPadding(screenWidth);
+            int buttonHeight = GetButtonHeight(screenHeight);
+
+            // Position buttons above the chips with consistent spacing
+            // Bottom of screen - chip height - padding below chips - button height - padding between buttons and chips
+            return screenHeight - chipTextureHeight - smallPadding - buttonHeight - (smallPadding * 2);
+        }
+
+        /// <summary>
+        /// Calculate button width based on text size with padding.
+        /// Returns the larger of minWidth or the width required to fit the text.
+        /// </summary>
+        /// <param name="text">The button text to measure</param>
+        /// <param name="font">The font used to render the text</param>
+        /// <param name="minWidth">The minimum button width</param>
+        /// <param name="screenWidth">The screen width (for calculating padding)</param>
+        /// <returns>The calculated button width in pixels</returns>
+        public static int CalculateButtonWidth(string text, SpriteFont font, int minWidth, int screenWidth)
+        {
+            if (string.IsNullOrEmpty(text) || font == null)
+                return minWidth;
+
+            Vector2 textSize = font.MeasureString(text);
+            int padding = GetSmallPadding(screenWidth) * 2; // Padding on both sides
+            int requiredWidth = (int)textSize.X + padding;
+
+            return Math.Max(minWidth, requiredWidth);
+        }
     }
-}
+}

+ 3 - 0
CardsStarterKit/Core/Game/Misc/AudioManager.cs

@@ -147,6 +147,7 @@ namespace Blackjack
             // If the sound exists, start it
             if (audioManager.soundBank.ContainsKey(soundName))
             {
+                audioManager.soundBank[soundName].Volume = GameSettings.Instance.SoundVolume;
                 audioManager.soundBank[soundName].Play();
             }
         }
@@ -166,6 +167,7 @@ namespace Blackjack
                     audioManager.soundBank[soundName].IsLooped = isLooped;
                 }
 
+                audioManager.soundBank[soundName].Volume = GameSettings.Instance.SoundVolume;
                 audioManager.soundBank[soundName].Play();
             }
         }
@@ -261,6 +263,7 @@ namespace Blackjack
                 }
 
                 MediaPlayer.IsRepeating = true;
+                MediaPlayer.Volume = GameSettings.Instance.MusicVolume;
 
                 MediaPlayer.Play(audioManager.musicBank[musicSoundName]);
             }

+ 301 - 0
CardsStarterKit/Core/Game/Misc/GameSettings.cs

@@ -0,0 +1,301 @@
+//-----------------------------------------------------------------------------
+// GameSettings.cs
+//
+// Manages game settings with JSON persistence
+//-----------------------------------------------------------------------------
+
+using System;
+using System.Globalization;
+using System.IO;
+using System.Text.Json;
+using CardsFramework;
+using Microsoft.Xna.Framework;
+
+namespace Blackjack
+{
+    /// <summary>
+    /// Manages all game settings with automatic persistence to JSON file.
+    /// </summary>
+    public class GameSettings
+    {
+        private static GameSettings instance;
+        private static readonly string settingsFileName = "blackjack_settings.json";
+        private static string settingsFilePath;
+
+        private string language = "English";
+
+        // Display settings
+        public string Language
+        {
+            get => language;
+            set
+            {
+                language = value;
+                ApplyLanguage(value);
+            }
+        }
+        public string Theme { get; set; } = "Red";
+        public string Currency { get; set; } = "$";
+
+        // AI settings
+        public byte MaxAIPlayers { get; set; } = GetPlatformMaxAIPlayers();
+        public bool FillEmptySlotsWithAI { get; set; } = true;
+
+        // Audio settings
+        public float SoundVolume { get; set; } = 1.0f;
+        public float MusicVolume { get; set; } = 1.0f;
+
+        // Gameplay settings
+        public AnimationSpeed AnimationSpeed { get; set; } = AnimationSpeed.Normal;
+        public bool AutoStandOn21 { get; set; } = false;
+        public bool ShowCardCount { get; set; } = true;
+        public bool PersistWinnings { get; set; } = false;
+        public float SavedPlayerBalance { get; set; } = 500f;
+
+        /// <summary>
+        /// Gets the singleton instance of GameSettings.
+        /// </summary>
+        public static GameSettings Instance
+        {
+            get
+            {
+                if (instance == null)
+                {
+                    Load();
+                }
+                return instance;
+            }
+        }
+
+        /// <summary>
+        /// Gets the maximum allowed AI players based on the current platform.
+        /// </summary>
+        public static byte GetPlatformMaxAIPlayers()
+        {
+            if (UIUtility.IsMobile)
+            {
+                return 3; // Mobile platforms have limited screen space
+            }
+            else
+            {
+                return 6; // Desktop/Console can handle more players
+            }
+        }
+
+        /// <summary>
+        /// Initializes the settings file path based on platform.
+        /// </summary>
+        private static void InitializeFilePath()
+        {
+            if (string.IsNullOrEmpty(settingsFilePath))
+            {
+                // Get platform-specific storage location
+                string storageFolder;
+                if (OperatingSystem.IsAndroid())
+                {
+                    storageFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
+                }
+                else if (OperatingSystem.IsIOS())
+                {
+                    storageFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
+                }
+                else
+                {
+                    // Desktop: use AppData/LocalApplicationData
+                    storageFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+                }
+                storageFolder = Path.Combine(storageFolder, "Blackjack");
+
+                // Ensure directory exists
+                if (!Directory.Exists(storageFolder))
+                {
+                    Directory.CreateDirectory(storageFolder);
+                }
+
+                settingsFilePath = Path.Combine(storageFolder, settingsFileName);
+            }
+        }
+
+        /// <summary>
+        /// Loads settings from disk. If file doesn't exist, creates default settings.
+        /// </summary>
+        public static void Load()
+        {
+            InitializeFilePath();
+
+            try
+            {
+                if (File.Exists(settingsFilePath))
+                {
+                    string json = File.ReadAllText(settingsFilePath);
+                    instance = JsonSerializer.Deserialize<GameSettings>(json);
+                    System.Console.WriteLine($"[Settings] Loaded from {settingsFilePath}");
+                    System.Console.WriteLine($"[Settings] PersistWinnings={instance.PersistWinnings}, SavedPlayerBalance={instance.SavedPlayerBalance}");
+                }
+                else
+                {
+                    // First launch - detect OS language and set defaults
+                    instance = new GameSettings();
+                    DetectAndSetOSLanguage();
+                    System.Console.WriteLine("[Settings] Created default settings (first launch)");
+                    System.Console.WriteLine($"[Settings] Detected language: {instance.Language}, Currency: {instance.Currency}");
+                    Save(); // Save default settings
+                }
+            }
+            catch (Exception ex)
+            {
+                System.Console.WriteLine($"[Settings] Error loading: {ex.Message}");
+                instance = new GameSettings(); // Use defaults on error
+            }
+
+            // Ensure MaxAIPlayers doesn't exceed platform limit
+            byte platformMax = GetPlatformMaxAIPlayers();
+            if (instance.MaxAIPlayers > platformMax)
+            {
+                instance.MaxAIPlayers = platformMax;
+            }
+
+            // Apply language setting
+            ApplyLanguage(instance.Language);
+        }
+
+        /// <summary>
+        /// Detects the OS language and sets the language and currency accordingly.
+        /// Called only on first launch.
+        /// </summary>
+        private static void DetectAndSetOSLanguage()
+        {
+            try
+            {
+                string osLanguage = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName.ToLower();
+                System.Console.WriteLine($"[Settings] Detected OS language: {osLanguage} ({CultureInfo.CurrentUICulture.Name})");
+
+                // Map OS language to game language and set appropriate currency
+                switch (osLanguage)
+                {
+                    case "fr":
+                        instance.language = "Français";
+                        instance.Currency = "€";
+                        break;
+                    case "es":
+                        instance.language = "Español";
+                        instance.Currency = "€";
+                        break;
+                    case "it":
+                        instance.language = "Italiano";
+                        instance.Currency = "€";
+                        break;
+                    case "ja":
+                        instance.language = "日本語";
+                        instance.Currency = "¥";
+                        break;
+                    case "zh":
+                        instance.language = "中文";
+                        instance.Currency = "¥";
+                        break;
+                    case "ru":
+                        instance.language = "Русский";
+                        instance.Currency = "R";
+                        break;
+                    default:
+                        instance.language = "English";
+                        instance.Currency = "$";
+                        break;
+                }
+
+                // Apply the detected language
+                ApplyLanguage(instance.language);
+            }
+            catch (Exception ex)
+            {
+                System.Console.WriteLine($"[Settings] Error detecting OS language: {ex.Message}");
+                instance.language = "English";
+                instance.Currency = "$";
+            }
+        }
+
+        /// <summary>
+        /// Applies the language setting by changing the current UI culture.
+        /// </summary>
+        private static void ApplyLanguage(string languageName)
+        {
+            try
+            {
+                string cultureCode = languageName switch
+                {
+                    "English" => "en-US",
+                    "Français" => "fr-FR",
+                    "Español" => "es-ES",
+                    "Italiano" => "it-IT",
+                    "日本語" => "ja-JP",
+                    "中文" => "zh-CN",
+                    "Русский" => "ru-RU",
+                    _ => "en-US" // Default to English
+                };
+
+                CultureInfo culture = new CultureInfo(cultureCode);
+                CultureInfo.CurrentUICulture = culture;
+                System.Console.WriteLine($"[Settings] Language changed to {languageName} ({cultureCode})");
+            }
+            catch (Exception ex)
+            {
+                System.Console.WriteLine($"[Settings] Error applying language: {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// Saves current settings to disk.
+        /// </summary>
+        public static void Save()
+        {
+            InitializeFilePath();
+
+            try
+            {
+                var options = new JsonSerializerOptions
+                {
+                    WriteIndented = true
+                };
+                string json = JsonSerializer.Serialize(instance, options);
+                File.WriteAllText(settingsFilePath, json);
+                System.Console.WriteLine($"[Settings] Saved to {settingsFilePath}");
+                System.Console.WriteLine($"[Settings] PersistWinnings={instance.PersistWinnings}, SavedPlayerBalance={instance.SavedPlayerBalance}");
+            }
+            catch (Exception ex)
+            {
+                System.Console.WriteLine($"[Settings] Error saving: {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// Validates and clamps a setting value.
+        /// </summary>
+        public void ValidateAndClamp()
+        {
+            // Clamp volumes to 0-1 range
+            SoundVolume = MathHelper.Clamp(SoundVolume, 0f, 1f);
+            MusicVolume = MathHelper.Clamp(MusicVolume, 0f, 1f);
+
+            // Clamp MaxAIPlayers to platform limit
+            byte platformMax = GetPlatformMaxAIPlayers();
+            if (MaxAIPlayers > platformMax)
+            {
+                MaxAIPlayers = platformMax;
+            }
+            if (MaxAIPlayers < 0)
+            {
+                MaxAIPlayers = 0;
+            }
+        }
+    }
+
+    /// <summary>
+    /// Animation speed options for card dealing and gameplay.
+    /// </summary>
+    public enum AnimationSpeed
+    {
+        Fast,
+        Normal,
+        Slow
+    }
+}

+ 1116 - 0
CardsStarterKit/Core/Game/Resources.Designer.cs

@@ -0,0 +1,1116 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.1
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Blackjack
+{
+    using System;
+
+
+    /// <summary>
+    ///   A strongly-typed resource class, for looking up localized strings, etc.
+    /// </summary>
+    // This class was auto-generated by the StronglyTypedResourceBuilder
+    // class via a tool like ResGen or Visual Studio.
+    // To add or remove a member, edit your .ResX file then rerun ResGen
+    // with the /str option, or rebuild your VS project.
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    internal class Resources
+    {
+
+        private static global::System.Resources.ResourceManager resourceMan;
+
+        private static global::System.Globalization.CultureInfo resourceCulture;
+
+        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+        internal Resources()
+        {
+        }
+
+        /// <summary>
+        ///   Returns the cached ResourceManager instance used by this class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Resources.ResourceManager ResourceManager
+        {
+            get
+            {
+                if (object.ReferenceEquals(resourceMan, null))
+                {
+                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Blackjack.Core.Resources", typeof(Resources).Assembly);
+                    resourceMan = temp;
+                }
+                return resourceMan;
+            }
+        }
+
+        /// <summary>
+        ///   Overrides the current thread's CurrentUICulture property for all
+        ///   resource lookups using this strongly typed resource class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Globalization.CultureInfo Culture
+        {
+            get
+            {
+                return resourceCulture;
+            }
+            set
+            {
+                resourceCulture = value;
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Back.
+        /// </summary>
+        internal static string Back
+        {
+            get
+            {
+                return ResourceManager.GetString("Back", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Are you sure you want to end this session?.
+        /// </summary>
+        internal static string ConfirmEndSession
+        {
+            get
+            {
+                return ResourceManager.GetString("ConfirmEndSession", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Are you sure you want to exit this sample?.
+        /// </summary>
+        internal static string ConfirmExitSample
+        {
+            get
+            {
+                return ResourceManager.GetString("ConfirmExitSample", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Are you sure you want to start the game,
+        ///even though not all players are ready?.
+        /// </summary>
+        internal static string ConfirmForceStartGame
+        {
+            get
+            {
+                return ResourceManager.GetString("ConfirmForceStartGame", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Are you sure you want to leave this session?.
+        /// </summary>
+        internal static string ConfirmLeaveSession
+        {
+            get
+            {
+                return ResourceManager.GetString("ConfirmLeaveSession", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Online gameplay is not available in trial mode.
+        ///Would you like to purchase this game?.
+        /// </summary>
+        internal static string ConfirmMarketplace
+        {
+            get
+            {
+                return ResourceManager.GetString("ConfirmMarketplace", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Are you sure you want to quit this game?.
+        /// </summary>
+        internal static string ConfirmQuitGame
+        {
+            get
+            {
+                return ResourceManager.GetString("ConfirmQuitGame", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Create Session.
+        /// </summary>
+        internal static string CreateSession
+        {
+            get
+            {
+                return ResourceManager.GetString("CreateSession", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to End Session.
+        /// </summary>
+        internal static string EndSession
+        {
+            get
+            {
+                return ResourceManager.GetString("EndSession", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Lost connection to the network session.
+        /// </summary>
+        internal static string ErrorDisconnected
+        {
+            get
+            {
+                return ResourceManager.GetString("ErrorDisconnected", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to You must sign in a suitable gamer profile
+        ///in order to access this functionality.
+        /// </summary>
+        internal static string ErrorGamerPrivilege
+        {
+            get
+            {
+                return ResourceManager.GetString("ErrorGamerPrivilege", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Host ended the session.
+        /// </summary>
+        internal static string ErrorHostEndedSession
+        {
+            get
+            {
+                return ResourceManager.GetString("ErrorHostEndedSession", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to There was an error while
+        ///accessing the network.
+        /// </summary>
+        internal static string ErrorNetwork
+        {
+            get
+            {
+                return ResourceManager.GetString("ErrorNetwork", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Networking is turned
+        ///off or not connected.
+        /// </summary>
+        internal static string ErrorNetworkNotAvailable
+        {
+            get
+            {
+                return ResourceManager.GetString("ErrorNetworkNotAvailable", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Host kicked you out of the session.
+        /// </summary>
+        internal static string ErrorRemovedByHost
+        {
+            get
+            {
+                return ResourceManager.GetString("ErrorRemovedByHost", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to This session is already full.
+        /// </summary>
+        internal static string ErrorSessionFull
+        {
+            get
+            {
+                return ResourceManager.GetString("ErrorSessionFull", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Session not found. It may have ended,
+        ///or there may be no network connectivity
+        ///between the local machine and session host.
+        /// </summary>
+        internal static string ErrorSessionNotFound
+        {
+            get
+            {
+                return ResourceManager.GetString("ErrorSessionNotFound", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to You must wait for the host to return to
+        ///the lobby before you can join this session.
+        /// </summary>
+        internal static string ErrorSessionNotJoinable
+        {
+            get
+            {
+                return ResourceManager.GetString("ErrorSessionNotJoinable", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to This functionality is not available in trial mode.
+        /// </summary>
+        internal static string ErrorTrialMode
+        {
+            get
+            {
+                return ResourceManager.GetString("ErrorTrialMode", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to An unknown error occurred.
+        /// </summary>
+        internal static string ErrorUnknown
+        {
+            get
+            {
+                return ResourceManager.GetString("ErrorUnknown", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Exit.
+        /// </summary>
+        internal static string Exit
+        {
+            get
+            {
+                return ResourceManager.GetString("Exit", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Find Sessions.
+        /// </summary>
+        internal static string FindSessions
+        {
+            get
+            {
+                return ResourceManager.GetString("FindSessions", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to  (host).
+        /// </summary>
+        internal static string HostSuffix
+        {
+            get
+            {
+                return ResourceManager.GetString("HostSuffix", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Join Session.
+        /// </summary>
+        internal static string JoinSession
+        {
+            get
+            {
+                return ResourceManager.GetString("JoinSession", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Leave Session.
+        /// </summary>
+        internal static string LeaveSession
+        {
+            get
+            {
+                return ResourceManager.GetString("LeaveSession", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Loading.
+        /// </summary>
+        internal static string Loading
+        {
+            get
+            {
+                return ResourceManager.GetString("Loading", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Lobby.
+        /// </summary>
+        internal static string Lobby
+        {
+            get
+            {
+                return ResourceManager.GetString("Lobby", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Main Menu.
+        /// </summary>
+        internal static string MainMenu
+        {
+            get
+            {
+                return ResourceManager.GetString("MainMenu", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to 
+        ///A button, Space, Enter = ok
+        ///B button, Esc = cancel.
+        /// </summary>
+        internal static string MessageBoxUsage
+        {
+            get
+            {
+                return ResourceManager.GetString("MessageBoxUsage", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to {0} joined.
+        /// </summary>
+        internal static string MessageGamerJoined
+        {
+            get
+            {
+                return ResourceManager.GetString("MessageGamerJoined", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to {0} left.
+        /// </summary>
+        internal static string MessageGamerLeft
+        {
+            get
+            {
+                return ResourceManager.GetString("MessageGamerLeft", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Networking....
+        /// </summary>
+        internal static string NetworkBusy
+        {
+            get
+            {
+                return ResourceManager.GetString("NetworkBusy", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to No sessions found.
+        /// </summary>
+        internal static string NoSessionsFound
+        {
+            get
+            {
+                return ResourceManager.GetString("NoSessionsFound", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Paused.
+        /// </summary>
+        internal static string Paused
+        {
+            get
+            {
+                return ResourceManager.GetString("Paused", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to LIVE.
+        /// </summary>
+        internal static string PlayerMatch
+        {
+            get
+            {
+                return ResourceManager.GetString("PlayerMatch", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Quit Game.
+        /// </summary>
+        internal static string QuitGame
+        {
+            get
+            {
+                return ResourceManager.GetString("QuitGame", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Resume Game.
+        /// </summary>
+        internal static string ResumeGame
+        {
+            get
+            {
+                return ResourceManager.GetString("ResumeGame", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Return to Lobby.
+        /// </summary>
+        internal static string ReturnToLobby
+        {
+            get
+            {
+                return ResourceManager.GetString("ReturnToLobby", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Single Player.
+        /// </summary>
+        internal static string SinglePlayer
+        {
+            get
+            {
+                return ResourceManager.GetString("SinglePlayer", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to System Link.
+        /// </summary>
+        internal static string SystemLink
+        {
+            get
+            {
+                return ResourceManager.GetString("SystemLink", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Play.
+        /// </summary>
+        internal static string Play
+        {
+            get
+            {
+                return ResourceManager.GetString("Play", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Theme.
+        /// </summary>
+        internal static string Theme
+        {
+            get
+            {
+                return ResourceManager.GetString("Theme", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Settings.
+        /// </summary>
+        internal static string Settings
+        {
+            get
+            {
+                return ResourceManager.GetString("Settings", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Quit.
+        /// </summary>
+        internal static string Quit
+        {
+            get
+            {
+                return ResourceManager.GetString("Quit", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Host New Game.
+        /// </summary>
+        internal static string HostNewGame
+        {
+            get
+            {
+                return ResourceManager.GetString("HostNewGame", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Refresh.
+        /// </summary>
+        internal static string Refresh
+        {
+            get
+            {
+                return ResourceManager.GetString("Refresh", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Start Game.
+        /// </summary>
+        internal static string StartGame
+        {
+            get
+            {
+                return ResourceManager.GetString("StartGame", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Available Games:.
+        /// </summary>
+        internal static string AvailableGames
+        {
+            get
+            {
+                return ResourceManager.GetString("AvailableGames", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Players:.
+        /// </summary>
+        internal static string Players
+        {
+            get
+            {
+                return ResourceManager.GetString("Players", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Deck.
+        /// </summary>
+        internal static string Deck
+        {
+            get
+            {
+                return ResourceManager.GetString("Deck", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Return.
+        /// </summary>
+        internal static string Return
+        {
+            get
+            {
+                return ResourceManager.GetString("Return", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Searching for games....
+        /// </summary>
+        internal static string SearchingForGames
+        {
+            get
+            {
+                return ResourceManager.GetString("SearchingForGames", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Found {0} game{1}.
+        /// </summary>
+        internal static string FoundGames
+        {
+            get
+            {
+                return ResourceManager.GetString("FoundGames", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Auto-refresh in {0}s.
+        /// </summary>
+        internal static string AutoRefreshIn
+        {
+            get
+            {
+                return ResourceManager.GetString("AutoRefreshIn", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Dealer: House.
+        /// </summary>
+        internal static string Dealer
+        {
+            get
+            {
+                return ResourceManager.GetString("Dealer", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to HOST.
+        /// </summary>
+        internal static string Host
+        {
+            get
+            {
+                return ResourceManager.GetString("Host", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Slot {0}: {1}.
+        /// </summary>
+        internal static string Slot
+        {
+            get
+            {
+                return ResourceManager.GetString("Slot", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Join or Host a Game.
+        /// </summary>
+        internal static string JoinOrHostGame
+        {
+            get
+            {
+                return ResourceManager.GetString("JoinOrHostGame", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Failed to create session..
+        /// </summary>
+        internal static string FailedToCreateSession
+        {
+            get
+            {
+                return ResourceManager.GetString("FailedToCreateSession", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Failed to join session..
+        /// </summary>
+        internal static string FailedToJoinSession
+        {
+            get
+            {
+                return ResourceManager.GetString("FailedToJoinSession", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to OK.
+        /// </summary>
+        internal static string OK
+        {
+            get
+            {
+                return ResourceManager.GetString("OK", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Settings.
+        /// </summary>
+        internal static string SettingsTitle
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsTitle", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to DISPLAY.
+        /// </summary>
+        internal static string SettingsDisplay
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsDisplay", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to AUDIO.
+        /// </summary>
+        internal static string SettingsAudio
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsAudio", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to AI PLAYERS.
+        /// </summary>
+        internal static string SettingsAIPlayers
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsAIPlayers", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to GAMEPLAY.
+        /// </summary>
+        internal static string SettingsGameplay
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsGameplay", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Language.
+        /// </summary>
+        internal static string SettingsLanguage
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsLanguage", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Card Back Theme.
+        /// </summary>
+        internal static string SettingsCardBackTheme
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsCardBackTheme", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Currency.
+        /// </summary>
+        internal static string SettingsCurrency
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsCurrency", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Sound Volume.
+        /// </summary>
+        internal static string SettingsSoundVolume
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsSoundVolume", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Music Volume.
+        /// </summary>
+        internal static string SettingsMusicVolume
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsMusicVolume", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Max AI Players.
+        /// </summary>
+        internal static string SettingsMaxAIPlayers
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsMaxAIPlayers", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Fill Empty Slots.
+        /// </summary>
+        internal static string SettingsFillEmptySlots
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsFillEmptySlots", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Animation Speed.
+        /// </summary>
+        internal static string SettingsAnimationSpeed
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsAnimationSpeed", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Auto-Stand on 21.
+        /// </summary>
+        internal static string SettingsAutoStandOn21
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsAutoStandOn21", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Show Card Count.
+        /// </summary>
+        internal static string SettingsShowCardCount
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsShowCardCount", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Persist Winnings.
+        /// </summary>
+        internal static string SettingsPersistWinnings
+        {
+            get
+            {
+                return ResourceManager.GetString("SettingsPersistWinnings", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Previous.
+        /// </summary>
+        internal static string Previous
+        {
+            get
+            {
+                return ResourceManager.GetString("Previous", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Next.
+        /// </summary>
+        internal static string Next
+        {
+            get
+            {
+                return ResourceManager.GetString("Next", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Page {0} of {1}.
+        /// </summary>
+        internal static string PageIndicator
+        {
+            get
+            {
+                return ResourceManager.GetString("PageIndicator", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Deal.
+        /// </summary>
+        internal static string Deal
+        {
+            get
+            {
+                return ResourceManager.GetString("Deal", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Clear.
+        /// </summary>
+        internal static string Clear
+        {
+            get
+            {
+                return ResourceManager.GetString("Clear", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Hit.
+        /// </summary>
+        internal static string Hit
+        {
+            get
+            {
+                return ResourceManager.GetString("Hit", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Stand.
+        /// </summary>
+        internal static string Stand
+        {
+            get
+            {
+                return ResourceManager.GetString("Stand", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Double.
+        /// </summary>
+        internal static string Double
+        {
+            get
+            {
+                return ResourceManager.GetString("Double", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Split.
+        /// </summary>
+        internal static string Split
+        {
+            get
+            {
+                return ResourceManager.GetString("Split", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to Insurance.
+        /// </summary>
+        internal static string Insurance
+        {
+            get
+            {
+                return ResourceManager.GetString("Insurance", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized string similar to New Hand.
+        /// </summary>
+        internal static string NewHand
+        {
+            get
+            {
+                return ResourceManager.GetString("NewHand", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized AI player name 1.
+        /// </summary>
+        internal static string AIPlayer1
+        {
+            get
+            {
+                return ResourceManager.GetString("AIPlayer1", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized AI player name 2.
+        /// </summary>
+        internal static string AIPlayer2
+        {
+            get
+            {
+                return ResourceManager.GetString("AIPlayer2", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized AI player name 3.
+        /// </summary>
+        internal static string AIPlayer3
+        {
+            get
+            {
+                return ResourceManager.GetString("AIPlayer3", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized AI player name 4.
+        /// </summary>
+        internal static string AIPlayer4
+        {
+            get
+            {
+                return ResourceManager.GetString("AIPlayer4", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized AI player name 5.
+        /// </summary>
+        internal static string AIPlayer5
+        {
+            get
+            {
+                return ResourceManager.GetString("AIPlayer5", resourceCulture);
+            }
+        }
+
+        /// <summary>
+        ///   Looks up a localized AI player name 6.
+        /// </summary>
+        internal static string AIPlayer6
+        {
+            get
+            {
+                return ResourceManager.GetString("AIPlayer6", resourceCulture);
+            }
+        }
+    }
+}

+ 353 - 0
CardsStarterKit/Core/Game/Resources.es.resx

@@ -0,0 +1,353 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <data name="Back" xml:space="preserve">
+    <value>Atrás</value>
+  </data>
+  <data name="ConfirmEndSession" xml:space="preserve">
+    <value>¿Estás seguro de que quieres finalizar esta sesión?</value>
+  </data>
+  <data name="ConfirmExitSample" xml:space="preserve">
+    <value>¿Estás seguro de que quieres salir de este ejemplo?</value>
+  </data>
+  <data name="ConfirmForceStartGame" xml:space="preserve">
+    <value>¿Estás seguro de que quieres comenzar el juego,
+incluso si no todos los jugadores están listos?</value>
+  </data>
+  <data name="ConfirmLeaveSession" xml:space="preserve">
+    <value>¿Estás seguro de que quieres abandonar esta sesión?</value>
+  </data>
+  <data name="ConfirmMarketplace" xml:space="preserve">
+    <value>El juego en línea no está disponible en el modo de prueba.
+¿Te gustaría comprar este juego?</value>
+  </data>
+  <data name="ConfirmQuitGame" xml:space="preserve">
+    <value>¿Estás seguro de que quieres salir de este juego?</value>
+  </data>
+  <data name="CreateSession" xml:space="preserve">
+    <value>Crear sesión</value>
+  </data>
+  <data name="EndSession" xml:space="preserve">
+    <value>Finalizar sesión</value>
+  </data>
+  <data name="ErrorDisconnected" xml:space="preserve">
+    <value>Conexión perdida con la sesión de red</value>
+  </data>
+  <data name="ErrorGamerPrivilege" xml:space="preserve">
+    <value>Debes iniciar sesión con un perfil de jugador apropiado
+para acceder a esta función</value>
+  </data>
+  <data name="ErrorHostEndedSession" xml:space="preserve">
+    <value>El anfitrión finalizó la sesión</value>
+  </data>
+  <data name="ErrorNetwork" xml:space="preserve">
+    <value>Se produjo un error al
+acceder a la red</value>
+  </data>
+  <data name="ErrorNetworkNotAvailable" xml:space="preserve">
+    <value>La red está deshabilitada
+o no está conectada</value>
+  </data>
+  <data name="ErrorRemovedByHost" xml:space="preserve">
+    <value>El anfitrión te expulsó de la sesión</value>
+  </data>
+  <data name="ErrorSessionFull" xml:space="preserve">
+    <value>Esta sesión ya está completa</value>
+  </data>
+  <data name="ErrorSessionNotFound" xml:space="preserve">
+    <value>No se encontró la sesión. Puede que haya terminado,
+o puede que no haya conectividad de red
+entre la máquina local y el anfitrión de la sesión</value>
+  </data>
+  <data name="ErrorSessionNotJoinable" xml:space="preserve">
+    <value>Debes esperar a que el anfitrión regrese al
+lobby antes de unirte a esta sesión</value>
+  </data>
+  <data name="ErrorTrialMode" xml:space="preserve">
+    <value>Esta función no está disponible en el modo de prueba</value>
+  </data>
+  <data name="ErrorUnknown" xml:space="preserve">
+    <value>Se produjo un error desconocido</value>
+  </data>
+  <data name="Exit" xml:space="preserve">
+    <value>Salir</value>
+  </data>
+  <data name="FindSessions" xml:space="preserve">
+    <value>Buscar sesiones</value>
+  </data>
+  <data name="HostSuffix" xml:space="preserve">
+    <value> (anfitrión)</value>
+  </data>
+  <data name="JoinSession" xml:space="preserve">
+    <value>Unirse a la sesión</value>
+  </data>
+  <data name="LeaveSession" xml:space="preserve">
+    <value>Abandonar sesión</value>
+  </data>
+  <data name="Loading" xml:space="preserve">
+    <value>Cargando</value>
+  </data>
+  <data name="Lobby" xml:space="preserve">
+    <value>Lobby</value>
+  </data>
+  <data name="MainMenu" xml:space="preserve">
+    <value>Menú principal</value>
+  </data>
+  <data name="MessageBoxUsage" xml:space="preserve">
+    <value>
+Botón A, Espacio, Enter = OK
+Botón B, Esc = Cancelar</value>
+  </data>
+  <data name="MessageGamerJoined" xml:space="preserve">
+    <value>{0} se ha unido</value>
+  </data>
+  <data name="MessageGamerLeft" xml:space="preserve">
+    <value>{0} se ha ido</value>
+  </data>
+  <data name="NetworkBusy" xml:space="preserve">
+    <value>Buscando...</value>
+  </data>
+  <data name="NoSessionsFound" xml:space="preserve">
+    <value>No se encontraron sesiones</value>
+  </data>
+  <data name="Paused" xml:space="preserve">
+    <value>En pausa</value>
+  </data>
+  <data name="PlayerMatch" xml:space="preserve">
+    <value>EN LÍNEA</value>
+  </data>
+  <data name="QuitGame" xml:space="preserve">
+    <value>Salir</value>
+  </data>
+  <data name="ResumeGame" xml:space="preserve">
+    <value>Reanudar</value>
+  </data>
+  <data name="ReturnToLobby" xml:space="preserve">
+    <value>Volver al lobby</value>
+  </data>
+  <data name="SinglePlayer" xml:space="preserve">
+    <value>Un jugador</value>
+  </data>
+  <data name="SystemLink" xml:space="preserve">
+    <value>Sesión LAN</value>
+  </data>
+  <data name="Play" xml:space="preserve">
+    <value>Jugar</value>
+  </data>
+  <data name="Theme" xml:space="preserve">
+    <value>Tema</value>
+  </data>
+  <data name="Settings" xml:space="preserve">
+    <value>Configuración</value>
+  </data>
+  <data name="Quit" xml:space="preserve">
+    <value>Salir</value>
+  </data>
+  <data name="HostNewGame" xml:space="preserve">
+    <value>Nuevo Juego</value>
+  </data>
+  <data name="Refresh" xml:space="preserve">
+    <value>Actualizar</value>
+  </data>
+  <data name="StartGame" xml:space="preserve">
+    <value>Comenzar</value>
+  </data>
+  <data name="AvailableGames" xml:space="preserve">
+    <value>Juegos disponibles:</value>
+  </data>
+  <data name="Players" xml:space="preserve">
+    <value>Jugadores:</value>
+  </data>
+  <data name="Deck" xml:space="preserve">
+    <value>Baraja</value>
+  </data>
+  <data name="Return" xml:space="preserve">
+    <value>Volver</value>
+  </data>
+  <data name="SearchingForGames" xml:space="preserve">
+    <value>Buscando juegos...</value>
+  </data>
+  <data name="FoundGames" xml:space="preserve">
+    <value>{0} juego{1} encontrado{1}</value>
+  </data>
+  <data name="AutoRefreshIn" xml:space="preserve">
+    <value>Actualización automática en {0}s</value>
+  </data>
+  <data name="Dealer" xml:space="preserve">
+    <value>Crupier: Casa</value>
+  </data>
+  <data name="Host" xml:space="preserve">
+    <value>ANFITRIÓN</value>
+  </data>
+  <data name="Slot" xml:space="preserve">
+    <value>Ranura {0}: {1}</value>
+  </data>
+  <data name="JoinOrHostGame" xml:space="preserve">
+    <value>Unirse o alojar un juego</value>
+  </data>
+  <data name="FailedToCreateSession" xml:space="preserve">
+    <value>No se pudo crear la sesión.</value>
+  </data>
+  <data name="FailedToJoinSession" xml:space="preserve">
+    <value>No se pudo unir a la sesión.</value>
+  </data>
+  <data name="OK" xml:space="preserve">
+    <value>OK</value>
+  </data>
+  <data name="SettingsTitle" xml:space="preserve">
+    <value>Configuración</value>
+  </data>
+  <data name="SettingsDisplay" xml:space="preserve">
+    <value>PANTALLA</value>
+  </data>
+  <data name="SettingsAudio" xml:space="preserve">
+    <value>AUDIO</value>
+  </data>
+  <data name="SettingsAIPlayers" xml:space="preserve">
+    <value>JUGADORES IA</value>
+  </data>
+  <data name="SettingsGameplay" xml:space="preserve">
+    <value>JUGABILIDAD</value>
+  </data>
+  <data name="SettingsLanguage" xml:space="preserve">
+    <value>Idioma</value>
+  </data>
+  <data name="SettingsCardBackTheme" xml:space="preserve">
+    <value>Tema del reverso de carta</value>
+  </data>
+  <data name="SettingsCurrency" xml:space="preserve">
+    <value>Moneda</value>
+  </data>
+  <data name="SettingsSoundVolume" xml:space="preserve">
+    <value>Volumen de sonido</value>
+  </data>
+  <data name="SettingsMusicVolume" xml:space="preserve">
+    <value>Volumen de música</value>
+  </data>
+  <data name="SettingsMaxAIPlayers" xml:space="preserve">
+    <value>Jugadores IA máximos</value>
+  </data>
+  <data name="SettingsFillEmptySlots" xml:space="preserve">
+    <value>Llenar ranuras vacías</value>
+  </data>
+  <data name="SettingsAnimationSpeed" xml:space="preserve">
+    <value>Velocidad de animación</value>
+  </data>
+  <data name="SettingsAutoStandOn21" xml:space="preserve">
+    <value>Plantarse automáticamente en 21</value>
+  </data>
+  <data name="SettingsShowCardCount" xml:space="preserve">
+    <value>Mostrar conteo de cartas</value>
+  </data>
+  <data name="SettingsPersistWinnings" xml:space="preserve">
+    <value>Mantener ganancias</value>
+  </data>
+  <data name="Previous" xml:space="preserve">
+    <value>Anterior</value>
+  </data>
+  <data name="Next" xml:space="preserve">
+    <value>Siguiente</value>
+  </data>
+  <data name="PageIndicator" xml:space="preserve">
+    <value>Página {0} de {1}</value>
+  </data>
+  <data name="Deal" xml:space="preserve">
+    <value>Repartir</value>
+  </data>
+  <data name="Clear" xml:space="preserve">
+    <value>Borrar</value>
+  </data>
+  <data name="Hit" xml:space="preserve">
+    <value>Pedir</value>
+  </data>
+  <data name="Stand" xml:space="preserve">
+    <value>Plantarse</value>
+  </data>
+  <data name="Double" xml:space="preserve">
+    <value>Doblar</value>
+  </data>
+  <data name="Split" xml:space="preserve">
+    <value>Dividir</value>
+  </data>
+  <data name="Insurance" xml:space="preserve">
+    <value>Seguro</value>
+  </data>
+  <data name="NewHand" xml:space="preserve">
+    <value>Nueva mano</value>
+  </data>
+  <data name="AIPlayer1" xml:space="preserve">
+    <value>Benito</value>
+  </data>
+  <data name="AIPlayer2" xml:space="preserve">
+    <value>Carlos</value>
+  </data>
+  <data name="AIPlayer3" xml:space="preserve">
+    <value>Diana</value>
+  </data>
+  <data name="AIPlayer4" xml:space="preserve">
+    <value>Eduardo</value>
+  </data>
+  <data name="AIPlayer5" xml:space="preserve">
+    <value>Fernanda</value>
+  </data>
+  <data name="AIPlayer6" xml:space="preserve">
+    <value>Gerardo</value>
+  </data>
+</root>

+ 353 - 0
CardsStarterKit/Core/Game/Resources.fr.resx

@@ -0,0 +1,353 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <data name="Back" xml:space="preserve">
+    <value>Retour</value>
+  </data>
+  <data name="ConfirmEndSession" xml:space="preserve">
+    <value>Êtes-vous sûr de vouloir terminer cette session ?</value>
+  </data>
+  <data name="ConfirmExitSample" xml:space="preserve">
+    <value>Êtes-vous sûr de vouloir quitter cet exemple ?</value>
+  </data>
+  <data name="ConfirmForceStartGame" xml:space="preserve">
+    <value>Êtes-vous sûr de vouloir commencer le jeu,
+même si tous les joueurs ne sont pas prêts ?</value>
+  </data>
+  <data name="ConfirmLeaveSession" xml:space="preserve">
+    <value>Êtes-vous sûr de vouloir quitter cette session ?</value>
+  </data>
+  <data name="ConfirmMarketplace" xml:space="preserve">
+    <value>Le jeu en ligne n'est pas disponible en mode d'essai.
+Souhaitez-vous acheter ce jeu ?</value>
+  </data>
+  <data name="ConfirmQuitGame" xml:space="preserve">
+    <value>Êtes-vous sûr de vouloir quitter ce jeu ?</value>
+  </data>
+  <data name="CreateSession" xml:space="preserve">
+    <value>Créer une session</value>
+  </data>
+  <data name="EndSession" xml:space="preserve">
+    <value>Terminer la session</value>
+  </data>
+  <data name="ErrorDisconnected" xml:space="preserve">
+    <value>Connexion perdue à la session réseau</value>
+  </data>
+  <data name="ErrorGamerPrivilege" xml:space="preserve">
+    <value>Vous devez vous connecter avec un profil de joueur approprié
+pour accéder à cette fonctionnalité</value>
+  </data>
+  <data name="ErrorHostEndedSession" xml:space="preserve">
+    <value>L'hôte a terminé la session</value>
+  </data>
+  <data name="ErrorNetwork" xml:space="preserve">
+    <value>Une erreur s'est produite lors de
+l'accès au réseau</value>
+  </data>
+  <data name="ErrorNetworkNotAvailable" xml:space="preserve">
+    <value>Le réseau est désactivé
+ou non connecté</value>
+  </data>
+  <data name="ErrorRemovedByHost" xml:space="preserve">
+    <value>L'hôte vous a expulsé de la session</value>
+  </data>
+  <data name="ErrorSessionFull" xml:space="preserve">
+    <value>Cette session est déjà complète</value>
+  </data>
+  <data name="ErrorSessionNotFound" xml:space="preserve">
+    <value>Session introuvable. Elle a peut-être pris fin,
+ou il n'y a peut-être pas de connectivité réseau
+entre la machine locale et l'hôte de la session</value>
+  </data>
+  <data name="ErrorSessionNotJoinable" xml:space="preserve">
+    <value>Vous devez attendre que l'hôte retourne au
+hall avant de pouvoir rejoindre cette session</value>
+  </data>
+  <data name="ErrorTrialMode" xml:space="preserve">
+    <value>Cette fonctionnalité n'est pas disponible en mode d'essai</value>
+  </data>
+  <data name="ErrorUnknown" xml:space="preserve">
+    <value>Une erreur inconnue s'est produite</value>
+  </data>
+  <data name="Exit" xml:space="preserve">
+    <value>Quitter</value>
+  </data>
+  <data name="FindSessions" xml:space="preserve">
+    <value>Trouver des sessions</value>
+  </data>
+  <data name="HostSuffix" xml:space="preserve">
+    <value> (hôte)</value>
+  </data>
+  <data name="JoinSession" xml:space="preserve">
+    <value>Rejoindre la session</value>
+  </data>
+  <data name="LeaveSession" xml:space="preserve">
+    <value>Quitter la session</value>
+  </data>
+  <data name="Loading" xml:space="preserve">
+    <value>Chargement</value>
+  </data>
+  <data name="Lobby" xml:space="preserve">
+    <value>Hall</value>
+  </data>
+  <data name="MainMenu" xml:space="preserve">
+    <value>Menu principal</value>
+  </data>
+  <data name="MessageBoxUsage" xml:space="preserve">
+    <value>
+Bouton A, Espace, Entrée = OK
+Bouton B, Échap = Annuler</value>
+  </data>
+  <data name="MessageGamerJoined" xml:space="preserve">
+    <value>{0} a rejoint</value>
+  </data>
+  <data name="MessageGamerLeft" xml:space="preserve">
+    <value>{0} est parti</value>
+  </data>
+  <data name="NetworkBusy" xml:space="preserve">
+    <value>Recherche...</value>
+  </data>
+  <data name="NoSessionsFound" xml:space="preserve">
+    <value>Aucune session trouvée</value>
+  </data>
+  <data name="Paused" xml:space="preserve">
+    <value>En pause</value>
+  </data>
+  <data name="PlayerMatch" xml:space="preserve">
+    <value>EN LIGNE</value>
+  </data>
+  <data name="QuitGame" xml:space="preserve">
+    <value>Quitter</value>
+  </data>
+  <data name="ResumeGame" xml:space="preserve">
+    <value>Reprendre</value>
+  </data>
+  <data name="ReturnToLobby" xml:space="preserve">
+    <value>Retour au hall</value>
+  </data>
+  <data name="SinglePlayer" xml:space="preserve">
+    <value>Joueur solo</value>
+  </data>
+  <data name="SystemLink" xml:space="preserve">
+    <value>Session LAN</value>
+  </data>
+  <data name="Play" xml:space="preserve">
+    <value>Jouer</value>
+  </data>
+  <data name="Theme" xml:space="preserve">
+    <value>Thème</value>
+  </data>
+  <data name="Settings" xml:space="preserve">
+    <value>Paramètres</value>
+  </data>
+  <data name="Quit" xml:space="preserve">
+    <value>Quitter</value>
+  </data>
+  <data name="HostNewGame" xml:space="preserve">
+    <value>Nouvelle Partie</value>
+  </data>
+  <data name="Refresh" xml:space="preserve">
+    <value>Actualiser</value>
+  </data>
+  <data name="StartGame" xml:space="preserve">
+    <value>Commencer</value>
+  </data>
+  <data name="AvailableGames" xml:space="preserve">
+    <value>Jeux disponibles :</value>
+  </data>
+  <data name="Players" xml:space="preserve">
+    <value>Joueurs :</value>
+  </data>
+  <data name="Deck" xml:space="preserve">
+    <value>Paquet</value>
+  </data>
+  <data name="Return" xml:space="preserve">
+    <value>Retour</value>
+  </data>
+  <data name="SearchingForGames" xml:space="preserve">
+    <value>Recherche de jeux...</value>
+  </data>
+  <data name="FoundGames" xml:space="preserve">
+    <value>{0} jeu{1} trouvé{1}</value>
+  </data>
+  <data name="AutoRefreshIn" xml:space="preserve">
+    <value>Actualisation automatique dans {0}s</value>
+  </data>
+  <data name="Dealer" xml:space="preserve">
+    <value>Croupier : Maison</value>
+  </data>
+  <data name="Host" xml:space="preserve">
+    <value>HÔTE</value>
+  </data>
+  <data name="Slot" xml:space="preserve">
+    <value>Place {0} : {1}</value>
+  </data>
+  <data name="JoinOrHostGame" xml:space="preserve">
+    <value>Rejoindre ou héberger une partie</value>
+  </data>
+  <data name="FailedToCreateSession" xml:space="preserve">
+    <value>Échec de la création de la session.</value>
+  </data>
+  <data name="FailedToJoinSession" xml:space="preserve">
+    <value>Échec de l'adhésion à la session.</value>
+  </data>
+  <data name="OK" xml:space="preserve">
+    <value>OK</value>
+  </data>
+  <data name="SettingsTitle" xml:space="preserve">
+    <value>Paramètres</value>
+  </data>
+  <data name="SettingsDisplay" xml:space="preserve">
+    <value>AFFICHAGE</value>
+  </data>
+  <data name="SettingsAudio" xml:space="preserve">
+    <value>AUDIO</value>
+  </data>
+  <data name="SettingsAIPlayers" xml:space="preserve">
+    <value>JOUEURS IA</value>
+  </data>
+  <data name="SettingsGameplay" xml:space="preserve">
+    <value>JEU</value>
+  </data>
+  <data name="SettingsLanguage" xml:space="preserve">
+    <value>Langue</value>
+  </data>
+  <data name="SettingsCardBackTheme" xml:space="preserve">
+    <value>Thème du dos de carte</value>
+  </data>
+  <data name="SettingsCurrency" xml:space="preserve">
+    <value>Devise</value>
+  </data>
+  <data name="SettingsSoundVolume" xml:space="preserve">
+    <value>Volume sonore</value>
+  </data>
+  <data name="SettingsMusicVolume" xml:space="preserve">
+    <value>Volume de la musique</value>
+  </data>
+  <data name="SettingsMaxAIPlayers" xml:space="preserve">
+    <value>Joueurs IA maximum</value>
+  </data>
+  <data name="SettingsFillEmptySlots" xml:space="preserve">
+    <value>Remplir les places vides</value>
+  </data>
+  <data name="SettingsAnimationSpeed" xml:space="preserve">
+    <value>Vitesse d'animation</value>
+  </data>
+  <data name="SettingsAutoStandOn21" xml:space="preserve">
+    <value>Rester automatiquement sur 21</value>
+  </data>
+  <data name="SettingsShowCardCount" xml:space="preserve">
+    <value>Afficher le compte de cartes</value>
+  </data>
+  <data name="SettingsPersistWinnings" xml:space="preserve">
+    <value>Conserver les gains</value>
+  </data>
+  <data name="Previous" xml:space="preserve">
+    <value>Précédent</value>
+  </data>
+  <data name="Next" xml:space="preserve">
+    <value>Suivant</value>
+  </data>
+  <data name="PageIndicator" xml:space="preserve">
+    <value>Page {0} sur {1}</value>
+  </data>
+  <data name="Deal" xml:space="preserve">
+    <value>Distribuer</value>
+  </data>
+  <data name="Clear" xml:space="preserve">
+    <value>Effacer</value>
+  </data>
+  <data name="Hit" xml:space="preserve">
+    <value>Tirer</value>
+  </data>
+  <data name="Stand" xml:space="preserve">
+    <value>Rester</value>
+  </data>
+  <data name="Double" xml:space="preserve">
+    <value>Doubler</value>
+  </data>
+  <data name="Split" xml:space="preserve">
+    <value>Séparer</value>
+  </data>
+  <data name="Insurance" xml:space="preserve">
+    <value>Assurance</value>
+  </data>
+  <data name="NewHand" xml:space="preserve">
+    <value>Nouvelle main</value>
+  </data>
+  <data name="AIPlayer1" xml:space="preserve">
+    <value>Benoît</value>
+  </data>
+  <data name="AIPlayer2" xml:space="preserve">
+    <value>Charles</value>
+  </data>
+  <data name="AIPlayer3" xml:space="preserve">
+    <value>Diane</value>
+  </data>
+  <data name="AIPlayer4" xml:space="preserve">
+    <value>Édouard</value>
+  </data>
+  <data name="AIPlayer5" xml:space="preserve">
+    <value>Florence</value>
+  </data>
+  <data name="AIPlayer6" xml:space="preserve">
+    <value>Georges</value>
+  </data>
+</root>

+ 353 - 0
CardsStarterKit/Core/Game/Resources.it.resx

@@ -0,0 +1,353 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <data name="Back" xml:space="preserve">
+    <value>Indietro</value>
+  </data>
+  <data name="ConfirmEndSession" xml:space="preserve">
+    <value>Sei sicuro di voler terminare questa sessione?</value>
+  </data>
+  <data name="ConfirmExitSample" xml:space="preserve">
+    <value>Sei sicuro di voler uscire da questo esempio?</value>
+  </data>
+  <data name="ConfirmForceStartGame" xml:space="preserve">
+    <value>Sei sicuro di voler iniziare il gioco,
+anche se non tutti i giocatori sono pronti?</value>
+  </data>
+  <data name="ConfirmLeaveSession" xml:space="preserve">
+    <value>Sei sicuro di voler abbandonare questa sessione?</value>
+  </data>
+  <data name="ConfirmMarketplace" xml:space="preserve">
+    <value>Il gioco online non è disponibile in modalità prova.
+Vorresti acquistare questo gioco?</value>
+  </data>
+  <data name="ConfirmQuitGame" xml:space="preserve">
+    <value>Sei sicuro di voler uscire da questo gioco?</value>
+  </data>
+  <data name="CreateSession" xml:space="preserve">
+    <value>Crea sessione</value>
+  </data>
+  <data name="EndSession" xml:space="preserve">
+    <value>Termina sessione</value>
+  </data>
+  <data name="ErrorDisconnected" xml:space="preserve">
+    <value>Connessione persa alla sessione di rete</value>
+  </data>
+  <data name="ErrorGamerPrivilege" xml:space="preserve">
+    <value>Devi accedere con un profilo giocatore appropriato
+per accedere a questa funzione</value>
+  </data>
+  <data name="ErrorHostEndedSession" xml:space="preserve">
+    <value>L'host ha terminato la sessione</value>
+  </data>
+  <data name="ErrorNetwork" xml:space="preserve">
+    <value>Si è verificato un errore durante
+l'accesso alla rete</value>
+  </data>
+  <data name="ErrorNetworkNotAvailable" xml:space="preserve">
+    <value>La rete è disabilitata
+o non è connessa</value>
+  </data>
+  <data name="ErrorRemovedByHost" xml:space="preserve">
+    <value>L'host ti ha espulso dalla sessione</value>
+  </data>
+  <data name="ErrorSessionFull" xml:space="preserve">
+    <value>Questa sessione è già completa</value>
+  </data>
+  <data name="ErrorSessionNotFound" xml:space="preserve">
+    <value>Sessione non trovata. Potrebbe essere terminata,
+o potrebbe non esserci connettività di rete
+tra la macchina locale e l'host della sessione</value>
+  </data>
+  <data name="ErrorSessionNotJoinable" xml:space="preserve">
+    <value>Devi aspettare che l'host torni alla
+lobby prima di unirti a questa sessione</value>
+  </data>
+  <data name="ErrorTrialMode" xml:space="preserve">
+    <value>Questa funzione non è disponibile in modalità prova</value>
+  </data>
+  <data name="ErrorUnknown" xml:space="preserve">
+    <value>Si è verificato un errore sconosciuto</value>
+  </data>
+  <data name="Exit" xml:space="preserve">
+    <value>Esci</value>
+  </data>
+  <data name="FindSessions" xml:space="preserve">
+    <value>Trova sessioni</value>
+  </data>
+  <data name="HostSuffix" xml:space="preserve">
+    <value> (host)</value>
+  </data>
+  <data name="JoinSession" xml:space="preserve">
+    <value>Unisciti alla sessione</value>
+  </data>
+  <data name="LeaveSession" xml:space="preserve">
+    <value>Abbandona sessione</value>
+  </data>
+  <data name="Loading" xml:space="preserve">
+    <value>Caricamento</value>
+  </data>
+  <data name="Lobby" xml:space="preserve">
+    <value>Lobby</value>
+  </data>
+  <data name="MainMenu" xml:space="preserve">
+    <value>Menu principale</value>
+  </data>
+  <data name="MessageBoxUsage" xml:space="preserve">
+    <value>
+Pulsante A, Spazio, Invio = OK
+Pulsante B, Esc = Annulla</value>
+  </data>
+  <data name="MessageGamerJoined" xml:space="preserve">
+    <value>{0} si è unito</value>
+  </data>
+  <data name="MessageGamerLeft" xml:space="preserve">
+    <value>{0} se n'è andato</value>
+  </data>
+  <data name="NetworkBusy" xml:space="preserve">
+    <value>Ricerca...</value>
+  </data>
+  <data name="NoSessionsFound" xml:space="preserve">
+    <value>Nessuna sessione trovata</value>
+  </data>
+  <data name="Paused" xml:space="preserve">
+    <value>In pausa</value>
+  </data>
+  <data name="PlayerMatch" xml:space="preserve">
+    <value>ONLINE</value>
+  </data>
+  <data name="QuitGame" xml:space="preserve">
+    <value>Esci</value>
+  </data>
+  <data name="ResumeGame" xml:space="preserve">
+    <value>Riprendi</value>
+  </data>
+  <data name="ReturnToLobby" xml:space="preserve">
+    <value>Torna alla lobby</value>
+  </data>
+  <data name="SinglePlayer" xml:space="preserve">
+    <value>Giocatore singolo</value>
+  </data>
+  <data name="SystemLink" xml:space="preserve">
+    <value>Sessione LAN</value>
+  </data>
+  <data name="Play" xml:space="preserve">
+    <value>Gioca</value>
+  </data>
+  <data name="Theme" xml:space="preserve">
+    <value>Tema</value>
+  </data>
+  <data name="Settings" xml:space="preserve">
+    <value>Impostazioni</value>
+  </data>
+  <data name="Quit" xml:space="preserve">
+    <value>Esci</value>
+  </data>
+  <data name="HostNewGame" xml:space="preserve">
+    <value>Nuovo Gioco</value>
+  </data>
+  <data name="Refresh" xml:space="preserve">
+    <value>Aggiorna</value>
+  </data>
+  <data name="StartGame" xml:space="preserve">
+    <value>Inizia</value>
+  </data>
+  <data name="AvailableGames" xml:space="preserve">
+    <value>Giochi disponibili:</value>
+  </data>
+  <data name="Players" xml:space="preserve">
+    <value>Giocatori:</value>
+  </data>
+  <data name="Deck" xml:space="preserve">
+    <value>Mazzo</value>
+  </data>
+  <data name="Return" xml:space="preserve">
+    <value>Torna</value>
+  </data>
+  <data name="SearchingForGames" xml:space="preserve">
+    <value>Ricerca di giochi...</value>
+  </data>
+  <data name="FoundGames" xml:space="preserve">
+    <value>{0} gioco{1} trovato{1}</value>
+  </data>
+  <data name="AutoRefreshIn" xml:space="preserve">
+    <value>Aggiornamento automatico tra {0}s</value>
+  </data>
+  <data name="Dealer" xml:space="preserve">
+    <value>Banco: Casa</value>
+  </data>
+  <data name="Host" xml:space="preserve">
+    <value>HOST</value>
+  </data>
+  <data name="Slot" xml:space="preserve">
+    <value>Posto {0}: {1}</value>
+  </data>
+  <data name="JoinOrHostGame" xml:space="preserve">
+    <value>Unisciti o ospita un gioco</value>
+  </data>
+  <data name="FailedToCreateSession" xml:space="preserve">
+    <value>Impossibile creare la sessione.</value>
+  </data>
+  <data name="FailedToJoinSession" xml:space="preserve">
+    <value>Impossibile unirsi alla sessione.</value>
+  </data>
+  <data name="OK" xml:space="preserve">
+    <value>OK</value>
+  </data>
+  <data name="SettingsTitle" xml:space="preserve">
+    <value>Impostazioni</value>
+  </data>
+  <data name="SettingsDisplay" xml:space="preserve">
+    <value>SCHERMO</value>
+  </data>
+  <data name="SettingsAudio" xml:space="preserve">
+    <value>AUDIO</value>
+  </data>
+  <data name="SettingsAIPlayers" xml:space="preserve">
+    <value>GIOCATORI IA</value>
+  </data>
+  <data name="SettingsGameplay" xml:space="preserve">
+    <value>GAMEPLAY</value>
+  </data>
+  <data name="SettingsLanguage" xml:space="preserve">
+    <value>Lingua</value>
+  </data>
+  <data name="SettingsCardBackTheme" xml:space="preserve">
+    <value>Tema retro carta</value>
+  </data>
+  <data name="SettingsCurrency" xml:space="preserve">
+    <value>Valuta</value>
+  </data>
+  <data name="SettingsSoundVolume" xml:space="preserve">
+    <value>Volume suono</value>
+  </data>
+  <data name="SettingsMusicVolume" xml:space="preserve">
+    <value>Volume musica</value>
+  </data>
+  <data name="SettingsMaxAIPlayers" xml:space="preserve">
+    <value>Giocatori IA massimi</value>
+  </data>
+  <data name="SettingsFillEmptySlots" xml:space="preserve">
+    <value>Riempi posti vuoti</value>
+  </data>
+  <data name="SettingsAnimationSpeed" xml:space="preserve">
+    <value>Velocità animazione</value>
+  </data>
+  <data name="SettingsAutoStandOn21" xml:space="preserve">
+    <value>Resta automaticamente su 21</value>
+  </data>
+  <data name="SettingsShowCardCount" xml:space="preserve">
+    <value>Mostra conteggio carte</value>
+  </data>
+  <data name="SettingsPersistWinnings" xml:space="preserve">
+    <value>Mantieni vincite</value>
+  </data>
+  <data name="Previous" xml:space="preserve">
+    <value>Precedente</value>
+  </data>
+  <data name="Next" xml:space="preserve">
+    <value>Successivo</value>
+  </data>
+  <data name="PageIndicator" xml:space="preserve">
+    <value>Pagina {0} di {1}</value>
+  </data>
+  <data name="Deal" xml:space="preserve">
+    <value>Distribuire</value>
+  </data>
+  <data name="Clear" xml:space="preserve">
+    <value>Cancella</value>
+  </data>
+  <data name="Hit" xml:space="preserve">
+    <value>Carta</value>
+  </data>
+  <data name="Stand" xml:space="preserve">
+    <value>Stai</value>
+  </data>
+  <data name="Double" xml:space="preserve">
+    <value>Raddoppia</value>
+  </data>
+  <data name="Split" xml:space="preserve">
+    <value>Dividi</value>
+  </data>
+  <data name="Insurance" xml:space="preserve">
+    <value>Assicurazione</value>
+  </data>
+  <data name="NewHand" xml:space="preserve">
+    <value>Nuova mano</value>
+  </data>
+  <data name="AIPlayer1" xml:space="preserve">
+    <value>Benedetto</value>
+  </data>
+  <data name="AIPlayer2" xml:space="preserve">
+    <value>Carlo</value>
+  </data>
+  <data name="AIPlayer3" xml:space="preserve">
+    <value>Diana</value>
+  </data>
+  <data name="AIPlayer4" xml:space="preserve">
+    <value>Edoardo</value>
+  </data>
+  <data name="AIPlayer5" xml:space="preserve">
+    <value>Fiorella</value>
+  </data>
+  <data name="AIPlayer6" xml:space="preserve">
+    <value>Giorgio</value>
+  </data>
+</root>

+ 352 - 0
CardsStarterKit/Core/Game/Resources.ja.resx

@@ -0,0 +1,352 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <data name="Back" xml:space="preserve">
+    <value>戻る</value>
+  </data>
+  <data name="ConfirmEndSession" xml:space="preserve">
+    <value>このセッションを終了してもよろしいですか?</value>
+  </data>
+  <data name="ConfirmExitSample" xml:space="preserve">
+    <value>このサンプルを終了してもよろしいですか?</value>
+  </data>
+  <data name="ConfirmForceStartGame" xml:space="preserve">
+    <value>すべてのプレイヤーが準備できていませんが、
+ゲームを開始してもよろしいですか?</value>
+  </data>
+  <data name="ConfirmLeaveSession" xml:space="preserve">
+    <value>このセッションから退出してもよろしいですか?</value>
+  </data>
+  <data name="ConfirmMarketplace" xml:space="preserve">
+    <value>試用版ではオンラインゲームプレイは利用できません。
+このゲームを購入しますか?</value>
+  </data>
+  <data name="ConfirmQuitGame" xml:space="preserve">
+    <value>このゲームを終了してもよろしいですか?</value>
+  </data>
+  <data name="CreateSession" xml:space="preserve">
+    <value>セッション作成</value>
+  </data>
+  <data name="EndSession" xml:space="preserve">
+    <value>セッション終了</value>
+  </data>
+  <data name="ErrorDisconnected" xml:space="preserve">
+    <value>ネットワークセッションとの接続が切断されました</value>
+  </data>
+  <data name="ErrorGamerPrivilege" xml:space="preserve">
+    <value>この機能にアクセスするには、
+適切なゲーマープロファイルでサインインする必要があります</value>
+  </data>
+  <data name="ErrorHostEndedSession" xml:space="preserve">
+    <value>ホストがセッションを終了しました</value>
+  </data>
+  <data name="ErrorNetwork" xml:space="preserve">
+    <value>ネットワークへのアクセス中に
+エラーが発生しました</value>
+  </data>
+  <data name="ErrorNetworkNotAvailable" xml:space="preserve">
+    <value>ネットワークがオフになっているか
+接続されていません</value>
+  </data>
+  <data name="ErrorRemovedByHost" xml:space="preserve">
+    <value>ホストによってセッションから追放されました</value>
+  </data>
+  <data name="ErrorSessionFull" xml:space="preserve">
+    <value>このセッションは既に満員です</value>
+  </data>
+  <data name="ErrorSessionNotFound" xml:space="preserve">
+    <value>セッションが見つかりません。終了したか、
+ローカルマシンとセッションホスト間に
+ネットワーク接続がない可能性があります</value>
+  </data>
+  <data name="ErrorSessionNotJoinable" xml:space="preserve">
+    <value>ホストがロビーに戻るまで待つ必要があります。
+その後、このセッションに参加できます</value>
+  </data>
+  <data name="ErrorTrialMode" xml:space="preserve">
+    <value>この機能は試用版では利用できません</value>
+  </data>
+  <data name="ErrorUnknown" xml:space="preserve">
+    <value>不明なエラーが発生しました</value>
+  </data>
+  <data name="Exit" xml:space="preserve">
+    <value>終了</value>
+  </data>
+  <data name="FindSessions" xml:space="preserve">
+    <value>セッション検索</value>
+  </data>
+  <data name="HostSuffix" xml:space="preserve">
+    <value>(ホスト)</value>
+  </data>
+  <data name="JoinSession" xml:space="preserve">
+    <value>セッション参加</value>
+  </data>
+  <data name="LeaveSession" xml:space="preserve">
+    <value>セッション退出</value>
+  </data>
+  <data name="Loading" xml:space="preserve">
+    <value>読み込み中</value>
+  </data>
+  <data name="Lobby" xml:space="preserve">
+    <value>ロビー</value>
+  </data>
+  <data name="MainMenu" xml:space="preserve">
+    <value>メインメニュー</value>
+  </data>
+  <data name="MessageBoxUsage" xml:space="preserve">
+    <value>Aボタン、スペース、Enter = OK
+Bボタン、Esc = キャンセル</value>
+  </data>
+  <data name="MessageGamerJoined" xml:space="preserve">
+    <value>{0}が参加しました</value>
+  </data>
+  <data name="MessageGamerLeft" xml:space="preserve">
+    <value>{0}が退出しました</value>
+  </data>
+  <data name="NetworkBusy" xml:space="preserve">
+    <value>検索中...</value>
+  </data>
+  <data name="NoSessionsFound" xml:space="preserve">
+    <value>セッションが見つかりません</value>
+  </data>
+  <data name="Paused" xml:space="preserve">
+    <value>一時停止</value>
+  </data>
+  <data name="PlayerMatch" xml:space="preserve">
+    <value>ライブ</value>
+  </data>
+  <data name="QuitGame" xml:space="preserve">
+    <value>ゲーム終了</value>
+  </data>
+  <data name="ResumeGame" xml:space="preserve">
+    <value>再開</value>
+  </data>
+  <data name="ReturnToLobby" xml:space="preserve">
+    <value>ロビーに戻る</value>
+  </data>
+  <data name="SinglePlayer" xml:space="preserve">
+    <value>シングルプレイヤー</value>
+  </data>
+  <data name="SystemLink" xml:space="preserve">
+    <value>LANセッション</value>
+  </data>
+  <data name="Play" xml:space="preserve">
+    <value>プレイ</value>
+  </data>
+  <data name="Theme" xml:space="preserve">
+    <value>テーマ</value>
+  </data>
+  <data name="Settings" xml:space="preserve">
+    <value>設定</value>
+  </data>
+  <data name="Quit" xml:space="preserve">
+    <value>終了</value>
+  </data>
+  <data name="HostNewGame" xml:space="preserve">
+    <value>新しいゲーム</value>
+  </data>
+  <data name="Refresh" xml:space="preserve">
+    <value>更新</value>
+  </data>
+  <data name="StartGame" xml:space="preserve">
+    <value>開始</value>
+  </data>
+  <data name="AvailableGames" xml:space="preserve">
+    <value>利用可能なゲーム </value>
+  </data>
+  <data name="Players" xml:space="preserve">
+    <value>プレイヤー </value>
+  </data>
+  <data name="Deck" xml:space="preserve">
+    <value>デッキ</value>
+  </data>
+  <data name="Return" xml:space="preserve">
+    <value>戻る</value>
+  </data>
+  <data name="SearchingForGames" xml:space="preserve">
+    <value>ゲームを検索中...</value>
+  </data>
+  <data name="FoundGames" xml:space="preserve">
+    <value>{0}個のゲームが見つかりました</value>
+  </data>
+  <data name="AutoRefreshIn" xml:space="preserve">
+    <value>{0}秒後に自動更新</value>
+  </data>
+  <data name="Dealer" xml:space="preserve">
+    <value>ディーラー ハウス</value>
+  </data>
+  <data name="Host" xml:space="preserve">
+    <value>ホスト</value>
+  </data>
+  <data name="Slot" xml:space="preserve">
+    <value>スロット{0} {1}</value>
+  </data>
+  <data name="JoinOrHostGame" xml:space="preserve">
+    <value>ゲームに参加またはホスト</value>
+  </data>
+  <data name="FailedToCreateSession" xml:space="preserve">
+    <value>セッションの作成に失敗しました</value>
+  </data>
+  <data name="FailedToJoinSession" xml:space="preserve">
+    <value>セッションへの参加に失敗しました</value>
+  </data>
+  <data name="OK" xml:space="preserve">
+    <value>OK</value>
+  </data>
+  <data name="SettingsTitle" xml:space="preserve">
+    <value>設定</value>
+  </data>
+  <data name="SettingsDisplay" xml:space="preserve">
+    <value>表示</value>
+  </data>
+  <data name="SettingsAudio" xml:space="preserve">
+    <value>オーディオ</value>
+  </data>
+  <data name="SettingsAIPlayers" xml:space="preserve">
+    <value>AIプレイヤー</value>
+  </data>
+  <data name="SettingsGameplay" xml:space="preserve">
+    <value>ゲームプレイ</value>
+  </data>
+  <data name="SettingsLanguage" xml:space="preserve">
+    <value>言語</value>
+  </data>
+  <data name="SettingsCardBackTheme" xml:space="preserve">
+    <value>カード裏面テーマ</value>
+  </data>
+  <data name="SettingsCurrency" xml:space="preserve">
+    <value>通貨</value>
+  </data>
+  <data name="SettingsSoundVolume" xml:space="preserve">
+    <value>効果音の音量</value>
+  </data>
+  <data name="SettingsMusicVolume" xml:space="preserve">
+    <value>音楽の音量</value>
+  </data>
+  <data name="SettingsMaxAIPlayers" xml:space="preserve">
+    <value>最大AIプレイヤー数</value>
+  </data>
+  <data name="SettingsFillEmptySlots" xml:space="preserve">
+    <value>空きスロットを埋める</value>
+  </data>
+  <data name="SettingsAnimationSpeed" xml:space="preserve">
+    <value>アニメーション速度</value>
+  </data>
+  <data name="SettingsAutoStandOn21" xml:space="preserve">
+    <value>21で自動スタンド</value>
+  </data>
+  <data name="SettingsShowCardCount" xml:space="preserve">
+    <value>カードカウントを表示</value>
+  </data>
+  <data name="SettingsPersistWinnings" xml:space="preserve">
+    <value>賞金を保持</value>
+  </data>
+  <data name="Previous" xml:space="preserve">
+    <value>前へ</value>
+  </data>
+  <data name="Next" xml:space="preserve">
+    <value>次へ</value>
+  </data>
+  <data name="PageIndicator" xml:space="preserve">
+    <value>ページ{0}/{1}</value>
+  </data>
+  <data name="Deal" xml:space="preserve">
+    <value>配る</value>
+  </data>
+  <data name="Clear" xml:space="preserve">
+    <value>クリア</value>
+  </data>
+  <data name="Hit" xml:space="preserve">
+    <value>ヒット</value>
+  </data>
+  <data name="Stand" xml:space="preserve">
+    <value>スタンド</value>
+  </data>
+  <data name="Double" xml:space="preserve">
+    <value>ダブル</value>
+  </data>
+  <data name="Split" xml:space="preserve">
+    <value>スプリット</value>
+  </data>
+  <data name="Insurance" xml:space="preserve">
+    <value>インシュランス</value>
+  </data>
+  <data name="NewHand" xml:space="preserve">
+    <value>新しい手</value>
+  </data>
+  <data name="AIPlayer1" xml:space="preserve">
+    <value>健太</value>
+  </data>
+  <data name="AIPlayer2" xml:space="preserve">
+    <value>大輔</value>
+  </data>
+  <data name="AIPlayer3" xml:space="preserve">
+    <value>恵美</value>
+  </data>
+  <data name="AIPlayer4" xml:space="preserve">
+    <value>一郎</value>
+  </data>
+  <data name="AIPlayer5" xml:space="preserve">
+    <value>花子</value>
+  </data>
+  <data name="AIPlayer6" xml:space="preserve">
+    <value>剛</value>
+  </data>
+</root>

BIN
CardsStarterKit/Core/Game/Resources.resources


+ 412 - 0
CardsStarterKit/Core/Game/Resources.resx

@@ -0,0 +1,412 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <data name="Back" xml:space="preserve">
+    <value>Back</value>
+  </data>
+  <data name="ConfirmEndSession" xml:space="preserve">
+    <value>Are you sure you want to end this session?</value>
+  </data>
+  <data name="ConfirmExitSample" xml:space="preserve">
+    <value>Are you sure you want to exit this sample?</value>
+  </data>
+  <data name="ConfirmForceStartGame" xml:space="preserve">
+    <value>Are you sure you want to start the game,
+even though not all players are ready?</value>
+  </data>
+  <data name="ConfirmLeaveSession" xml:space="preserve">
+    <value>Are you sure you want to leave this session?</value>
+  </data>
+  <data name="ConfirmMarketplace" xml:space="preserve">
+    <value>Online gameplay is not available in trial mode.
+Would you like to purchase this game?</value>
+  </data>
+  <data name="ConfirmQuitGame" xml:space="preserve">
+    <value>Are you sure you want to quit this game?</value>
+  </data>
+  <data name="CreateSession" xml:space="preserve">
+    <value>Create Session</value>
+  </data>
+  <data name="EndSession" xml:space="preserve">
+    <value>End Session</value>
+  </data>
+  <data name="ErrorDisconnected" xml:space="preserve">
+    <value>Lost connection to the network session</value>
+  </data>
+  <data name="ErrorGamerPrivilege" xml:space="preserve">
+    <value>You must sign in a suitable gamer profile
+in order to access this functionality</value>
+  </data>
+  <data name="ErrorHostEndedSession" xml:space="preserve">
+    <value>Host ended the session</value>
+  </data>
+  <data name="ErrorNetwork" xml:space="preserve">
+    <value>There was an error while
+accessing the network</value>
+  </data>
+  <data name="ErrorNetworkNotAvailable" xml:space="preserve">
+    <value>Networking is turned
+off or not connected</value>
+  </data>
+  <data name="ErrorRemovedByHost" xml:space="preserve">
+    <value>Host kicked you out of the session</value>
+  </data>
+  <data name="ErrorSessionFull" xml:space="preserve">
+    <value>This session is already full</value>
+  </data>
+  <data name="ErrorSessionNotFound" xml:space="preserve">
+    <value>Session not found. It may have ended,
+or there may be no network connectivity
+between the local machine and session host</value>
+  </data>
+  <data name="ErrorSessionNotJoinable" xml:space="preserve">
+    <value>You must wait for the host to return to
+the lobby before you can join this session</value>
+  </data>
+  <data name="ErrorTrialMode" xml:space="preserve">
+    <value>This functionality is not available in trial mode</value>
+  </data>
+  <data name="ErrorUnknown" xml:space="preserve">
+    <value>An unknown error occurred</value>
+  </data>
+  <data name="Exit" xml:space="preserve">
+    <value>Exit</value>
+  </data>
+  <data name="FindSessions" xml:space="preserve">
+    <value>Find Sessions</value>
+  </data>
+  <data name="HostSuffix" xml:space="preserve">
+    <value> (host)</value>
+  </data>
+  <data name="JoinSession" xml:space="preserve">
+    <value>Join Session</value>
+  </data>
+  <data name="LeaveSession" xml:space="preserve">
+    <value>Leave Session</value>
+  </data>
+  <data name="Loading" xml:space="preserve">
+    <value>Loading</value>
+  </data>
+  <data name="Lobby" xml:space="preserve">
+    <value>Lobby</value>
+  </data>
+  <data name="MainMenu" xml:space="preserve">
+    <value>Main Menu</value>
+  </data>
+  <data name="MessageBoxUsage" xml:space="preserve">
+    <value>
+A button, Space, Enter = ok
+B button, Esc = Cancel</value>
+  </data>
+  <data name="MessageGamerJoined" xml:space="preserve">
+    <value>{0} joined</value>
+  </data>
+  <data name="MessageGamerLeft" xml:space="preserve">
+    <value>{0} left</value>
+  </data>
+  <data name="NetworkBusy" xml:space="preserve">
+    <value>Searching...</value>
+  </data>
+  <data name="NoSessionsFound" xml:space="preserve">
+    <value>No sessions found</value>
+  </data>
+  <data name="Paused" xml:space="preserve">
+    <value>Paused</value>
+  </data>
+  <data name="PlayerMatch" xml:space="preserve">
+    <value>LIVE</value>
+  </data>
+  <data name="QuitGame" xml:space="preserve">
+    <value>Quit Game</value>
+  </data>
+  <data name="ResumeGame" xml:space="preserve">
+    <value>Resume</value>
+  </data>
+  <data name="ReturnToLobby" xml:space="preserve">
+    <value>Return to Lobby</value>
+  </data>
+  <data name="SinglePlayer" xml:space="preserve">
+    <value>Single Player</value>
+  </data>
+  <data name="SystemLink" xml:space="preserve">
+    <value>LAN Session</value>
+  </data>
+  <data name="Play" xml:space="preserve">
+    <value>Play</value>
+  </data>
+  <data name="Theme" xml:space="preserve">
+    <value>Theme</value>
+  </data>
+  <data name="Settings" xml:space="preserve">
+    <value>Settings</value>
+  </data>
+  <data name="Quit" xml:space="preserve">
+    <value>Quit</value>
+  </data>
+  <data name="HostNewGame" xml:space="preserve">
+    <value>New Game</value>
+  </data>
+  <data name="Refresh" xml:space="preserve">
+    <value>Refresh</value>
+  </data>
+  <data name="StartGame" xml:space="preserve">
+    <value>Start</value>
+  </data>
+  <data name="AvailableGames" xml:space="preserve">
+    <value>Available Games:</value>
+  </data>
+  <data name="Players" xml:space="preserve">
+    <value>Players:</value>
+  </data>
+  <data name="Deck" xml:space="preserve">
+    <value>Deck</value>
+  </data>
+  <data name="Return" xml:space="preserve">
+    <value>Return</value>
+  </data>
+  <data name="SearchingForGames" xml:space="preserve">
+    <value>Searching for games...</value>
+  </data>
+  <data name="FoundGames" xml:space="preserve">
+    <value>Found {0} game{1}</value>
+  </data>
+  <data name="AutoRefreshIn" xml:space="preserve">
+    <value>Auto-refresh in {0}s</value>
+  </data>
+  <data name="Dealer" xml:space="preserve">
+    <value>Dealer: House</value>
+  </data>
+  <data name="Host" xml:space="preserve">
+    <value>HOST</value>
+  </data>
+  <data name="Slot" xml:space="preserve">
+    <value>Slot {0}: {1}</value>
+  </data>
+  <data name="JoinOrHostGame" xml:space="preserve">
+    <value>Join or Host a Game</value>
+  </data>
+  <data name="FailedToCreateSession" xml:space="preserve">
+    <value>Failed to create session.</value>
+  </data>
+  <data name="FailedToJoinSession" xml:space="preserve">
+    <value>Failed to join session.</value>
+  </data>
+  <data name="OK" xml:space="preserve">
+    <value>OK</value>
+  </data>
+  <data name="SettingsTitle" xml:space="preserve">
+    <value>Settings</value>
+  </data>
+  <data name="SettingsDisplay" xml:space="preserve">
+    <value>DISPLAY</value>
+  </data>
+  <data name="SettingsAudio" xml:space="preserve">
+    <value>AUDIO</value>
+  </data>
+  <data name="SettingsAIPlayers" xml:space="preserve">
+    <value>AI PLAYERS</value>
+  </data>
+  <data name="SettingsGameplay" xml:space="preserve">
+    <value>GAMEPLAY</value>
+  </data>
+  <data name="SettingsLanguage" xml:space="preserve">
+    <value>Language</value>
+  </data>
+  <data name="SettingsCardBackTheme" xml:space="preserve">
+    <value>Card Back Theme</value>
+  </data>
+  <data name="SettingsCurrency" xml:space="preserve">
+    <value>Currency</value>
+  </data>
+  <data name="SettingsSoundVolume" xml:space="preserve">
+    <value>Sound Volume</value>
+  </data>
+  <data name="SettingsMusicVolume" xml:space="preserve">
+    <value>Music Volume</value>
+  </data>
+  <data name="SettingsMaxAIPlayers" xml:space="preserve">
+    <value>Max AI Players</value>
+  </data>
+  <data name="SettingsFillEmptySlots" xml:space="preserve">
+    <value>Fill Empty Slots</value>
+  </data>
+  <data name="SettingsAnimationSpeed" xml:space="preserve">
+    <value>Animation Speed</value>
+  </data>
+  <data name="SettingsAutoStandOn21" xml:space="preserve">
+    <value>Auto-Stand on 21</value>
+  </data>
+  <data name="SettingsShowCardCount" xml:space="preserve">
+    <value>Show Card Count</value>
+  </data>
+  <data name="SettingsPersistWinnings" xml:space="preserve">
+    <value>Persist Winnings</value>
+  </data>
+  <data name="Previous" xml:space="preserve">
+    <value>Previous</value>
+  </data>
+  <data name="Next" xml:space="preserve">
+    <value>Next</value>
+  </data>
+  <data name="PageIndicator" xml:space="preserve">
+    <value>Page {0} of {1}</value>
+  </data>
+  <data name="Deal" xml:space="preserve">
+    <value>Deal</value>
+  </data>
+  <data name="Clear" xml:space="preserve">
+    <value>Clear</value>
+  </data>
+  <data name="Hit" xml:space="preserve">
+    <value>Hit</value>
+  </data>
+  <data name="Stand" xml:space="preserve">
+    <value>Stand</value>
+  </data>
+  <data name="Double" xml:space="preserve">
+    <value>Double</value>
+  </data>
+  <data name="Split" xml:space="preserve">
+    <value>Split</value>
+  </data>
+  <data name="Insurance" xml:space="preserve">
+    <value>Insurance</value>
+  </data>
+  <data name="NewHand" xml:space="preserve">
+    <value>New Hand</value>
+  </data>
+  <data name="AIPlayer1" xml:space="preserve">
+    <value>Benny</value>
+  </data>
+  <data name="AIPlayer2" xml:space="preserve">
+    <value>Chuck</value>
+  </data>
+  <data name="AIPlayer3" xml:space="preserve">
+    <value>Diana</value>
+  </data>
+  <data name="AIPlayer4" xml:space="preserve">
+    <value>Eddie</value>
+  </data>
+  <data name="AIPlayer5" xml:space="preserve">
+    <value>Fiona</value>
+  </data>
+  <data name="AIPlayer6" xml:space="preserve">
+    <value>George</value>
+  </data>
+</root>

+ 352 - 0
CardsStarterKit/Core/Game/Resources.ru.resx

@@ -0,0 +1,352 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <data name="Back" xml:space="preserve">
+    <value>Назад</value>
+  </data>
+  <data name="ConfirmEndSession" xml:space="preserve">
+    <value>Вы уверены, что хотите завершить сессию?</value>
+  </data>
+  <data name="ConfirmExitSample" xml:space="preserve">
+    <value>Вы уверены, что хотите выйти из примера?</value>
+  </data>
+  <data name="ConfirmForceStartGame" xml:space="preserve">
+    <value>Вы уверены, что хотите начать игру,
+хотя не все игроки готовы?</value>
+  </data>
+  <data name="ConfirmLeaveSession" xml:space="preserve">
+    <value>Вы уверены, что хотите покинуть сессию?</value>
+  </data>
+  <data name="ConfirmMarketplace" xml:space="preserve">
+    <value>Онлайн-игра недоступна в пробной версии.
+Хотите приобрести эту игру?</value>
+  </data>
+  <data name="ConfirmQuitGame" xml:space="preserve">
+    <value>Вы уверены, что хотите выйти из игры?</value>
+  </data>
+  <data name="CreateSession" xml:space="preserve">
+    <value>Создать сессию</value>
+  </data>
+  <data name="EndSession" xml:space="preserve">
+    <value>Завершить сессию</value>
+  </data>
+  <data name="ErrorDisconnected" xml:space="preserve">
+    <value>Потеряно соединение с сетевой сессией</value>
+  </data>
+  <data name="ErrorGamerPrivilege" xml:space="preserve">
+    <value>Необходимо войти в подходящий профиль игрока
+для доступа к этой функции</value>
+  </data>
+  <data name="ErrorHostEndedSession" xml:space="preserve">
+    <value>Хост завершил сессию</value>
+  </data>
+  <data name="ErrorNetwork" xml:space="preserve">
+    <value>Произошла ошибка при
+доступе к сети</value>
+  </data>
+  <data name="ErrorNetworkNotAvailable" xml:space="preserve">
+    <value>Сеть отключена или
+не подключена</value>
+  </data>
+  <data name="ErrorRemovedByHost" xml:space="preserve">
+    <value>Хост исключил вас из сессии</value>
+  </data>
+  <data name="ErrorSessionFull" xml:space="preserve">
+    <value>Эта сессия уже заполнена</value>
+  </data>
+  <data name="ErrorSessionNotFound" xml:space="preserve">
+    <value>Сессия не найдена. Возможно, она завершилась,
+или нет сетевого подключения между
+локальным компьютером и хостом сессии</value>
+  </data>
+  <data name="ErrorSessionNotJoinable" xml:space="preserve">
+    <value>Необходимо дождаться возвращения хоста в
+лобби, прежде чем присоединиться к сессии</value>
+  </data>
+  <data name="ErrorTrialMode" xml:space="preserve">
+    <value>Эта функция недоступна в пробной версии</value>
+  </data>
+  <data name="ErrorUnknown" xml:space="preserve">
+    <value>Произошла неизвестная ошибка</value>
+  </data>
+  <data name="Exit" xml:space="preserve">
+    <value>Выход</value>
+  </data>
+  <data name="FindSessions" xml:space="preserve">
+    <value>Найти сессии</value>
+  </data>
+  <data name="HostSuffix" xml:space="preserve">
+    <value> (хост)</value>
+  </data>
+  <data name="JoinSession" xml:space="preserve">
+    <value>Присоединиться к сессии</value>
+  </data>
+  <data name="LeaveSession" xml:space="preserve">
+    <value>Покинуть сессию</value>
+  </data>
+  <data name="Loading" xml:space="preserve">
+    <value>Загрузка</value>
+  </data>
+  <data name="Lobby" xml:space="preserve">
+    <value>Лобби</value>
+  </data>
+  <data name="MainMenu" xml:space="preserve">
+    <value>Главное меню</value>
+  </data>
+  <data name="MessageBoxUsage" xml:space="preserve">
+    <value>Кнопка A, Пробел, Enter = OK
+Кнопка B, Esc = Отмена</value>
+  </data>
+  <data name="MessageGamerJoined" xml:space="preserve">
+    <value>{0} присоединился</value>
+  </data>
+  <data name="MessageGamerLeft" xml:space="preserve">
+    <value>{0} вышел</value>
+  </data>
+  <data name="NetworkBusy" xml:space="preserve">
+    <value>Поиск...</value>
+  </data>
+  <data name="NoSessionsFound" xml:space="preserve">
+    <value>Сессии не найдены</value>
+  </data>
+  <data name="Paused" xml:space="preserve">
+    <value>Пауза</value>
+  </data>
+  <data name="PlayerMatch" xml:space="preserve">
+    <value>ОНЛАЙН</value>
+  </data>
+  <data name="QuitGame" xml:space="preserve">
+    <value>Выход из игры</value>
+  </data>
+  <data name="ResumeGame" xml:space="preserve">
+    <value>Продолжить</value>
+  </data>
+  <data name="ReturnToLobby" xml:space="preserve">
+    <value>Вернуться в лобби</value>
+  </data>
+  <data name="SinglePlayer" xml:space="preserve">
+    <value>Одиночная игра</value>
+  </data>
+  <data name="SystemLink" xml:space="preserve">
+    <value>Локальная сессия</value>
+  </data>
+  <data name="Play" xml:space="preserve">
+    <value>Играть</value>
+  </data>
+  <data name="Theme" xml:space="preserve">
+    <value>Тема</value>
+  </data>
+  <data name="Settings" xml:space="preserve">
+    <value>Настройки</value>
+  </data>
+  <data name="Quit" xml:space="preserve">
+    <value>Выход</value>
+  </data>
+  <data name="HostNewGame" xml:space="preserve">
+    <value>Новая игра</value>
+  </data>
+  <data name="Refresh" xml:space="preserve">
+    <value>Обновить</value>
+  </data>
+  <data name="StartGame" xml:space="preserve">
+    <value>Начать</value>
+  </data>
+  <data name="AvailableGames" xml:space="preserve">
+    <value>Доступные игры:</value>
+  </data>
+  <data name="Players" xml:space="preserve">
+    <value>Игроки:</value>
+  </data>
+  <data name="Deck" xml:space="preserve">
+    <value>Колода</value>
+  </data>
+  <data name="Return" xml:space="preserve">
+    <value>Назад</value>
+  </data>
+  <data name="SearchingForGames" xml:space="preserve">
+    <value>Поиск игр...</value>
+  </data>
+  <data name="FoundGames" xml:space="preserve">
+    <value>Найдено игр: {0}</value>
+  </data>
+  <data name="AutoRefreshIn" xml:space="preserve">
+    <value>Автообновление через {0}с</value>
+  </data>
+  <data name="Dealer" xml:space="preserve">
+    <value>Дилер: Казино</value>
+  </data>
+  <data name="Host" xml:space="preserve">
+    <value>ХОСТ</value>
+  </data>
+  <data name="Slot" xml:space="preserve">
+    <value>Слот {0}: {1}</value>
+  </data>
+  <data name="JoinOrHostGame" xml:space="preserve">
+    <value>Присоединиться или создать игру</value>
+  </data>
+  <data name="FailedToCreateSession" xml:space="preserve">
+    <value>Не удалось создать сессию</value>
+  </data>
+  <data name="FailedToJoinSession" xml:space="preserve">
+    <value>Не удалось присоединиться к сессии</value>
+  </data>
+  <data name="OK" xml:space="preserve">
+    <value>ОК</value>
+  </data>
+  <data name="SettingsTitle" xml:space="preserve">
+    <value>Настройки</value>
+  </data>
+  <data name="SettingsDisplay" xml:space="preserve">
+    <value>ДИСПЛЕЙ</value>
+  </data>
+  <data name="SettingsAudio" xml:space="preserve">
+    <value>АУДИО</value>
+  </data>
+  <data name="SettingsAIPlayers" xml:space="preserve">
+    <value>ИИ ИГРОКИ</value>
+  </data>
+  <data name="SettingsGameplay" xml:space="preserve">
+    <value>ИГРОВОЙ ПРОЦЕСС</value>
+  </data>
+  <data name="SettingsLanguage" xml:space="preserve">
+    <value>Язык</value>
+  </data>
+  <data name="SettingsCardBackTheme" xml:space="preserve">
+    <value>Тема рубашки карты</value>
+  </data>
+  <data name="SettingsCurrency" xml:space="preserve">
+    <value>Валюта</value>
+  </data>
+  <data name="SettingsSoundVolume" xml:space="preserve">
+    <value>Громкость звука</value>
+  </data>
+  <data name="SettingsMusicVolume" xml:space="preserve">
+    <value>Громкость музыки</value>
+  </data>
+  <data name="SettingsMaxAIPlayers" xml:space="preserve">
+    <value>Макс. игроков ИИ</value>
+  </data>
+  <data name="SettingsFillEmptySlots" xml:space="preserve">
+    <value>Заполнить пустые места</value>
+  </data>
+  <data name="SettingsAnimationSpeed" xml:space="preserve">
+    <value>Скорость анимации</value>
+  </data>
+  <data name="SettingsAutoStandOn21" xml:space="preserve">
+    <value>Автостоп на 21</value>
+  </data>
+  <data name="SettingsShowCardCount" xml:space="preserve">
+    <value>Показать счёт карт</value>
+  </data>
+  <data name="SettingsPersistWinnings" xml:space="preserve">
+    <value>Сохранять выигрыш</value>
+  </data>
+  <data name="Previous" xml:space="preserve">
+    <value>Назад</value>
+  </data>
+  <data name="Next" xml:space="preserve">
+    <value>Далее</value>
+  </data>
+  <data name="PageIndicator" xml:space="preserve">
+    <value>Страница {0} из {1}</value>
+  </data>
+  <data name="Deal" xml:space="preserve">
+    <value>Раздать</value>
+  </data>
+  <data name="Clear" xml:space="preserve">
+    <value>Очистить</value>
+  </data>
+  <data name="Hit" xml:space="preserve">
+    <value>Взять</value>
+  </data>
+  <data name="Stand" xml:space="preserve">
+    <value>Остаться</value>
+  </data>
+  <data name="Double" xml:space="preserve">
+    <value>Удвоить</value>
+  </data>
+  <data name="Split" xml:space="preserve">
+    <value>Разделить</value>
+  </data>
+  <data name="Insurance" xml:space="preserve">
+    <value>Страховка</value>
+  </data>
+  <data name="NewHand" xml:space="preserve">
+    <value>Новая рука</value>
+  </data>
+  <data name="AIPlayer1" xml:space="preserve">
+    <value>Борис</value>
+  </data>
+  <data name="AIPlayer2" xml:space="preserve">
+    <value>Виктор</value>
+  </data>
+  <data name="AIPlayer3" xml:space="preserve">
+    <value>Дарья</value>
+  </data>
+  <data name="AIPlayer4" xml:space="preserve">
+    <value>Евгений</value>
+  </data>
+  <data name="AIPlayer5" xml:space="preserve">
+    <value>Фаина</value>
+  </data>
+  <data name="AIPlayer6" xml:space="preserve">
+    <value>Георгий</value>
+  </data>
+</root>

+ 352 - 0
CardsStarterKit/Core/Game/Resources.zh.resx

@@ -0,0 +1,352 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <data name="Back" xml:space="preserve">
+    <value>返回</value>
+  </data>
+  <data name="ConfirmEndSession" xml:space="preserve">
+    <value>确定要结束此会话吗?</value>
+  </data>
+  <data name="ConfirmExitSample" xml:space="preserve">
+    <value>确定要退出此示例吗?</value>
+  </data>
+  <data name="ConfirmForceStartGame" xml:space="preserve">
+    <value>确定要开始游戏吗?
+即使并非所有玩家都已准备好</value>
+  </data>
+  <data name="ConfirmLeaveSession" xml:space="preserve">
+    <value>确定要离开此会话吗?</value>
+  </data>
+  <data name="ConfirmMarketplace" xml:space="preserve">
+    <value>试用版中不提供在线游戏功能。
+是否购买此游戏?</value>
+  </data>
+  <data name="ConfirmQuitGame" xml:space="preserve">
+    <value>确定要退出此游戏吗?</value>
+  </data>
+  <data name="CreateSession" xml:space="preserve">
+    <value>创建会话</value>
+  </data>
+  <data name="EndSession" xml:space="preserve">
+    <value>结束会话</value>
+  </data>
+  <data name="ErrorDisconnected" xml:space="preserve">
+    <value>失去与网络会话的连接</value>
+  </data>
+  <data name="ErrorGamerPrivilege" xml:space="preserve">
+    <value>您必须登录合适的玩家配置文件
+才能访问此功能</value>
+  </data>
+  <data name="ErrorHostEndedSession" xml:space="preserve">
+    <value>主机已结束会话</value>
+  </data>
+  <data name="ErrorNetwork" xml:space="preserve">
+    <value>访问网络时
+发生错误</value>
+  </data>
+  <data name="ErrorNetworkNotAvailable" xml:space="preserve">
+    <value>网络已关闭或
+未连接</value>
+  </data>
+  <data name="ErrorRemovedByHost" xml:space="preserve">
+    <value>主机将您踢出会话</value>
+  </data>
+  <data name="ErrorSessionFull" xml:space="preserve">
+    <value>此会话已满</value>
+  </data>
+  <data name="ErrorSessionNotFound" xml:space="preserve">
+    <value>未找到会话。它可能已结束,
+或者本地计算机与会话主机之间
+没有网络连接</value>
+  </data>
+  <data name="ErrorSessionNotJoinable" xml:space="preserve">
+    <value>您必须等待主机返回大厅
+才能加入此会话</value>
+  </data>
+  <data name="ErrorTrialMode" xml:space="preserve">
+    <value>试用版中不提供此功能</value>
+  </data>
+  <data name="ErrorUnknown" xml:space="preserve">
+    <value>发生未知错误</value>
+  </data>
+  <data name="Exit" xml:space="preserve">
+    <value>退出</value>
+  </data>
+  <data name="FindSessions" xml:space="preserve">
+    <value>查找会话</value>
+  </data>
+  <data name="HostSuffix" xml:space="preserve">
+    <value>(主机)</value>
+  </data>
+  <data name="JoinSession" xml:space="preserve">
+    <value>加入会话</value>
+  </data>
+  <data name="LeaveSession" xml:space="preserve">
+    <value>离开会话</value>
+  </data>
+  <data name="Loading" xml:space="preserve">
+    <value>加载中</value>
+  </data>
+  <data name="Lobby" xml:space="preserve">
+    <value>大厅</value>
+  </data>
+  <data name="MainMenu" xml:space="preserve">
+    <value>主菜单</value>
+  </data>
+  <data name="MessageBoxUsage" xml:space="preserve">
+    <value>A按钮、空格、回车 = 确定
+B按钮、Esc = 取消</value>
+  </data>
+  <data name="MessageGamerJoined" xml:space="preserve">
+    <value>{0}已加入</value>
+  </data>
+  <data name="MessageGamerLeft" xml:space="preserve">
+    <value>{0}已离开</value>
+  </data>
+  <data name="NetworkBusy" xml:space="preserve">
+    <value>搜索中...</value>
+  </data>
+  <data name="NoSessionsFound" xml:space="preserve">
+    <value>未找到会话</value>
+  </data>
+  <data name="Paused" xml:space="preserve">
+    <value>已暂停</value>
+  </data>
+  <data name="PlayerMatch" xml:space="preserve">
+    <value>在线</value>
+  </data>
+  <data name="QuitGame" xml:space="preserve">
+    <value>退出游戏</value>
+  </data>
+  <data name="ResumeGame" xml:space="preserve">
+    <value>继续</value>
+  </data>
+  <data name="ReturnToLobby" xml:space="preserve">
+    <value>返回大厅</value>
+  </data>
+  <data name="SinglePlayer" xml:space="preserve">
+    <value>单人游戏</value>
+  </data>
+  <data name="SystemLink" xml:space="preserve">
+    <value>局域网会话</value>
+  </data>
+  <data name="Play" xml:space="preserve">
+    <value>开始</value>
+  </data>
+  <data name="Theme" xml:space="preserve">
+    <value>主题</value>
+  </data>
+  <data name="Settings" xml:space="preserve">
+    <value>设置</value>
+  </data>
+  <data name="Quit" xml:space="preserve">
+    <value>退出</value>
+  </data>
+  <data name="HostNewGame" xml:space="preserve">
+    <value>新游戏</value>
+  </data>
+  <data name="Refresh" xml:space="preserve">
+    <value>刷新</value>
+  </data>
+  <data name="StartGame" xml:space="preserve">
+    <value>开始</value>
+  </data>
+  <data name="AvailableGames" xml:space="preserve">
+    <value>可用游戏 </value>
+  </data>
+  <data name="Players" xml:space="preserve">
+    <value>玩家 </value>
+  </data>
+  <data name="Deck" xml:space="preserve">
+    <value>牌组</value>
+  </data>
+  <data name="Return" xml:space="preserve">
+    <value>返回</value>
+  </data>
+  <data name="SearchingForGames" xml:space="preserve">
+    <value>正在搜索游戏...</value>
+  </data>
+  <data name="FoundGames" xml:space="preserve">
+    <value>找到{0}个游戏</value>
+  </data>
+  <data name="AutoRefreshIn" xml:space="preserve">
+    <value>{0}秒后自动刷新</value>
+  </data>
+  <data name="Dealer" xml:space="preserve">
+    <value>庄家 赌场</value>
+  </data>
+  <data name="Host" xml:space="preserve">
+    <value>主机</value>
+  </data>
+  <data name="Slot" xml:space="preserve">
+    <value>槽位{0} {1}</value>
+  </data>
+  <data name="JoinOrHostGame" xml:space="preserve">
+    <value>加入或主持游戏</value>
+  </data>
+  <data name="FailedToCreateSession" xml:space="preserve">
+    <value>创建会话失败</value>
+  </data>
+  <data name="FailedToJoinSession" xml:space="preserve">
+    <value>加入会话失败</value>
+  </data>
+  <data name="OK" xml:space="preserve">
+    <value>确定</value>
+  </data>
+  <data name="SettingsTitle" xml:space="preserve">
+    <value>设置</value>
+  </data>
+  <data name="SettingsDisplay" xml:space="preserve">
+    <value>显示</value>
+  </data>
+  <data name="SettingsAudio" xml:space="preserve">
+    <value>音频</value>
+  </data>
+  <data name="SettingsAIPlayers" xml:space="preserve">
+    <value>AI玩家</value>
+  </data>
+  <data name="SettingsGameplay" xml:space="preserve">
+    <value>游戏玩法</value>
+  </data>
+  <data name="SettingsLanguage" xml:space="preserve">
+    <value>语言</value>
+  </data>
+  <data name="SettingsCardBackTheme" xml:space="preserve">
+    <value>卡背主题</value>
+  </data>
+  <data name="SettingsCurrency" xml:space="preserve">
+    <value>货币</value>
+  </data>
+  <data name="SettingsSoundVolume" xml:space="preserve">
+    <value>音效音量</value>
+  </data>
+  <data name="SettingsMusicVolume" xml:space="preserve">
+    <value>音乐音量</value>
+  </data>
+  <data name="SettingsMaxAIPlayers" xml:space="preserve">
+    <value>最大AI玩家数</value>
+  </data>
+  <data name="SettingsFillEmptySlots" xml:space="preserve">
+    <value>填充空位</value>
+  </data>
+  <data name="SettingsAnimationSpeed" xml:space="preserve">
+    <value>动画速度</value>
+  </data>
+  <data name="SettingsAutoStandOn21" xml:space="preserve">
+    <value>21点自动停牌</value>
+  </data>
+  <data name="SettingsShowCardCount" xml:space="preserve">
+    <value>显示牌数</value>
+  </data>
+  <data name="SettingsPersistWinnings" xml:space="preserve">
+    <value>保持奖金</value>
+  </data>
+  <data name="Previous" xml:space="preserve">
+    <value>上一页</value>
+  </data>
+  <data name="Next" xml:space="preserve">
+    <value>下一页</value>
+  </data>
+  <data name="PageIndicator" xml:space="preserve">
+    <value>第{0}页 共{1}页</value>
+  </data>
+  <data name="Deal" xml:space="preserve">
+    <value>发牌</value>
+  </data>
+  <data name="Clear" xml:space="preserve">
+    <value>清除</value>
+  </data>
+  <data name="Hit" xml:space="preserve">
+    <value>要牌</value>
+  </data>
+  <data name="Stand" xml:space="preserve">
+    <value>停牌</value>
+  </data>
+  <data name="Double" xml:space="preserve">
+    <value>加倍</value>
+  </data>
+  <data name="Split" xml:space="preserve">
+    <value>分牌</value>
+  </data>
+  <data name="Insurance" xml:space="preserve">
+    <value>保险</value>
+  </data>
+  <data name="NewHand" xml:space="preserve">
+    <value>新手牌</value>
+  </data>
+  <data name="AIPlayer1" xml:space="preserve">
+    <value>明浩</value>
+  </data>
+  <data name="AIPlayer2" xml:space="preserve">
+    <value>志强</value>
+  </data>
+  <data name="AIPlayer3" xml:space="preserve">
+    <value>丹妮</value>
+  </data>
+  <data name="AIPlayer4" xml:space="preserve">
+    <value>建国</value>
+  </data>
+  <data name="AIPlayer5" xml:space="preserve">
+    <value>芳芳</value>
+  </data>
+  <data name="AIPlayer6" xml:space="preserve">
+    <value>国强</value>
+  </data>
+</root>

+ 42 - 16
CardsStarterKit/Core/Game/ScreenManager/MenuEntry.cs

@@ -10,9 +10,30 @@ using Microsoft.Xna.Framework;
 using Microsoft.Xna.Framework.Graphics;
 using Blackjack;
 using CardsFramework;
+using Microsoft.Xna.Framework.Net;
 
 namespace GameStateManagement
 {
+    /// <summary>
+    /// MenuEntry subclass for displaying AvailableNetworkSession info.
+    /// </summary>
+    class AvailableSessionMenuEntry : MenuEntry
+    {
+        public AvailableNetworkSession AvailableSession { get; }
+
+        public AvailableSessionMenuEntry(AvailableNetworkSession session)
+            : base(GetMenuItemText(session))
+        {
+            AvailableSession = session;
+        }
+
+        static string GetMenuItemText(AvailableNetworkSession session)
+        {
+            int totalSlots = session.CurrentGamerCount + session.OpenPublicGamerSlots;
+            return $"{session.HostGamertag} ({session.CurrentGamerCount}/{totalSlots})";
+        }
+    }
+
     /// <summary>
     /// Helper class represents a single entry in a MenuScreen. By default this
     /// just draws the entry text string, but it can be customized to display menu
@@ -59,6 +80,11 @@ namespace GameStateManagement
 
         public float Rotation { get; set; }
 
+        /// <summary>
+        /// Optional tag for storing custom data (e.g., session reference).
+        /// </summary>
+        public object Tag { get; set; }
+
         /// <summary>
         /// Event raised when the menu entry is selected.
         /// </summary>
@@ -115,15 +141,16 @@ namespace GameStateManagement
         public virtual void Draw(MenuScreen screen, bool isSelected, GameTime gameTime)
         {
             Color textColor = isSelected ? Color.White : Color.Black;
-            Color tintColor = isSelected ? Color.White : Color.Gray;
+
+            // Check if this menu entry is currently being pressed
+            bool isPressed = screen.IsMouseDown && isSelected;
 
             if (UIUtility.IsMobile)
             {
                 // there is no such thing as a selected item on Windows Phone, so we always
                 // force isSelected to be false
-
                 isSelected = false;
-                tintColor = Color.White;
+                isPressed = false;
                 textColor = Color.Black;
             }
 
@@ -132,7 +159,9 @@ namespace GameStateManagement
             SpriteBatch spriteBatch = screenManager.SpriteBatch;
             SpriteFont font = screenManager.Font;
 
-            spriteBatch.Draw(screenManager.ButtonBackground, destination, tintColor);
+            // Use pressed texture when button is being pressed, otherwise use regular texture
+            Texture2D buttonTexture = isPressed ? screenManager.ButtonPressed : screenManager.ButtonBackground;
+            spriteBatch.Draw(buttonTexture, destination, Color.White);
 
             spriteBatch.DrawString(screenManager.Font, text, getTextPosition(screen),
                 textColor, Rotation, Vector2.Zero, Scale, SpriteEffects.None, 0);
@@ -158,19 +187,16 @@ namespace GameStateManagement
         private Vector2 getTextPosition(MenuScreen screen)
         {
             Vector2 textPosition = Vector2.Zero;
-            if (Scale == 1f)
-            {
-                textPosition = new Vector2((int)destination.X + destination.Width / 2 - GetWidth(screen) / 2,
-                                   (int)destination.Y);
-            }
-            else
-            {
-                textPosition = new Vector2(
-                    (int)destination.X + (destination.Width / 2 - ((GetWidth(screen) / 2) * Scale)),
-                                 (int)destination.Y + (GetHeight(screen) - GetHeight(screen) * Scale) / 2);
-            }
+
+            // Calculate horizontal centering
+            float centeredX = destination.X + (destination.Width / 2.0f) - (GetWidth(screen) / 2.0f) * Scale;
+
+            // Calculate vertical centering
+            float centeredY = destination.Y + (destination.Height / 2.0f) - (GetHeight(screen) / 2.0f) * Scale;
+
+            textPosition = new Vector2(centeredX, centeredY);
 
             return textPosition;
         }
     }
-}
+}

+ 13 - 2
CardsStarterKit/Core/Game/ScreenManager/MenuScreen.cs

@@ -34,6 +34,15 @@ namespace GameStateManagement
 
         private bool isMouseDown = false;
 
+        /// <summary>
+        /// Gets whether the mouse button is currently pressed down.
+        /// Used by MenuEntry to determine if it should show the pressed texture.
+        /// </summary>
+        public bool IsMouseDown
+        {
+            get { return isMouseDown; }
+        }
+
         /// <summary>
         /// Gets the list of menu entries, so derived classes can add
         /// or change the menu contents.
@@ -299,8 +308,10 @@ namespace GameStateManagement
             // the movement slow down as it nears the end).
             float transitionOffset = (float)Math.Pow(TransitionPosition, 2);
 
-            // Draw the menu title centered on the screen
-            Vector2 titlePosition = new Vector2(graphics.Viewport.Width / 2, 375);
+            // Draw the menu title in the top third of the screen
+            Vector2 titlePosition = new Vector2(
+                graphics.Viewport.Width / 2,
+                ScreenManager.SafeArea.Top + 80); // Position in top third instead of center
             Vector2 titleOrigin = font.MeasureString(menuTitle) / 2;
             Color titleColor = new Color(192, 192, 192) * TransitionAlpha;
             float titleScale = 1.25f;

+ 89 - 2
CardsStarterKit/Core/Game/ScreenManager/ScreenManager.cs

@@ -34,9 +34,12 @@ namespace GameStateManagement
         public InputState InputState => inputState;
 
         SpriteBatch spriteBatch;
-        SpriteFont font;
+        SpriteFont font;  // MenuFont
+        SpriteFont regularFont;
+        SpriteFont boldFont;
         Texture2D blankTexture;
         Texture2D buttonBackground;
+        Texture2D buttonPressed;
 
         bool isInitialized;
 
@@ -75,6 +78,11 @@ namespace GameStateManagement
             get { return buttonBackground; }
         }
 
+        public Texture2D ButtonPressed
+        {
+            get { return buttonPressed; }
+        }
+
         public Texture2D BlankTexture
         {
             get { return blankTexture; }
@@ -87,6 +95,23 @@ namespace GameStateManagement
         public SpriteFont Font
         {
             get { return font; }
+            set { font = value; }
+        }
+
+        /// <summary>
+        /// Regular font (automatically switches between Regular and Regular_CJK based on language)
+        /// </summary>
+        public SpriteFont RegularFont
+        {
+            get { return regularFont; }
+        }
+
+        /// <summary>
+        /// Bold font (automatically switches between Bold and Bold_CJK based on language)
+        /// </summary>
+        public SpriteFont BoldFont
+        {
+            get { return boldFont; }
         }
 
         /// <summary>
@@ -143,9 +168,23 @@ namespace GameStateManagement
             ContentManager content = Game.Content;
 
             spriteBatch = new SpriteBatch(GraphicsDevice);
-            font = content.Load<SpriteFont>("Fonts/MenuFont");
+
+            // Load the appropriate fonts based on the saved language setting
+            // This handles the case where the game was closed with a CJK language selected
+            string currentLanguage = Blackjack.GameSettings.Instance.Language;
+            bool useCJKFont = currentLanguage == "日本語" || currentLanguage == "中文";
+
+            string menuFontPath = useCJKFont ? "Fonts/MenuFont_CJK" : "Fonts/MenuFont";
+            string regularFontPath = useCJKFont ? "Fonts/Regular_CJK" : "Fonts/Regular";
+            string boldFontPath = useCJKFont ? "Fonts/Bold_CJK" : "Fonts/Bold";
+
+            font = content.Load<SpriteFont>(menuFontPath);
+            regularFont = content.Load<SpriteFont>(regularFontPath);
+            boldFont = content.Load<SpriteFont>(boldFontPath);
+
             blankTexture = content.Load<Texture2D>("Images/blank");
             buttonBackground = content.Load<Texture2D>("Images/ButtonRegular");
+            buttonPressed = content.Load<Texture2D>("Images/ButtonPressed");
 
             // Tell each of the screens to load their content.
             foreach (GameScreen screen in screens)
@@ -154,6 +193,54 @@ namespace GameStateManagement
             }
         }
 
+        /// <summary>
+        /// Reloads the font based on the current language setting.
+        /// Uses CJK fonts for Japanese and Chinese, regular fonts for other languages.
+        /// NOTE: This only loads fonts. Call RefreshScreensAfterLanguageChange() after
+        /// the language has been set to rebuild screen content.
+        /// </summary>
+        public void ReloadFontForLanguage(string language)
+        {
+            ContentManager content = Game.Content;
+
+            // Determine if we need CJK font support
+            bool useCJKFont = language == "日本語" || language == "中文";
+
+            // Load all appropriate fonts
+            string menuFontPath = useCJKFont ? "Fonts/MenuFont_CJK" : "Fonts/MenuFont";
+            string regularFontPath = useCJKFont ? "Fonts/Regular_CJK" : "Fonts/Regular";
+            string boldFontPath = useCJKFont ? "Fonts/Bold_CJK" : "Fonts/Bold";
+
+            font = content.Load<SpriteFont>(menuFontPath);
+            regularFont = content.Load<SpriteFont>(regularFontPath);
+            boldFont = content.Load<SpriteFont>(boldFontPath);
+        }
+
+        /// <summary>
+        /// Refreshes all screens after language change. Call this AFTER setting the language
+        /// to ensure screens rebuild with matching language and fonts.
+        /// </summary>
+        public void RefreshScreensAfterLanguageChange()
+        {
+            foreach (GameScreen screen in screens)
+            {
+                if (screen is Blackjack.SettingsScreen settingsScreen)
+                {
+                    // SettingsScreen rebuilds itself in the cycle methods
+                }
+                else if (screen is Blackjack.GameplayScreen gameplayScreen)
+                {
+                    // Update gameplay button text (Deal, Clear, Hit, Stand, etc.)
+                    gameplayScreen.UpdateButtonText();
+                }
+                else if (screen is GameStateManagement.MenuScreen menuScreen)
+                {
+                    // Force menu screens to rebuild their entries with the new language
+                    menuScreen.LoadContent();
+                }
+            }
+        }
+
         /// <summary>
         /// Unload your graphics content.
         /// </summary>

+ 85 - 0
CardsStarterKit/Core/Game/Screens/AvailableSessionMenuEntry.cs

@@ -0,0 +1,85 @@
+//-----------------------------------------------------------------------------
+// AvailableSessionMenuEntry.cs
+//
+// Adapted from NetworkStateManagement sample for Blackjack
+//-----------------------------------------------------------------------------
+
+using System;
+using GameStateManagement;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Net;
+
+namespace Blackjack
+{
+    /// <summary>
+    /// Helper class customizes the standard MenuEntry class
+    /// for displaying AvailableNetworkSession objects.
+    /// </summary>
+    class AvailableSessionMenuEntry : MenuEntry
+    {
+        AvailableNetworkSession availableSession;
+        bool gotQualityOfService;
+
+        /// <summary>
+        /// Gets the available network session corresponding to this menu entry.
+        /// </summary>
+        public AvailableNetworkSession AvailableSession
+        {
+            get { return availableSession; }
+        }
+
+        /// <summary>
+        /// Constructs a menu entry describing an available network session.
+        /// </summary>
+        public AvailableSessionMenuEntry(AvailableNetworkSession availableSession)
+            : base(GetMenuItemText(availableSession))
+        {
+            this.availableSession = availableSession;
+        }
+
+        /// <summary>
+        /// Formats session information to create the menu text string.
+        /// </summary>
+        static string GetMenuItemText(AvailableNetworkSession session)
+        {
+            int totalSlots = session.CurrentGamerCount +
+                session.OpenPublicGamerSlots;
+
+            return string.Format("{0} ({1}/{2})", session.HostGamertag,
+                        session.CurrentGamerCount,
+                        totalSlots);
+        }
+
+        /// <summary>
+        /// Updates the menu item text, adding information about the network
+        /// quality of service as soon as that becomes available.
+        /// </summary>
+        public override void Update(MenuScreen screen, bool isSelected,
+                    GameTime gameTime)
+        {
+            base.Update(screen, isSelected, gameTime);
+
+            // Quality of service data can take some time to query, so it will not
+            // be filled in straight away when NetworkSession.Find returns. We want
+            // to display the list of available sessions straight away, and then
+            // fill in the quality of service data whenever that becomes available,
+            // so we keep checking until this data shows up.
+            if (screen.IsActive && !gotQualityOfService)
+            {
+                QualityOfService qualityOfService = availableSession.QualityOfService;
+
+                if (qualityOfService.IsAvailable)
+                {
+                    // TODO: Check if the compatibility layer supports AverageRoundtripTime
+                    // For now, show a default ping time
+                    TimeSpan pingTime = TimeSpan.FromMilliseconds(50); // Default 50ms ping
+                    // TimeSpan pingTime = qualityOfService.AverageRoundtripTime;
+
+                    Text += string.Format(" - {0:0} ms", pingTime.TotalMilliseconds);
+
+                    gotQualityOfService = true;
+                }
+            }
+        }
+    }
+}

+ 196 - 0
CardsStarterKit/Core/Game/Screens/BlackjackLobbyScreen.cs

@@ -0,0 +1,196 @@
+//----------------------------------------------------------------------------- 
+// BlackjackLobbyScreen.cs
+//
+// Displays the lobby with player slots and host controls.
+//-----------------------------------------------------------------------------
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using GameStateManagement;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Net;
+
+namespace Blackjack
+{
+    class BlackjackLobbyScreen : MenuScreen
+    {
+        MenuEntry startGameMenuEntry;
+        MenuEntry leaveLobbyMenuEntry;
+        List<string> joinedPlayers = new List<string>();
+        bool isHost;
+        NetworkSession networkSession;
+
+        public BlackjackLobbyScreen(NetworkSession networkSession = null)
+            : base(Resources.Lobby)
+        {
+            this.networkSession = networkSession;
+
+            if (networkSession != null)
+            {
+                this.isHost = networkSession.IsHost;
+
+                // Subscribe to session events
+                networkSession.GamerJoined += OnGamerJoined;
+                networkSession.GamerLeft += OnGamerLeft;
+
+                // Initialize player list from current session
+                UpdatePlayerList();
+            }
+            else
+            {
+                // Local game fallback
+                this.isHost = true;
+                var defaultPlayerName = Environment.UserName;
+                if (string.IsNullOrEmpty(defaultPlayerName))
+                    defaultPlayerName = "You";
+                joinedPlayers.Add(defaultPlayerName);
+            }
+        }
+
+        public override void LoadContent()
+        {
+            startGameMenuEntry = new MenuEntry(Resources.StartGame);
+            leaveLobbyMenuEntry = new MenuEntry(Resources.LeaveSession);
+
+            startGameMenuEntry.Selected += StartGameMenuEntrySelected;
+            leaveLobbyMenuEntry.Selected += LeaveLobbyMenuEntrySelected;
+
+            // Only add "Start Game" button for the host
+            if (isHost)
+            {
+                MenuEntries.Add(startGameMenuEntry);
+            }
+            MenuEntries.Add(leaveLobbyMenuEntry);
+
+            base.LoadContent();
+        }
+
+        public override void Draw(GameTime gameTime)
+        {
+            // Draw solid background to cover any BackgroundScreen logo
+            ScreenManager.GraphicsDevice.Clear(new Color(50, 20, 20)); // Dark red background
+
+            base.Draw(gameTime);
+
+            SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
+            SpriteFont font = ScreenManager.Font;
+            Vector2 position = new Vector2(ScreenManager.SafeArea.Left + 50, ScreenManager.SafeArea.Top + 100);
+
+            spriteBatch.Begin();
+            spriteBatch.DrawString(font, Resources.Players, position, Color.White);
+            position.Y += font.LineSpacing * 2;
+
+            int slotIndex = 0;
+            // Show joined players
+            foreach (var playerName in joinedPlayers)
+            {
+                // Strip GUID suffix for display (e.g., "Player_abc123de" -> "Player")
+                string displayName = CardsFramework.UIUtility.StripGuidSuffix(playerName);
+                string slotText = string.Format(Resources.Slot, slotIndex + 1, displayName);
+                if (slotIndex == 0 && isHost)
+                    slotText += $"    [{Resources.Host}]";
+                spriteBatch.DrawString(font, slotText, position, Color.Green);
+                position.Y += font.LineSpacing;
+                slotIndex++;
+            }
+            // Do NOT show AI players until Start Game is clicked
+            // Dealer
+            position.Y += font.LineSpacing;
+            spriteBatch.DrawString(font, Resources.Dealer, position, Color.Yellow);
+            spriteBatch.End();
+        }
+
+        void StartGameMenuEntrySelected(object sender, EventArgs e)
+        {
+            if (networkSession != null && isHost)
+            {
+                // In network game, host calls StartGame which will trigger state change
+                // This will cause Update() to detect Playing state and transition all players
+                networkSession.StartGame();
+            }
+            else if (networkSession == null)
+            {
+                // TODO Display Message that network session is required to start game
+            }
+            else
+            {
+                // If client in network game, do nothing - only host can start
+            }
+        }
+
+        void LeaveLobbyMenuEntrySelected(object sender, EventArgs e)
+        {
+            // Unsubscribe from events
+            if (networkSession != null)
+            {
+                networkSession.GamerJoined -= OnGamerJoined;
+                networkSession.GamerLeft -= OnGamerLeft;
+            }
+
+            // Exit all current screens before returning to main menu
+            foreach (GameScreen screen in ScreenManager.GetScreens())
+                screen.ExitScreen();
+
+            ScreenManager.AddScreen(new BackgroundScreen(), null);
+            ScreenManager.AddScreen(new MainMenuScreen(), null);
+        }
+
+        void OnGamerJoined(object sender, GamerJoinedEventArgs e)
+        {
+            UpdatePlayerList();
+        }
+
+        void OnGamerLeft(object sender, GamerLeftEventArgs e)
+        {
+            UpdatePlayerList();
+        }
+
+        void UpdatePlayerList()
+        {
+            joinedPlayers.Clear();
+
+            if (networkSession != null)
+            {
+                // CRITICAL: Ensure consistent player ordering across all machines
+                // Host must ALWAYS be first, then other players sorted by their network ID
+                var orderedGamers = networkSession.AllGamers
+                    .OrderByDescending(g => g.IsHost)  // Host first
+                    .ThenBy(g => g.Id);                 // Then others sorted by ID
+
+                foreach (NetworkGamer gamer in orderedGamers)
+                {
+                    joinedPlayers.Add(gamer.Gamertag);
+                }
+            }
+        }
+
+        public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
+        {
+            base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
+
+            // In network games, check if host started the game
+            if (networkSession != null && !IsExiting)
+            {
+                if (networkSession.SessionState == NetworkSessionState.Playing)
+                {
+                    // Host started the game, transition all players
+                    // Only pass human player names - GameplayScreen will add AI players on host only
+                    var allPlayers = new List<string>(joinedPlayers);
+
+                    // Exit all screens to clear the background and lobby screens
+                    foreach (GameScreen screen in ScreenManager.GetScreens())
+                        screen.ExitScreen();
+
+                    ScreenManager.AddScreen(new GameplayScreen(MainMenuScreen.Theme, allPlayers, networkSession), null);
+                }
+            }
+        }
+
+        // Helper for session info - DEPRECATED, keeping for backward compatibility
+        public class SessionInfo
+        {
+            public List<string> PlayerNames = new List<string>();
+        }
+    }
+}

+ 14 - 0
CardsStarterKit/Core/Game/Screens/CardsStarterKit.code-workspace

@@ -0,0 +1,14 @@
+{
+	"folders": [
+		{
+			"path": "../../.."
+		},
+		{
+			"path": "../../../../NetworkStateManagement"
+		},
+		{
+			"path": "../../../../MonoGame.Xna.Framework.Net"
+		}
+	],
+	"settings": {}
+}

+ 536 - 33
CardsStarterKit/Core/Game/Screens/GameplayScreen.cs

@@ -7,12 +7,15 @@
 
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Text;
 using GameStateManagement;
 using Microsoft.Xna.Framework;
 using Microsoft.Xna.Framework.Input;
 using CardsFramework;
 using Microsoft.Xna.Framework.Input.Touch;
+using System.Globalization;
+using Microsoft.Xna.Framework.Net;
 
 namespace Blackjack
 {
@@ -29,10 +32,12 @@ namespace Blackjack
 
         Vector2[] playerCardOffset;
 
+        NetworkSession networkSession;
+
         /// <summary>
         /// Initializes a new instance of the screen.
         /// </summary>
-        public GameplayScreen(string theme)
+        public GameplayScreen(string theme, List<string> joinedPlayers = null, NetworkSession networkSession = null)
         {
             TransitionOnTime = TimeSpan.FromSeconds(0.0);
             TransitionOffTime = TimeSpan.FromSeconds(0.5);
@@ -40,8 +45,12 @@ namespace Blackjack
             EnabledGestures = GestureType.Tap;
 
             this.theme = theme;
+            this.joinedPlayers = joinedPlayers;
+            this.networkSession = networkSession;
         }
 
+        private List<string> joinedPlayers;
+
         /// <summary>
         /// Load content and initializes the actual game.
         /// </summary>
@@ -49,17 +58,7 @@ namespace Blackjack
         {
             safeArea = ScreenManager.SafeArea;
 
-            // Calculate proportional player positions (better centered and spread out)
-            // These positions should space the three player areas evenly across the table
-            float playerY = safeArea.Height * 0.29f; // ~210px at 720px
-            float centerY = safeArea.Height * 0.26f; // ~190px at 720px - slightly higher for side players
-
-            playerCardOffset = new Vector2[]
-            {
-                new Vector2(safeArea.Width * 0.18f, centerY),   // Left player - moved right (~230px at 1280px)
-                new Vector2(safeArea.Width * 0.42f, playerY),   // Center player - truly centered (~537px at 1280px)
-                new Vector2(safeArea.Width * 0.66f, centerY)    // Right player - balanced (~845px at 1280px)
-            };
+            // Player positions will be calculated dynamically after we know how many players there are
 
             // Initialize virtual cursor
             inputHelper = new InputHelper(ScreenManager);
@@ -74,9 +73,25 @@ namespace Blackjack
             blackJackGame = new BlackjackCardGame(safeArea, new Vector2(safeArea.Left + safeArea.Width / 2 - 50, safeArea.Top + 20),
                 GetPlayerCardPosition, ScreenManager, theme);
 
+            // Wire up network session if in multiplayer mode
+            // Only treat as network game if there are actually multiple human players
+            if (networkSession != null && networkSession.AllGamers.Count > 1)
+            {
+                blackJackGame.NetworkSession = networkSession;
+                blackJackGame.IsNetworkGame = true;
+                blackJackGame.IsHost = networkSession.IsHost;
+                System.Console.WriteLine($"[LoadContent] Network game detected with {networkSession.AllGamers.Count} gamers, IsNetworkGame=true");
+            }
+            else
+            {
+                System.Console.WriteLine($"[LoadContent] Single-player game, IsNetworkGame={blackJackGame.IsNetworkGame}, networkSession={(networkSession == null ? "null" : $"exists with {networkSession.AllGamers.Count} gamers")}");
+            }
 
             InitializeGame();
 
+            // Update button text/fonts to match current language
+            UpdateButtonText();
+
             base.LoadContent();
         }
 
@@ -127,9 +142,242 @@ namespace Blackjack
                 blackJackGame.Update(gameTime);
             }
 
+            // Centralized network packet dispatcher
+            ProcessNetworkPackets();
+
             base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
         }
 
+        // Centralized network packet dispatcher
+        private void ProcessNetworkPackets()
+        {
+            if (networkSession == null || networkSession.LocalGamers.Count == 0)
+                return;
+
+            var localGamer = networkSession.LocalGamers[0];
+            var packetReader = new PacketReader();
+            while (localGamer.IsDataAvailable)
+            {
+                NetworkGamer sender;
+                localGamer.ReceiveData(packetReader, out sender);
+
+                try
+                {
+                    // Read packet type (assume PacketType is a byte)
+                    var packetType = (Blackjack.Networking.PacketType)packetReader.ReadByte();
+                    System.Console.WriteLine($"[PACKET] Received {packetType} from {sender.Gamertag}");
+
+                    switch (packetType)
+                    {
+                        case Blackjack.Networking.PacketType.PlayerListSync:
+                            HandlePlayerListSyncPacket(sender, packetReader);
+                            break;
+                        case Blackjack.Networking.PacketType.CardDealt:
+                            HandleCardDealtPacket(sender, packetReader);
+                            break;
+                        case Blackjack.Networking.PacketType.BetPlaced:
+                            HandleBetPlacedPacket(sender, packetReader);
+                            break;
+                        case Blackjack.Networking.PacketType.ChipAdded:
+                            HandleChipAddedPacket(sender, packetReader);
+                            break;
+                        case Blackjack.Networking.PacketType.PlayerAction:
+                            HandlePlayerActionPacket(sender, packetReader);
+                            break;
+                        case Blackjack.Networking.PacketType.ShuffleSeed:
+                            HandleShuffleSeedPacket(sender, packetReader);
+                            break;
+                        // Phase 5: Gameplay action packets
+                        case Blackjack.Networking.PacketType.HitAction:
+                            HandleHitActionPacket(sender, packetReader);
+                            break;
+                        case Blackjack.Networking.PacketType.StandAction:
+                            HandleStandActionPacket(sender, packetReader);
+                            break;
+                        case Blackjack.Networking.PacketType.DoubleAction:
+                            HandleDoubleActionPacket(sender, packetReader);
+                            break;
+                        case Blackjack.Networking.PacketType.SplitAction:
+                            HandleSplitActionPacket(sender, packetReader);
+                            break;
+                        case Blackjack.Networking.PacketType.InsuranceAction:
+                            HandleInsuranceActionPacket(sender, packetReader);
+                            break;
+                        case Blackjack.Networking.PacketType.TurnChanged:
+                            HandleTurnChangedPacket(sender, packetReader);
+                            break;
+                        // Add more cases for other packet types as needed
+                        default:
+                            System.Console.WriteLine($"[PACKET] Unknown packet type: {(byte)packetType}");
+                            break;
+                    }
+                }
+                catch (System.IO.EndOfStreamException ex)
+                {
+                    System.Console.WriteLine($"[PACKET ERROR] EndOfStreamException while processing packet from {sender.Gamertag}: {ex.Message}");
+                    System.Console.WriteLine($"[PACKET ERROR] Stack trace: {ex.StackTrace}");
+                }
+                catch (System.Exception ex)
+                {
+                    System.Console.WriteLine($"[PACKET ERROR] Exception while processing packet from {sender.Gamertag}: {ex.GetType().Name} - {ex.Message}");
+                }
+            }
+        }
+
+        // Packet handlers
+        private void HandlePlayerListSyncPacket(NetworkGamer sender, PacketReader reader)
+        {
+            var packet = Blackjack.Networking.PlayerListSyncPacket.Deserialize(reader);
+
+            // Only clients should process this - host already has the correct player list
+            if (networkSession != null && !networkSession.IsHost)
+            {
+                // Clear existing AI players (clients shouldn't have created any)
+                // But we need to recreate the full player list to match the host
+
+                // The client should have already added human players in InitializeGame
+                // Now we need to add AI players to match the host's list
+
+                System.Globalization.TextInfo myTI = new System.Globalization.CultureInfo("en-GB", false).TextInfo;
+
+                // Get current player count (should be just human players)
+                int currentPlayerCount = blackJackGame.Players.Count;
+
+                // Add any missing players from the packet
+                for (int i = currentPlayerCount; i < packet.Players.Count; i++)
+                {
+                    var playerInfo = packet.Players[i];
+                    if (playerInfo.IsAI)
+                    {
+                        // Add AI player (but don't wire up events - host controls AI)
+                        BlackjackAIPlayer aiPlayer = new BlackjackAIPlayer(playerInfo.Name, blackJackGame);
+                        blackJackGame.AddPlayer(aiPlayer);
+                    }
+                    else
+                    {
+                        // Add human player (shouldn't normally happen, but handle it)
+                        blackJackGame.AddPlayer(new BlackjackPlayer(myTI.ToTitleCase(playerInfo.Name), blackJackGame));
+                    }
+                }
+
+                // CRITICAL: Recalculate player positions now that we have the full player list
+                int totalPlayers = blackJackGame.Players.Count;
+                CalculatePlayerPositions(totalPlayers);
+
+                // Update the table to show the correct number of player spots
+                blackJackGame.GameTable.SetPlaces(totalPlayers);
+
+                // Now that we have the complete player list, start the round
+                // This ensures DisplayPlayingHands() creates animatedHands for all players including AI
+                System.Console.WriteLine($"[PlayerListSync] Client received {packet.Players.Count} players, starting round now");
+                blackJackGame.StartRound();
+            }
+        }
+
+        // Example packet handlers (implement actual logic as needed)
+        private void HandleCardDealtPacket(NetworkGamer sender, PacketReader reader)
+        {
+            var packet = Blackjack.Networking.CardDealtPacket.Deserialize(reader);
+
+            // Only clients should process this - host already dealt the card locally
+            if (networkSession != null && !networkSession.IsHost)
+            {
+                // Forward to the game to handle the card dealing
+                blackJackGame.HandleReceivedCardDealt(packet.Card, packet.PlayerIndex, packet.FaceDown, packet.HandType);
+            }
+        }
+
+        private void HandleBetPlacedPacket(NetworkGamer sender, PacketReader reader)
+        {
+            var packet = Blackjack.Networking.BetPlacedPacket.Deserialize(reader);
+
+            if (networkSession != null)
+            {
+                if (networkSession.IsHost)
+                {
+                    // Host receives bet from a client
+                    // Don't process if this is from the local host machine (to avoid processing our own broadcast)
+                    if (!sender.IsLocal)
+                    {
+                        // Apply the bet locally on the host
+                        blackJackGame.HandleReceivedBetPlaced(packet.PlayerIndex, packet.BetAmount);
+
+                        // Broadcast to all other clients (so all clients stay in sync)
+                        blackJackGame.BroadcastBetPlaced(packet.PlayerIndex, packet.BetAmount);
+                    }
+                }
+                else
+                {
+                    // Client receives bet broadcast from host
+                    blackJackGame.HandleReceivedBetPlaced(packet.PlayerIndex, packet.BetAmount);
+                }
+            }
+        }
+
+        private void HandleChipAddedPacket(NetworkGamer sender, PacketReader reader)
+        {
+            var packet = Blackjack.Networking.ChipAddedPacket.Deserialize(reader);
+
+            if (networkSession != null)
+            {
+                if (networkSession.IsHost)
+                {
+                    // Host receives chip addition from a client
+                    // Don't process if this is from the local host machine (to avoid processing our own broadcast)
+                    if (!sender.IsLocal)
+                    {
+                        // Apply the chip locally on the host (this will trigger the animation and update bet)
+                        blackJackGame.HandleReceivedChipAdded(packet.PlayerIndex, packet.ChipValue);
+
+                        // Broadcast to all other clients (so all clients stay in sync)
+                        blackJackGame.BroadcastChipAdded(packet.PlayerIndex, packet.ChipValue);
+                    }
+                }
+                else
+                {
+                    // Client receives chip addition broadcast from host
+                    blackJackGame.HandleReceivedChipAdded(packet.PlayerIndex, packet.ChipValue);
+                }
+            }
+        }
+
+        private void HandlePlayerActionPacket(NetworkGamer sender, PacketReader reader)
+        {
+            var packet = Blackjack.Networking.PlayerActionPacket.Deserialize(reader);
+            // Host receives action from client and processes it
+            if (networkSession != null && networkSession.IsHost)
+            {
+                switch (packet.Action)
+                {
+                    case Blackjack.Networking.BlackjackAction.Hit:
+                        blackJackGame.Hit();
+                        break;
+                    case Blackjack.Networking.BlackjackAction.Stand:
+                        blackJackGame.Stand();
+                        break;
+                    case Blackjack.Networking.BlackjackAction.Double:
+                        blackJackGame.Double();
+                        break;
+                    case Blackjack.Networking.BlackjackAction.Split:
+                        blackJackGame.Split();
+                        break;
+                    case Blackjack.Networking.BlackjackAction.Insurance:
+                        blackJackGame.Insurance();
+                        break;
+                }
+            }
+        }
+
+        private void HandleShuffleSeedPacket(NetworkGamer sender, PacketReader reader)
+        {
+            var packet = Blackjack.Networking.ShuffleSeedPacket.Deserialize(reader);
+            // Client receives shuffle seed from host
+            if (networkSession != null && !networkSession.IsHost)
+            {
+                blackJackGame.ReceiveShuffleSeed(packet.Seed);
+            }
+        }
+
         /// <summary>
         /// Draw the screen
         /// </summary>
@@ -151,19 +399,117 @@ namespace Blackjack
         private void InitializeGame()
         {
             blackJackGame.Initialize();
-            // Add human player
-            blackJackGame.AddPlayer(new BlackjackPlayer("Abe", blackJackGame));
 
-            // Add AI players
-            BlackjackAIPlayer player = new BlackjackAIPlayer("Benny", blackJackGame);
-            blackJackGame.AddPlayer(player);
-            player.Hit += player_Hit;
-            player.Stand += player_Stand;
+            blackJackGame.UpdateButtonText();
+
+            TextInfo myTI = new CultureInfo("en-GB", false).TextInfo;
+
+            System.Console.WriteLine($"[InitializeGame] joinedPlayers={(joinedPlayers == null ? "null" : joinedPlayers.Count.ToString())}, networkSession={(networkSession == null ? "null" : "not null")}");
 
-            player = new BlackjackAIPlayer("Chuck", blackJackGame);
-            blackJackGame.AddPlayer(player);
-            player.Hit += player_Hit;
-            player.Stand += player_Stand;
+            // Add players from lobby
+            if (joinedPlayers != null && joinedPlayers.Count > 0)
+            {
+                // Determine how many are human players (from network session)
+                int humanPlayerCount = joinedPlayers.Count;
+                if (networkSession != null)
+                {
+                    // In network games, only count actual network gamers as human players
+                    humanPlayerCount = networkSession.AllGamers.Count;
+                }
+
+                // Add human players
+                for (int i = 0; i < humanPlayerCount; i++)
+                {
+                    var player = new BlackjackPlayer(myTI.ToTitleCase(joinedPlayers[i]), blackJackGame);
+
+                    // Load saved balance if persistent winnings is enabled (single-player only, first player)
+                    if (i == 0 && !blackJackGame.IsNetworkGame && GameSettings.Instance.PersistWinnings)
+                    {
+                        // Reset to default if saved balance is 0 or negative
+                        if (GameSettings.Instance.SavedPlayerBalance <= 0)
+                        {
+                            GameSettings.Instance.SavedPlayerBalance = 500f;
+                            GameSettings.Save();
+                            System.Console.WriteLine($"[PersistWinnings] (Path 1) Reset negative/zero balance to default: 500");
+                        }
+                        player.Balance = GameSettings.Instance.SavedPlayerBalance;
+                        System.Console.WriteLine($"[PersistWinnings] (Path 1) Loaded balance: {player.Balance}");
+                    }
+                    else
+                    {
+                        System.Console.WriteLine($"[PersistWinnings] (Path 1) Using default balance: {player.Balance} (i={i}, IsNetworkGame={blackJackGame.IsNetworkGame}, PersistWinnings={GameSettings.Instance.PersistWinnings})");
+                    }
+
+                    blackJackGame.AddPlayer(player);
+                }
+
+                // Only the host creates AI players in network games
+                // In local games, always create AI players
+                if (networkSession == null || networkSession.IsHost)
+                {
+                    // Fill remaining slots with AI based on settings
+                    int maxAI = GameSettings.Instance.MaxAIPlayers;
+                    int aiSlotsToFill = GameSettings.Instance.FillEmptySlotsWithAI
+                        ? Math.Min(BlackjackConstants.MaxPlayers - humanPlayerCount, maxAI)
+                        : Math.Min(maxAI, BlackjackConstants.MaxPlayers - humanPlayerCount);
+
+                    for (int i = 0; i < aiSlotsToFill && i < BlackjackConstants.DefaultAINames.Length; i++)
+                    {
+                        BlackjackAIPlayer player = new BlackjackAIPlayer(BlackjackConstants.DefaultAINames[i], blackJackGame);
+                        blackJackGame.AddPlayer(player);
+                        player.Hit += player_Hit;
+                        player.Stand += player_Stand;
+                    }
+                }
+            }
+            else
+            {
+                // Fallback: single player + 6 AI (local game only)
+                var defaultPlayerName = Environment.UserName;
+                if (string.IsNullOrEmpty(defaultPlayerName))
+                {
+                    defaultPlayerName = "You";
+                }
+
+                var humanPlayer = new BlackjackPlayer(myTI.ToTitleCase(defaultPlayerName), blackJackGame);
+
+                // Load saved balance if persistent winnings is enabled (single-player only)
+                if (GameSettings.Instance.PersistWinnings)
+                {
+                    // Reset to default if saved balance is 0 or negative
+                    if (GameSettings.Instance.SavedPlayerBalance <= 0)
+                    {
+                        GameSettings.Instance.SavedPlayerBalance = 500f;
+                        GameSettings.Save();
+                        System.Console.WriteLine($"[PersistWinnings] (Path 2 - Fallback) Reset negative/zero balance to default: 500");
+                    }
+                    humanPlayer.Balance = GameSettings.Instance.SavedPlayerBalance;
+                    System.Console.WriteLine($"[PersistWinnings] (Path 2 - Fallback) Loaded balance: {humanPlayer.Balance}");
+                }
+                else
+                {
+                    System.Console.WriteLine($"[PersistWinnings] (Path 2 - Fallback) Using default balance: {humanPlayer.Balance}");
+                }
+
+                blackJackGame.AddPlayer(humanPlayer);
+
+                // Add AI players based on settings
+                int maxAI = GameSettings.Instance.MaxAIPlayers;
+                for (int i = 0; i < maxAI && i < BlackjackConstants.DefaultAINames.Length; i++)
+                {
+                    BlackjackAIPlayer player = new BlackjackAIPlayer(BlackjackConstants.DefaultAINames[i], blackJackGame);
+                    blackJackGame.AddPlayer(player);
+                    player.Hit += player_Hit;
+                    player.Stand += player_Stand;
+                }
+            }
+
+            // Calculate player positions now that we know the actual number of players
+            int totalPlayers = blackJackGame.Players.Count;
+            CalculatePlayerPositions(totalPlayers);
+
+            // Update the table to show only the actual number of player spots
+            blackJackGame.GameTable.SetPlaces(totalPlayers);
 
             // Load UI assets
             string[] assets = { "blackjack", "bust", "lose", "push", "win", "pass", "Shuffle_" + theme };
@@ -173,7 +519,87 @@ namespace Blackjack
                 blackJackGame.LoadUITexture("UI", assets[chipIndex]);
             }
 
-            blackJackGame.StartRound();
+            // Host broadcasts the full player list to clients so they know about AI players
+            if (networkSession != null && networkSession.IsHost)
+            {
+                blackJackGame.BroadcastPlayerList();
+            }
+
+            // In network games, determine which player index belongs to the local user
+            if (networkSession != null && networkSession.LocalGamers.Count > 0)
+            {
+                string localGamerTag = networkSession.LocalGamers[0].Gamertag;
+
+                // Find which player in the game matches the local gamer's tag
+                for (int i = 0; i < blackJackGame.Players.Count; i++)
+                {
+                    if (blackJackGame.Players[i].Name.Equals(localGamerTag, StringComparison.OrdinalIgnoreCase))
+                    {
+                        // Found the local player - tell BetGameComponent
+                        var betComponent = blackJackGame.Game.Components.OfType<BetGameComponent>().FirstOrDefault();
+                        if (betComponent != null)
+                        {
+                            betComponent.LocalPlayerIndex = i;
+                        }
+                        break;
+                    }
+                }
+            }
+
+            // Only start the round immediately if we're the host or in a local game
+            // Clients need to wait for the PlayerListSync packet first
+            if (networkSession == null || networkSession.IsHost)
+            {
+                blackJackGame.StartRound();
+            }
+            // Note: Clients will call StartRound() after receiving PlayerListSync packet
+        }
+
+        /// <summary>
+        /// Calculates player positions dynamically based on the actual number of players.
+        /// Centers and spreads them evenly across the table.
+        /// </summary>
+        /// <param name="playerCount">The actual number of players in the game.</param>
+        private void CalculatePlayerPositions(int playerCount)
+        {
+            if (playerCount <= 0)
+            {
+                playerCardOffset = new Vector2[0];
+                return;
+            }
+
+            playerCardOffset = new Vector2[playerCount];
+
+            float bottomY = safeArea.Height * 0.41f;  // ~302px - bottom positions (moved down more)
+            float topY = safeArea.Height * 0.36f;     // ~266px - top positions (alternating arc, moved down more)
+
+            // Calculate spacing based on number of players
+            // More players = tighter spacing, fewer players = more spread out
+            float leftMargin = safeArea.Width * 0.10f;
+            float rightMargin = safeArea.Width * 0.10f;
+            float usableWidth = safeArea.Width - leftMargin - rightMargin;
+
+            // Distribute players evenly across the usable width
+            for (int i = 0; i < playerCount; i++)
+            {
+                float xPosition;
+                if (playerCount == 1)
+                {
+                    // Single player: center
+                    xPosition = safeArea.Width * 0.5f;
+                }
+                else
+                {
+                    // Multiple players: spread evenly
+                    float spacing = usableWidth / (playerCount - 1);
+                    xPosition = leftMargin + (i * spacing);
+                }
+
+                // Alternate between bottom and top Y positions for visual variety
+                float yPosition = (i % 2 == 0) ? bottomY : topY;
+
+                playerCardOffset[i] = new Vector2(xPosition, yPosition);
+            }
         }
 
         /// <summary>
@@ -183,15 +609,15 @@ namespace Blackjack
         /// <returns>The position for the player's hand on the game table.</returns>
         private Vector2 GetPlayerCardPosition(int player)
         {
-            switch (player)
-            {
-                case 0:
-                case 1:
-                case 2:
-                    return playerCardOffset[player];
-                default:
-                    throw new ArgumentException(
-                        "Player index should be between 0 and 2", "player");
+            // Support dynamic number of players
+            if (playerCardOffset != null && player >= 0 && player < playerCardOffset.Length)
+            {
+                return playerCardOffset[player];
+            }
+            else
+            {
+                // Fallback to center if positions haven't been calculated yet
+                return new Vector2(safeArea.Width * 0.5f, safeArea.Height * 0.30f);
             }
         }
 
@@ -286,5 +712,82 @@ namespace Blackjack
         {
             blackJackGame.Double();
         }
+
+        // Phase 5: Gameplay Action Packet Handlers
+        private void HandleHitActionPacket(NetworkGamer sender, PacketReader reader)
+        {
+            var packet = Blackjack.Networking.HitActionPacket.Deserialize(reader);
+
+            // Only clients should process this - host already executed the action locally
+            if (networkSession != null && !networkSession.IsHost)
+            {
+                blackJackGame.HandleReceivedHitAction(packet.PlayerIndex);
+            }
+        }
+
+        private void HandleStandActionPacket(NetworkGamer sender, PacketReader reader)
+        {
+            var packet = Blackjack.Networking.StandActionPacket.Deserialize(reader);
+
+            // Only clients should process this - host already executed the action locally
+            if (networkSession != null && !networkSession.IsHost)
+            {
+                blackJackGame.HandleReceivedStandAction(packet.PlayerIndex);
+            }
+        }
+
+        private void HandleDoubleActionPacket(NetworkGamer sender, PacketReader reader)
+        {
+            var packet = Blackjack.Networking.DoubleActionPacket.Deserialize(reader);
+
+            // Only clients should process this - host already executed the action locally
+            if (networkSession != null && !networkSession.IsHost)
+            {
+                blackJackGame.HandleReceivedDoubleAction(packet.PlayerIndex);
+            }
+        }
+
+        private void HandleSplitActionPacket(NetworkGamer sender, PacketReader reader)
+        {
+            var packet = Blackjack.Networking.SplitActionPacket.Deserialize(reader);
+
+            // Only clients should process this - host already executed the action locally
+            if (networkSession != null && !networkSession.IsHost)
+            {
+                blackJackGame.HandleReceivedSplitAction(packet.PlayerIndex);
+            }
+        }
+
+        private void HandleInsuranceActionPacket(NetworkGamer sender, PacketReader reader)
+        {
+            var packet = Blackjack.Networking.InsuranceActionPacket.Deserialize(reader);
+
+            // Only clients should process this - host already executed the action locally
+            if (networkSession != null && !networkSession.IsHost)
+            {
+                blackJackGame.HandleReceivedInsuranceAction(packet.PlayerIndex);
+            }
+        }
+
+        private void HandleTurnChangedPacket(NetworkGamer sender, PacketReader reader)
+        {
+            var packet = Blackjack.Networking.TurnChangedPacket.Deserialize(reader);
+            System.Console.WriteLine($"[PACKET] Turn changed from {sender.Gamertag}, current player index: {packet.CurrentPlayerIndex}");
+
+            blackJackGame.HandleReceivedTurnChanged(packet.CurrentPlayerIndex);
+        }
+
+        /// <summary>
+        /// Updates button text after language change
+        /// </summary>
+        public void UpdateButtonText()
+        {
+            if (blackJackGame != null)
+            {
+                blackJackGame.UpdateButtonText();
+                var betComponent = blackJackGame.Game.Components.OfType<BetGameComponent>().FirstOrDefault();
+                betComponent?.UpdateButtonText();
+            }
+        }
     }
 }

+ 1 - 1
CardsStarterKit/Core/Game/Screens/InstructionScreen.cs

@@ -124,7 +124,7 @@ namespace Blackjack
             if (isExit)
             {
                 Rectangle safeArea = ScreenManager.SafeArea;
-                string text = "Loading...";
+                string text = Resources.Loading;
                 Vector2 measure = font.MeasureString(text);
                 Vector2 textPosition = new Vector2(safeArea.Center.X - measure.X / 2,
                     safeArea.Center.Y - measure.Y / 2);

+ 41 - 14
CardsStarterKit/Core/Game/Screens/MainMenuScreen.cs

@@ -16,8 +16,8 @@ namespace Blackjack
 {
     class MainMenuScreen : MenuScreen
     {
-        public static string Theme = "Red";
-
+        public static string Theme { get; set; } = "Red";
+        private bool needsRefresh = false;
 
         /// <summary>
         /// Initializes a new instance of the screen.
@@ -25,26 +25,52 @@ namespace Blackjack
         public MainMenuScreen()
             : base("")
         {
+            // Load theme from settings
+            Theme = GameSettings.Instance.Theme;
         }
 
         public override void LoadContent()
         {
+            BuildMenuEntries();
+            base.LoadContent();
+        }
+
+        public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
+        {
+            // If we were covered and now we're not, refresh the menu entries
+            // to pick up any language changes from the settings screen
+            if (!coveredByOtherScreen && needsRefresh)
+            {
+                BuildMenuEntries();
+                needsRefresh = false;
+            }
+            else if (coveredByOtherScreen)
+            {
+                needsRefresh = true;
+            }
+
+            base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
+        }
+
+        private void BuildMenuEntries()
+        {
+            // Clear existing entries
+            MenuEntries.Clear();
+
             // Create our menu entries.
-            MenuEntry startGameMenuEntry = new MenuEntry("Play");
-            MenuEntry themeGameMenuEntry = new MenuEntry("Theme");
-            MenuEntry exitMenuEntry = new MenuEntry("Exit");
+            MenuEntry startGameMenuEntry = new MenuEntry(Resources.Play);
+            MenuEntry settingsMenuEntry = new MenuEntry(Resources.Settings);
+            MenuEntry exitMenuEntry = new MenuEntry(Resources.Exit);
 
             // Hook up menu event handlers.
             startGameMenuEntry.Selected += StartGameMenuEntrySelected;
-            themeGameMenuEntry.Selected += ThemeGameMenuEntrySelected;
+            settingsMenuEntry.Selected += SettingsMenuEntrySelected;
             exitMenuEntry.Selected += OnCancel;
 
             // Add entries to the menu.
             MenuEntries.Add(startGameMenuEntry);
-            MenuEntries.Add(themeGameMenuEntry);
+            MenuEntries.Add(settingsMenuEntry);
             MenuEntries.Add(exitMenuEntry);
-
-            base.LoadContent();
         }
 
         /// <summary>
@@ -57,17 +83,18 @@ namespace Blackjack
             foreach (GameScreen screen in ScreenManager.GetScreens())
                 screen.ExitScreen();
 
-            ScreenManager.AddScreen(new GameplayScreen(Theme), null);
+            // Don't add BackgroundScreen - we don't want the logo on the session browser
+            ScreenManager.AddScreen(new SessionBrowserScreen(), null);
         }
 
         /// <summary>
-        /// Respond to "Theme" Item Selection
+        /// Respond to "Settings" Item Selection
         /// </summary>
         /// <param name="sender"></param>
         /// <param name="e"></param>
-        void ThemeGameMenuEntrySelected(object sender, EventArgs e)
+        void SettingsMenuEntrySelected(object sender, EventArgs e)
         {
-            ScreenManager.AddScreen(new OptionsMenu(), null);
+            ScreenManager.AddScreen(new SettingsScreen(), null);
         }
 
         /// <summary>
@@ -79,4 +106,4 @@ namespace Blackjack
             ScreenManager.Game.Exit();
         }
     }
-}
+}

+ 57 - 0
CardsStarterKit/Core/Game/Screens/MessageBoxScreen.cs

@@ -0,0 +1,57 @@
+//----------------------------------------------------------------------------- 
+// MessageBoxScreen.cs
+// Adapted from NetworkStateManagement sample for Blackjack
+// Shows a simple message box with OK button
+//-----------------------------------------------------------------------------
+using System;
+using Microsoft.Xna.Framework;
+using GameStateManagement;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Blackjack
+{
+    /// <summary>
+    /// A popup screen that displays a message and waits for the user to acknowledge.
+    /// </summary>
+    class MessageBoxScreen : GameScreen
+    {
+        string message;
+        MenuEntry okMenuEntry;
+        public event EventHandler<PlayerIndexEventArgs> Accepted;
+
+        public MessageBoxScreen(string message)
+        {
+            this.message = message;
+            IsPopup = true;
+        }
+
+        public override void LoadContent()
+        {
+            okMenuEntry = new MenuEntry(Resources.OK);
+            okMenuEntry.Selected += OkMenuEntrySelected;
+            //MenuEntries.Add(okMenuEntry);
+            base.LoadContent();
+        }
+
+        void OkMenuEntrySelected(object sender, PlayerIndexEventArgs e)
+        {
+            Accepted?.Invoke(this, e);
+            ExitScreen();
+        }
+
+        public override void Draw(GameTime gameTime)
+        {
+            ScreenManager.FadeBackBufferToBlack(0.5f);
+            SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
+            SpriteFont font = ScreenManager.Font;
+            Vector2 viewportSize = new Vector2(
+                ScreenManager.GraphicsDevice.Viewport.Width,
+                ScreenManager.GraphicsDevice.Viewport.Height);
+            Vector2 textSize = font.MeasureString(message);
+            Vector2 position = (viewportSize - textSize) / 2;
+            spriteBatch.Begin();
+            spriteBatch.DrawString(font, message, position, Color.White);
+            spriteBatch.End();
+        }
+    }
+}

+ 196 - 0
CardsStarterKit/Core/Game/Screens/NetworkBusyScreen.cs

@@ -0,0 +1,196 @@
+//-----------------------------------------------------------------------------
+// NetworkBusyScreen.cs
+//
+// Microsoft XNA Community Game Platform
+// Copyright (C) Microsoft Corporation. All rights reserved.
+//-----------------------------------------------------------------------------
+
+using System;
+using System.Threading.Tasks;
+using GameStateManagement;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+
+
+namespace Blackjack
+{
+	/// <summary>
+	/// When an asynchronous network operation (for instance searching for or joining a
+	/// session) is in progress, this screen displays a busy indicator and blocks input.
+	/// It monitors a Task&lt;T&gt; returned by the async call, showing the indicator while
+	/// the task is running. When the task completes, it raises an event with the
+	/// operation result (or null on failure), then automatically dismisses itself.
+	/// Because this screen sits on top while the async operation is in progress, it
+	/// captures all user input to prevent interaction with underlying screens until
+	/// the operation completes.
+	/// </summary>
+	class NetworkBusyScreen<T> : GameScreen
+	{
+		readonly Task<T> task;
+		readonly System.Threading.CancellationTokenSource cts;
+		bool completionRaised;
+		Texture2D gradientTexture;
+		Texture2D busyTexture;
+
+		event EventHandler<OperationCompletedEventArgs> operationCompleted;
+		public event EventHandler<OperationCompletedEventArgs> OperationCompleted
+		{
+			add { operationCompleted += value; }
+			remove { operationCompleted -= value; }
+		}
+
+		/// <summary>
+		/// Constructs a network busy screen for the specified asynchronous operation.
+		/// Accepts a Task&lt;T&gt; representing the in-flight operation.
+		/// </summary>
+		public NetworkBusyScreen(Task<T> task)
+		{
+			this.task = task;
+			this.cts = null;
+
+			IsPopup = true;
+
+			TransitionOnTime = TimeSpan.FromSeconds(0.1);
+			TransitionOffTime = TimeSpan.FromSeconds(0.2);
+		}
+
+		/// <summary>
+		/// Overload that accepts a CancellationTokenSource to allow user cancellation.
+		/// </summary>
+		public NetworkBusyScreen(Task<T> task, System.Threading.CancellationTokenSource cts)
+		{
+			this.task = task;
+			this.cts = cts;
+
+			IsPopup = true;
+
+			TransitionOnTime = TimeSpan.FromSeconds(0.1);
+			TransitionOffTime = TimeSpan.FromSeconds(0.2);
+		}
+
+		/// <summary>
+		/// Loads graphics content for this screen. This uses the shared ContentManager
+		/// provided by the Game class, so the content will remain loaded forever.
+		/// Whenever a subsequent NetworkBusyScreen tries to load this same content,
+		/// it will just get back another reference to the already loaded data.
+		/// </summary>
+		public override void LoadContent()
+		{
+			ContentManager content = ScreenManager.Game.Content;
+
+			gradientTexture = content.Load<Texture2D>("Images/UI/gradient");
+			busyTexture = content.Load<Texture2D>("Images/GamePadCursor"); // TODO change to hourglass
+		}
+
+		/// <summary>
+		/// Updates the NetworkBusyScreen, checking whether the underlying Task has
+		/// completed and raising OperationCompleted when it does.
+		/// </summary>
+		public override void Update(GameTime gameTime, bool otherScreenHasFocus,
+							bool coveredByOtherScreen)
+		{
+			base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
+
+			// Optional: allow user to cancel (Esc or B)
+			if (!completionRaised && cts != null)
+			{
+				var kb = Keyboard.GetState();
+				if (kb.IsKeyDown(Keys.Escape))
+				{
+					cts.Cancel();
+				}
+				var gp = GamePad.GetState(PlayerIndex.One);
+				if (gp.IsConnected && gp.IsButtonDown(Buttons.B))
+				{
+					cts.Cancel();
+				}
+			}
+
+			// Has our asynchronous operation completed?
+			if (!completionRaised && task != null && task.IsCompleted)
+			{
+				object resultObject = default(T);
+				Exception error = null;
+				try
+				{
+					// Accessing Result will throw if the task faulted/canceled.
+					// We catch here and allow handlers to present error UI.
+					resultObject = task.Result;
+				}
+				catch (Exception ex)
+				{
+					// Leave result as default (usually null) to signal failure to handlers.
+					error = (task?.Exception?.GetBaseException()) ?? ex;
+				}
+
+				var handler = operationCompleted;
+				if (handler != null)
+				{
+					handler(this, new OperationCompletedEventArgs(resultObject, error));
+				}
+
+				completionRaised = true;
+				// Clear handlers to avoid reentry if this screen lingers during transition off
+				operationCompleted = null;
+				ExitScreen();
+			}
+		}
+
+		/// <summary>
+		/// Draws the NetworkBusyScreen.
+		/// </summary>
+		public override void Draw(GameTime gameTime)
+		{
+			SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
+			SpriteFont font = ScreenManager.Font;
+
+            string message = Resources.NetworkBusy;
+
+			const int hPad = 32;
+			const int vPad = 16;
+
+			// Center the message text in the viewport.
+			Vector2 viewportSize = new Vector2(ScreenManager.BASE_BUFFER_WIDTH, ScreenManager.BASE_BUFFER_HEIGHT);
+			Vector2 textSize = font.MeasureString(message);
+
+			// Add enough room to spin a cat.
+			Vector2 catSize = new Vector2(busyTexture.Width);
+
+			textSize.X = Math.Max(textSize.X, catSize.X);
+			textSize.Y += catSize.Y + vPad;
+
+			Vector2 textPosition = (viewportSize - textSize) / 2;
+
+			// The background includes a border somewhat larger than the text itself.
+			Rectangle backgroundRectangle = new Rectangle((int)textPosition.X - hPad,
+							(int)textPosition.Y - vPad,
+							(int)textSize.X + hPad * 2,
+							(int)textSize.Y + vPad * 2);
+
+			// Fade the popup alpha during transitions.
+			Color color = Color.White * TransitionAlpha;
+
+			spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, ScreenManager.GlobalTransformation);
+
+			// Draw the background rectangle.
+			spriteBatch.Draw(gradientTexture, backgroundRectangle, color);
+
+			// Draw the message box text.
+			spriteBatch.DrawString(font, message, textPosition, color);
+
+			// Draw the spinning cat progress indicator.
+			float catRotation = (float)gameTime.TotalGameTime.TotalSeconds * 3;
+
+			Vector2 catPosition = new Vector2(textPosition.X + textSize.X / 2,
+						textPosition.Y + textSize.Y -
+								catSize.Y / 2);
+
+			spriteBatch.Draw(busyTexture, catPosition, null, color, catRotation,
+				catSize / 2, 1, SpriteEffects.None, 0);
+
+			spriteBatch.End();
+		}
+	}
+}

+ 130 - 0
CardsStarterKit/Core/Game/Screens/NetworkSessionComponent.cs

@@ -0,0 +1,130 @@
+//----------------------------------------------------------------------------- 
+// NetworkSessionComponent.cs
+// Adapted from NetworkStateManagement sample for Blackjack
+//-----------------------------------------------------------------------------
+using System;
+using GameStateManagement;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Net;
+
+namespace Blackjack
+{
+    /// <summary>
+    /// Component in charge of owning and updating the current NetworkSession object.
+    /// Responsible for calling NetworkSession.Update and exposing the session as a service.
+    /// </summary>
+    class NetworkSessionComponent : GameComponent
+    {
+        ScreenManager screenManager;
+        NetworkSession networkSession;
+        bool notifyWhenPlayersJoinOrLeave;
+        string sessionEndMessage;
+
+        public NetworkSessionComponent(ScreenManager screenManager, NetworkSession networkSession)
+            : base(screenManager.Game)
+        {
+            this.screenManager = screenManager;
+            this.networkSession = networkSession;
+            networkSession.GamerJoined += GamerJoined;
+            networkSession.GamerLeft += GamerLeft;
+            networkSession.SessionEnded += NetworkSessionEnded;
+        }
+
+        public static void Create(ScreenManager screenManager, NetworkSession networkSession)
+        {
+            Game game = screenManager.Game;
+
+            // Remove any existing NetworkSession service and component before adding new ones
+            if (game.Services.GetService(typeof(NetworkSession)) != null)
+            {
+                game.Services.RemoveService(typeof(NetworkSession));
+            }
+
+            // Remove any existing NetworkSessionComponent
+            var existingComponent = FindSessionComponent(game);
+            if (existingComponent != null)
+            {
+                game.Components.Remove(existingComponent);
+                existingComponent.Dispose();
+            }
+
+            game.Services.AddService(typeof(NetworkSession), networkSession);
+            game.Components.Add(new NetworkSessionComponent(screenManager, networkSession));
+        }
+
+        /// <summary>
+        /// Searches through the Game.Components collection to find the NetworkSessionComponent (if any exists).
+        /// </summary>
+        static NetworkSessionComponent FindSessionComponent(Game game)
+        {
+            foreach (var component in game.Components)
+            {
+                if (component is NetworkSessionComponent sessionComponent)
+                    return sessionComponent;
+            }
+            return null;
+        }
+
+        public override void Initialize()
+        {
+            base.Initialize();
+            // Optionally hook up message display here if needed
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                Game.Components.Remove(this);
+                Game.Services.RemoveService(typeof(NetworkSession));
+                if (networkSession != null)
+                {
+                    networkSession.Dispose();
+                    networkSession = null;
+                }
+            }
+            base.Dispose(disposing);
+        }
+
+        public override void Update(GameTime gameTime)
+        {
+            if (networkSession == null)
+                return;
+            try
+            {
+                networkSession.Update();
+                if (networkSession.SessionState == NetworkSessionState.Ended)
+                {
+                    LeaveSession();
+                }
+            }
+            catch (Exception)
+            {
+                sessionEndMessage = "Network error.";
+                LeaveSession();
+            }
+        }
+
+        void GamerJoined(object sender, GamerJoinedEventArgs e)
+        {
+            // Optionally display message or update UI
+        }
+
+        void GamerLeft(object sender, GamerLeftEventArgs e)
+        {
+            // Optionally display message or update UI
+        }
+
+        void NetworkSessionEnded(object sender, NetworkSessionEndedEventArgs e)
+        {
+            sessionEndMessage = "Session ended.";
+            LeaveSession();
+        }
+
+        void LeaveSession()
+        {
+            Dispose(true);
+            // Optionally transition to main menu or show message
+        }
+    }
+}

+ 44 - 0
CardsStarterKit/Core/Game/Screens/OperationCompletedEventArgs.cs

@@ -0,0 +1,44 @@
+//-----------------------------------------------------------------------------
+// OperationCompletedEventArgs.cs
+//
+// Microsoft XNA Community Game Platform
+// Copyright (C) Microsoft Corporation. All rights reserved.
+//-----------------------------------------------------------------------------
+
+using System;
+
+namespace Blackjack
+{
+    /// <summary>
+    /// Custom EventArgs class used by the NetworkBusyScreen.OperationCompleted event.
+    /// </summary>
+    class OperationCompletedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets or sets the result of the network operation that has just completed.
+        /// </summary>
+        public object Result { get; set; }
+
+        /// <summary>
+        /// Gets or sets the exception that caused the operation to fail, if any.
+        /// </summary>
+        public Exception Exception { get; set; }
+
+        /// <summary>
+        /// Constructs a new event arguments class.
+        /// </summary>
+        public OperationCompletedEventArgs(object result)
+        {
+            this.Result = result;
+        }
+
+        /// <summary>
+        /// Constructs a new event arguments class with an optional exception.
+        /// </summary>
+        public OperationCompletedEventArgs(object result, Exception exception)
+        {
+            this.Result = result;
+            this.Exception = exception;
+        }
+    }
+}

+ 0 - 120
CardsStarterKit/Core/Game/Screens/OptionsMenu.cs

@@ -1,120 +0,0 @@
-//-----------------------------------------------------------------------------
-// OptionsMenu.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-
-using System;
-using System.Collections.Generic;
-using System.Text;
-using GameStateManagement;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Graphics;
-using CardsFramework;
-using System.IO;
-
-namespace Blackjack
-{
-    class OptionsMenu : MenuScreen
-    {
-        private CardsGame cardGame;
-        Dictionary<string, Texture2D> themes = new Dictionary<string, Texture2D>();
-        AnimatedGameComponent card;
-        Texture2D background;
-        Rectangle safeArea;
-
-        /// <summary>
-        /// Initializes a new instance of the screen.
-        /// </summary>
-        public OptionsMenu()
-            : base("")
-        {
-        }
-
-        public OptionsMenu(CardsGame cardGame)
-            : base("")
-        {
-            this.cardGame = cardGame;
-        }
-
-        /// <summary>
-        /// Loads content required by the screen, and initializes the displayed menu.
-        /// </summary>
-        public override void LoadContent()
-        {
-            safeArea = ScreenManager.SafeArea;
-            // Create our menu entries.
-            MenuEntry themeGameMenuEntry = new MenuEntry("Deck");
-            MenuEntry returnMenuEntry = new MenuEntry("Return");
-
-            // Hook up menu event handlers.
-            themeGameMenuEntry.Selected += ThemeGameMenuEntrySelected;
-            returnMenuEntry.Selected += OnCancel;
-
-            // Add entries to the menu.
-            MenuEntries.Add(themeGameMenuEntry);
-            MenuEntries.Add(returnMenuEntry);
-
-            themes.Add("Red", ScreenManager.Game.Content.Load<Texture2D>(
-                Path.Combine("Images", "Cards", "CardBack_Red")));
-            themes.Add("Blue", ScreenManager.Game.Content.Load<Texture2D>(
-                Path.Combine("Images", "Cards", "CardBack_Blue")));
-            background = ScreenManager.Game.Content.Load<Texture2D>(
-                Path.Combine("Images", "UI", "table"));
-
-            card = new AnimatedGameComponent(cardGame,
-                themes[MainMenuScreen.Theme], ScreenManager.SpriteBatch, ScreenManager.GlobalTransformation)
-            {
-                CurrentPosition = new Vector2(safeArea.Center.X, safeArea.Center.Y - 50)
-            };
-
-            ScreenManager.Game.Components.Add(card);
-
-            base.LoadContent();
-        }
-
-        /// <summary>
-        /// Respond to "Theme" Item Selection
-        /// </summary>
-        /// <param name="sender"></param>
-        /// <param name="e"></param>
-        void ThemeGameMenuEntrySelected(object sender, EventArgs e)
-        {
-            if (MainMenuScreen.Theme == "Red")
-            {
-                MainMenuScreen.Theme = "Blue";
-            }
-            else
-            {
-                MainMenuScreen.Theme = "Red";
-            }
-            card.CurrentFrame = themes[MainMenuScreen.Theme];
-        }
-
-        /// <summary>
-        /// Respond to "Return" Item Selection
-        /// </summary>
-        /// <param name="playerIndex"></param>
-        protected override void OnCancel(PlayerIndex playerIndex)
-        {
-            ScreenManager.Game.Components.Remove(card);
-            ExitScreen();
-        }
-
-        /// <summary>
-        /// Draws the menu.
-        /// </summary>
-        /// <param name="gameTime"></param>
-        public override void Draw(GameTime gameTime)
-        {
-            ScreenManager.SpriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, ScreenManager.GlobalTransformation);
-
-            // Draw the card back
-            ScreenManager.SpriteBatch.Draw(background, ScreenManager.SafeArea, Color.White * TransitionAlpha);
-
-            ScreenManager.SpriteBatch.End();
-            base.Draw(gameTime);
-        }
-    }
-}

+ 3 - 3
CardsStarterKit/Core/Game/Screens/PauseScreen.cs

@@ -27,8 +27,8 @@ namespace Blackjack
         public override void LoadContent()
         {
             // Create our menu entries.
-            MenuEntry returnGameMenuEntry = new MenuEntry("Back");
-            MenuEntry exitMenuEntry = new MenuEntry("Quit");
+            MenuEntry returnGameMenuEntry = new MenuEntry(Resources.Back);
+            MenuEntry exitMenuEntry = new MenuEntry(Resources.Quit);
 
             // Hook up menu event handlers.
             returnGameMenuEntry.Selected += ReturnGameMenuEntrySelected;
@@ -100,4 +100,4 @@ namespace Blackjack
             ScreenManager.AddScreen(new MainMenuScreen(), null);
         }
     }
-}
+}

+ 301 - 0
CardsStarterKit/Core/Game/Screens/SessionBrowserScreen.cs

@@ -0,0 +1,301 @@
+//----------------------------------------------------------------------------- 
+// SessionBrowserScreen.cs
+//
+// Shows available sessions and allows hosting a new game.
+//-----------------------------------------------------------------------------
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Net;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using GameStateManagement;
+using CardsFramework;
+
+namespace Blackjack
+{
+    class SessionBrowserScreen : MenuScreen
+    {
+        MenuEntry hostGameMenuEntry;
+        MenuEntry refreshMenuEntry;
+        MenuEntry backMenuEntry;
+        List<MenuEntry> sessionEntries = new List<MenuEntry>();
+        List<AvailableNetworkSession> availableSessions = new List<AvailableNetworkSession>();
+
+        TimeSpan timeSinceLastSearch = TimeSpan.Zero;
+        const float AutoRefreshInterval = 5.0f; // Auto-refresh every 5 seconds
+        bool isSearching = false;
+
+        public SessionBrowserScreen()
+            : base(Resources.JoinOrHostGame)
+        {
+        }
+
+        public override void LoadContent()
+        {
+            hostGameMenuEntry = new MenuEntry(Resources.HostNewGame);
+            refreshMenuEntry = new MenuEntry(Resources.Refresh);
+            backMenuEntry = new MenuEntry(Resources.Back);
+
+            hostGameMenuEntry.Selected += HostGameMenuEntrySelected;
+            refreshMenuEntry.Selected += RefreshMenuEntrySelected;
+            backMenuEntry.Selected += OnCancel;
+
+            MenuEntries.Add(hostGameMenuEntry);
+            MenuEntries.Add(refreshMenuEntry);
+            MenuEntries.Add(backMenuEntry);
+
+            // Start async session discovery
+            BeginFindSessions();
+
+            base.LoadContent();
+        }
+
+        void RefreshMenuEntrySelected(object sender, EventArgs e)
+        {
+            // Manually trigger a refresh
+            if (!isSearching)
+            {
+                BeginFindSessions();
+                timeSinceLastSearch = TimeSpan.Zero;
+            }
+        }
+
+        void HostGameMenuEntrySelected(object sender, EventArgs e)
+        {
+            // Host a new session and go to lobby
+            // Use SystemLink for local network testing (PlayerMatch requires online services)
+            var asyncResult = NetworkSession.CreateAsync(
+                NetworkSessionType.SystemLink,
+                BlackjackConstants.MinPlayers, // local gamers
+                BlackjackConstants.MaxPlayers, // max gamers
+                0, // private slots
+                null);
+            var busyScreen = new NetworkBusyScreen<NetworkSession>(asyncResult);
+            busyScreen.OperationCompleted += (s, evt) =>
+            {
+                var networkSession = evt.Result as NetworkSession;
+                if (networkSession != null)
+                {
+                    NetworkSessionComponent.Create(ScreenManager, networkSession);
+                    ScreenManager.AddScreen(new BlackjackLobbyScreen(networkSession), null);
+                }
+                else
+                {
+                    ScreenManager.AddScreen(new MessageBoxScreen(Resources.FailedToCreateSession), null);
+                }
+            };
+            ScreenManager.AddScreen(busyScreen, null);
+        }
+
+        void JoinSessionMenuEntrySelected(object sender, EventArgs e)
+        {
+            var entry = sender as AvailableSessionMenuEntry;
+            var availableSession = entry?.AvailableSession;
+            if (availableSession != null)
+            {
+                var asyncResult = NetworkSession.JoinAsync(availableSession);
+                var busyScreen = new NetworkBusyScreen<NetworkSession>(asyncResult);
+                busyScreen.OperationCompleted += (s, evt) =>
+                {
+                    var networkSession = evt.Result as NetworkSession;
+                    if (networkSession != null)
+                    {
+                        NetworkSessionComponent.Create(ScreenManager, networkSession);
+                        ScreenManager.AddScreen(new BlackjackLobbyScreen(networkSession), null);
+                    }
+                    else
+                    {
+                        ScreenManager.AddScreen(new MessageBoxScreen(Resources.FailedToJoinSession), null);
+                    }
+                };
+                ScreenManager.AddScreen(busyScreen, null);
+            }
+        }
+
+        void RefreshSessionList()
+        {
+            // Remove all existing session entries from MenuEntries  
+            foreach (var entry in sessionEntries)
+            {
+                MenuEntries.Remove(entry);
+            }
+            sessionEntries.Clear();
+
+            // Don't add session entries to MenuEntries - we'll draw them separately
+            // Just create the entries for the available sessions
+            foreach (var session in availableSessions)
+            {
+                var entry = new AvailableSessionMenuEntry(session);
+                entry.Selected += JoinSessionMenuEntrySelected;
+                sessionEntries.Add(entry);
+            }
+        }
+
+        public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
+        {
+            base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
+
+            // Auto-refresh session list periodically
+            if (!isSearching && !coveredByOtherScreen)
+            {
+                timeSinceLastSearch += gameTime.ElapsedGameTime;
+                if (timeSinceLastSearch.TotalSeconds >= AutoRefreshInterval)
+                {
+                    BeginFindSessions();
+                    timeSinceLastSearch = TimeSpan.Zero;
+                }
+            }
+
+            // Update refresh button text to show status
+            if (refreshMenuEntry != null)
+            {
+                refreshMenuEntry.Text = isSearching ? Resources.NetworkBusy : Resources.Refresh;
+            }
+        }
+
+        public override void HandleInput(InputState input)
+        {
+            // Handle input for session entries (manual click detection)
+            // Check if mouse button was clicked (pressed then released)
+            if (input.CurrentMouseState.LeftButton == ButtonState.Released &&
+                input.LastMouseState.LeftButton == ButtonState.Pressed)
+            {
+                Vector2 mousePosition = new Vector2(input.CurrentMouseState.X, input.CurrentMouseState.Y);
+                SpriteFont font = ScreenManager.Font;
+
+                // Check if clicked on a session entry
+                if (availableSessions.Count > 0)
+                {
+                    Vector2 headerPosition = new Vector2(ScreenManager.SafeArea.Left + 100, ScreenManager.SafeArea.Top + 150);
+                    Vector2 sessionPosition = new Vector2(ScreenManager.SafeArea.Left + 120, headerPosition.Y + font.LineSpacing * 1.5f);
+                    float scale = 0.85f;
+
+                    for (int i = 0; i < sessionEntries.Count; i++)
+                    {
+                        var sessionEntry = sessionEntries[i];
+                        Vector2 textSize = font.MeasureString(sessionEntry.Text) * scale;
+                        Rectangle hitBox = new Rectangle(
+                            (int)sessionPosition.X,
+                            (int)sessionPosition.Y,
+                            (int)textSize.X,
+                            (int)(font.LineSpacing * scale)
+                        );
+
+                        if (hitBox.Contains(mousePosition))
+                        {
+                            // Clicked on this session - join it
+                            JoinSessionMenuEntrySelected(sessionEntry, EventArgs.Empty);
+                            return;
+                        }
+
+                        sessionPosition.Y += font.LineSpacing * scale + 10;
+                    }
+                }
+            }
+
+            // Let base class handle button input
+            base.HandleInput(input);
+        }
+
+        protected override void OnCancel(PlayerIndex playerIndex)
+        {
+            // Exit all screens and return to main menu (without BackgroundScreen to avoid logo)
+            foreach (GameScreen screen in ScreenManager.GetScreens())
+                screen.ExitScreen();
+
+            ScreenManager.AddScreen(new BackgroundScreen(), null);
+            ScreenManager.AddScreen(new MainMenuScreen(), null);
+        }
+
+        public override void Draw(GameTime gameTime)
+        {
+            // Draw solid background to cover any BackgroundScreen logo
+            ScreenManager.GraphicsDevice.Clear(new Color(50, 20, 20)); // Dark red background
+
+            // Draw menu entries (bottom buttons) and title from base class
+            base.Draw(gameTime);
+
+            SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
+            SpriteFont font = ScreenManager.Font;
+
+            spriteBatch.Begin();
+
+            // Draw "Available Games:" section header and session list
+            if (availableSessions.Count > 0)
+            {
+                // Draw header
+                Vector2 headerPosition = new Vector2(ScreenManager.SafeArea.Left + 100, ScreenManager.SafeArea.Top + 150);
+                spriteBatch.DrawString(font, Resources.AvailableGames, headerPosition, Color.Yellow, 0f, Vector2.Zero, 0.9f, SpriteEffects.None, 0f);
+
+                // Draw session entries in a vertical list below the header
+                Vector2 sessionPosition = new Vector2(ScreenManager.SafeArea.Left + 120, headerPosition.Y + font.LineSpacing * 1.5f);
+
+                for (int i = 0; i < sessionEntries.Count; i++)
+                {
+                    var sessionEntry = sessionEntries[i];
+                    Color color = Color.White;
+                    float scale = 0.85f;
+
+                    // Draw the session entry text
+                    string sessionText = sessionEntry.Text;
+                    spriteBatch.DrawString(font, sessionText, sessionPosition, color, 0f, Vector2.Zero, scale, SpriteEffects.None, 0f);
+
+                    // Move down for next entry
+                    sessionPosition.Y += font.LineSpacing * scale + 10; // Add some padding
+                }
+            }
+
+            // Draw status at bottom left
+            Vector2 statusPosition = new Vector2(ScreenManager.SafeArea.Left + 50, ScreenManager.SafeArea.Bottom - 80);
+
+            var pluralGames = availableSessions.Count == 0 || availableSessions.Count > 1 ? "s" : "";
+            string statusText = isSearching
+                ? Resources.SearchingForGames
+                : string.Format(Resources.FoundGames, availableSessions.Count, pluralGames);
+
+            spriteBatch.DrawString(font, statusText, statusPosition, Color.LightGreen, 0f, Vector2.Zero, 0.8f, SpriteEffects.None, 0f);
+
+            // Show auto-refresh timer
+            if (!isSearching)
+            {
+                int secondsUntilRefresh = (int)(AutoRefreshInterval - timeSinceLastSearch.TotalSeconds);
+                string timerText = string.Format(Resources.AutoRefreshIn, secondsUntilRefresh);
+                statusPosition.Y += font.LineSpacing;
+                spriteBatch.DrawString(font, timerText, statusPosition, Color.Gray, 0f, Vector2.Zero, 0.7f, SpriteEffects.None, 0f);
+            }
+
+            spriteBatch.End();
+        }
+
+        // Helper class for session info
+        // Deprecated: AvailableSession replaced by AvailableNetworkSession
+
+        void BeginFindSessions()
+        {
+            if (isSearching)
+                return; // Already searching
+
+            isSearching = true;
+
+            var asyncResult = NetworkSession.FindAsync(
+                NetworkSessionType.SystemLink,
+                1, // local gamers
+                null);
+            var busyScreen = new NetworkBusyScreen<AvailableNetworkSessionCollection>(asyncResult);
+            busyScreen.OperationCompleted += (s, evt) =>
+            {
+                isSearching = false;
+                var foundSessions = evt.Result as AvailableNetworkSessionCollection;
+                availableSessions.Clear();
+                if (foundSessions != null)
+                {
+                    foreach (var session in foundSessions)
+                        availableSessions.Add(session);
+                }
+                RefreshSessionList();
+            };
+            ScreenManager.AddScreen(busyScreen, null);
+        }
+    }
+}

+ 739 - 0
CardsStarterKit/Core/Game/Screens/SettingsScreen.cs

@@ -0,0 +1,739 @@
+//-----------------------------------------------------------------------------
+// SettingsScreen.cs
+//
+// Settings screen with custom layout for adjusting game options
+//-----------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using GameStateManagement;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using Microsoft.Xna.Framework.Content;
+using System.IO;
+
+namespace Blackjack
+{
+    class SettingsScreen : GameScreen
+    {
+        private Texture2D background;
+        private Texture2D cardBackTexture;
+        private Texture2D buttonRegularTexture;
+        private Texture2D buttonPressedTexture;
+        // Fonts are accessed directly from ScreenManager to ensure we always use current font
+        private GameSettings settings;
+        private Rectangle safeArea;
+
+        // Card preview
+        private Vector2 cardPreviewPosition;
+        private Vector2 cardSize;
+
+        // Setting item areas
+        private List<SettingItem> settingItems = new List<SettingItem>();
+        private int selectedIndex = -1;
+        private int hoveredIndex = -1;
+        private int pressedButtonIndex = -1; // Which button is being pressed (-1 for none, 0 for left, 1 for right)
+
+        // Pagination
+        private int currentPage = 0;
+        private const int TotalPages = 2;
+        private Rectangle nextButtonBounds;
+        private Rectangle prevButtonBounds;
+        private bool isNextButtonPressed = false;
+        private bool isPrevButtonPressed = false;
+
+        private Rectangle backButtonBounds;
+        private bool isBackButtonPressed = false;
+
+        private InputHelper inputHelper;
+        private float itemSpacing;
+        private float groupSpacing;
+        private float leftMargin;
+        private float buttonSize;
+        private float rightButtonMargin;
+        private float buttonSpacing;
+
+        public SettingsScreen()
+        {
+            TransitionOnTime = TimeSpan.FromSeconds(0.5);
+            TransitionOffTime = TimeSpan.FromSeconds(0.5);
+            settings = GameSettings.Instance;
+        }
+
+        public override void LoadContent()
+        {
+            ContentManager content = ScreenManager.Game.Content;
+
+            background = content.Load<Texture2D>("Images/UI/table");
+
+            // Fonts are accessed directly from ScreenManager properties (no caching)
+
+            buttonRegularTexture = content.Load<Texture2D>("Images/ButtonRegular");
+            buttonPressedTexture = content.Load<Texture2D>("Images/ButtonPressed");
+
+            // Load current theme card back
+            string themeCardBack = settings.Theme == "Red" ? "CardBack_Red" : "CardBack_Blue";
+            cardBackTexture = content.Load<Texture2D>($"Images/Cards/{themeCardBack}");
+
+            // Initialize input helper
+            inputHelper = new InputHelper(ScreenManager);
+
+            Viewport viewport = ScreenManager.GraphicsDevice.Viewport;
+            safeArea = viewport.TitleSafeArea;
+
+            // Calculate proportional spacing and sizes based on screen height
+            float heightScale = safeArea.Height / 720f; // Scale based on 720p baseline
+            itemSpacing = 45f * heightScale;
+            groupSpacing = 60f * heightScale;
+            leftMargin = 80f * heightScale;
+            buttonSize = 32f * heightScale;
+            rightButtonMargin = 10f * heightScale;
+            buttonSpacing = 168f * heightScale; // Space between left and right buttons
+
+            // Calculate card preview position and size (proportional to screen)
+            cardSize = new Vector2(80 * heightScale, 120 * heightScale);
+            cardPreviewPosition = new Vector2(
+                safeArea.Center.X - cardSize.X / 2,
+                safeArea.Top + 50 * heightScale
+            );
+
+            BuildSettingItems();
+
+            base.LoadContent();
+        }
+
+        /// <summary>
+        /// Reloads fonts when language changes between CJK and non-CJK languages.
+        /// </summary>
+        public void ReloadFonts(bool useCJKFont)
+        {
+            // No need to cache fonts - they're accessed directly from ScreenManager
+            // Don't rebuild items here - will be done after language is set to avoid
+            // measuring text in new language with old fonts
+        }
+
+        private void BuildSettingItems()
+        {
+            settingItems.Clear();
+            float yPos = cardPreviewPosition.Y + cardSize.Y + 20; // Start below card preview
+
+            if (currentPage == 0)
+            {
+                // Page 1: DISPLAY and AUDIO
+                AddHeader(Resources.SettingsDisplay, ref yPos);
+                AddCycleSetting(Resources.SettingsLanguage, GetLanguageDisplay,
+                    CycleToPreviousLanguage,
+                    CycleToNextLanguage,
+                    null, ref yPos);
+                AddCycleSetting(Resources.SettingsCardBackTheme, () => settings.Theme,
+                    () => settings.Theme = settings.Theme == "Red" ? "Blue" : "Red",
+                    () => settings.Theme = settings.Theme == "Red" ? "Blue" : "Red",
+                    () =>
+                    {
+                        // Reload card back texture
+                        string themeCardBack = settings.Theme == "Red" ? "CardBack_Red" : "CardBack_Blue";
+                        cardBackTexture = ScreenManager.Game.Content.Load<Texture2D>($"Images/Cards/{themeCardBack}");
+                        MainMenuScreen.Theme = settings.Theme;
+                    },
+                    ref yPos);
+                AddCycleSetting(Resources.SettingsCurrency, () => settings.Currency,
+                    CycleToPreviousCurrency,
+                    CycleToNextCurrency,
+                    null, ref yPos);
+
+                yPos += groupSpacing - itemSpacing; // Extra space before next group
+
+                // AUDIO section
+                AddHeader(Resources.SettingsAudio, ref yPos);
+                AddSliderSetting(Resources.SettingsSoundVolume, () => settings.SoundVolume,
+                    (v) => settings.SoundVolume = v, ref yPos);
+                AddSliderSetting(Resources.SettingsMusicVolume, () => settings.MusicVolume,
+                    (v) => settings.MusicVolume = v, ref yPos);
+            }
+            else if (currentPage == 1)
+            {
+                // Page 2: GAMEPLAY and AI PLAYERS
+                AddHeader(Resources.SettingsGameplay, ref yPos);
+                AddCycleSetting(Resources.SettingsAnimationSpeed, () => settings.AnimationSpeed.ToString(),
+                    () =>
+                    {
+                        settings.AnimationSpeed = settings.AnimationSpeed switch
+                        {
+                            AnimationSpeed.Fast => AnimationSpeed.Normal,
+                            AnimationSpeed.Normal => AnimationSpeed.Slow,
+                            _ => AnimationSpeed.Fast
+                        };
+                    },
+                    () =>
+                    {
+                        settings.AnimationSpeed = settings.AnimationSpeed switch
+                        {
+                            AnimationSpeed.Slow => AnimationSpeed.Normal,
+                            AnimationSpeed.Normal => AnimationSpeed.Fast,
+                            _ => AnimationSpeed.Slow
+                        };
+                    },
+                    null, ref yPos);
+                AddCheckboxSetting(Resources.SettingsAutoStandOn21, () => settings.AutoStandOn21,
+                    (v) => settings.AutoStandOn21 = v, ref yPos);
+                AddCheckboxSetting(Resources.SettingsShowCardCount, () => settings.ShowCardCount,
+                    (v) => settings.ShowCardCount = v, ref yPos);
+                AddCheckboxSetting(Resources.SettingsPersistWinnings, () => settings.PersistWinnings,
+                    (v) => settings.PersistWinnings = v, ref yPos);
+
+                yPos += groupSpacing - itemSpacing; // Extra space before next group
+
+                // AI PLAYERS section
+                AddHeader(Resources.SettingsAIPlayers, ref yPos);
+                AddCounterSetting(Resources.SettingsMaxAIPlayers, () => settings.MaxAIPlayers, 0, GameSettings.GetPlatformMaxAIPlayers(),
+                    (v) => settings.MaxAIPlayers = (byte)v, ref yPos);
+                AddCheckboxSetting(Resources.SettingsFillEmptySlots, () => settings.FillEmptySlotsWithAI,
+                    (v) => settings.FillEmptySlotsWithAI = v, ref yPos);
+            }
+
+            // Calculate navigation button bounds at bottom (proportional to screen)
+            float heightScale = safeArea.Height / 720f;
+            int navButtonY = safeArea.Bottom - (int)(60 * heightScale);
+            int navButtonHeight = (int)(40 * heightScale);
+
+            // Calculate button widths based on text size with padding
+            Vector2 prevTextSize = ScreenManager.RegularFont.MeasureString(Resources.Previous);
+            Vector2 nextTextSize = ScreenManager.RegularFont.MeasureString(Resources.Next);
+            int prevButtonWidth = (int)(prevTextSize.X + 40 * heightScale);
+            int nextButtonWidth = (int)(nextTextSize.X + 40 * heightScale);
+
+            prevButtonBounds = new Rectangle(safeArea.Left + (int)(100 * heightScale), navButtonY, prevButtonWidth, navButtonHeight);
+            nextButtonBounds = new Rectangle(safeArea.Right - nextButtonWidth - (int)(100 * heightScale), navButtonY, nextButtonWidth, navButtonHeight);
+
+            // Calculate back button bounds in top-left corner
+            int backButtonSize = (int)(50 * heightScale);
+            int backButtonPadding = (int)(20 * heightScale);
+            backButtonBounds = new Rectangle(
+                safeArea.Left + backButtonPadding,
+                safeArea.Top + backButtonPadding,
+                backButtonSize,
+                backButtonSize);
+        }
+
+        private string GetLanguageDisplay()
+        {
+            // Language names are already stored in native format
+            return settings.Language;
+        }
+
+        private void CycleToNextLanguage()
+        {
+            // Determine next language
+            string nextLanguage = settings.Language switch
+            {
+                "English" => "Français",
+                "Français" => "Español",
+                "Español" => "Italiano",
+                "Italiano" => "Русский",
+                "Русский" => "日本語",
+                "日本語" => "中文",
+                "中文" => "English",
+                _ => "English"
+            };
+
+            // Phase 1: Reload fonts BEFORE changing language
+            ScreenManager.ReloadFontForLanguage(nextLanguage);
+
+            // Phase 2: Set the language (this will change CurrentUICulture)
+            settings.Language = nextLanguage;
+
+            // Phase 3: Refresh all screens now that language and fonts are both updated
+            BuildSettingItems();
+            ScreenManager.RefreshScreensAfterLanguageChange();
+        }
+
+        private void CycleToPreviousLanguage()
+        {
+            // Determine previous language
+            string previousLanguage = settings.Language switch
+            {
+                "English" => "中文",
+                "中文" => "日本語",
+                "日本語" => "Русский",
+                "Русский" => "Italiano",
+                "Italiano" => "Español",
+                "Español" => "Français",
+                "Français" => "English",
+                _ => "中文" // Default to last active language
+            };
+
+            // Phase 1: Reload fonts BEFORE changing language
+            ScreenManager.ReloadFontForLanguage(previousLanguage);
+
+            // Phase 2: Set the language (this will change CurrentUICulture)
+            settings.Language = previousLanguage;
+
+            // Phase 3: Refresh all screens now that language and fonts are both updated
+            BuildSettingItems();
+            ScreenManager.RefreshScreensAfterLanguageChange();
+        }
+
+        private void CycleToNextCurrency()
+        {
+            settings.Currency = settings.Currency switch
+            {
+                "$" => "€",
+                "€" => "£",
+                "£" => "¥",
+                "¥" => "R",
+                "R" => "$",
+                _ => "$"
+            };
+            GameSettings.Save();
+        }
+
+        private void CycleToPreviousCurrency()
+        {
+            settings.Currency = settings.Currency switch
+            {
+                "$" => "R",
+                "R" => "¥",
+                "¥" => "£",
+                "£" => "€",
+                "€" => "$",
+                _ => "$"
+            };
+            GameSettings.Save();
+        }
+
+        private void AddHeader(string text, ref float yPos)
+        {
+            settingItems.Add(new SettingItem
+            {
+                Type = SettingType.Header,
+                Label = text,
+                Bounds = new Rectangle((int)leftMargin, (int)yPos, safeArea.Width - (int)leftMargin * 2, (int)(40 * (safeArea.Height / 720f)))
+            });
+            yPos += itemSpacing;
+        }
+
+        private void AddCounterSetting(string label, Func<int> getValue, int min, int max,
+            Action<int> setValue, ref float yPos)
+        {
+            settingItems.Add(new SettingItem
+            {
+                Type = SettingType.Counter,
+                Label = label,
+                GetValue = () => getValue().ToString(),
+                OnDecrease = () =>
+                {
+                    int val = getValue() - 1;
+                    if (val < min) val = max;
+                    setValue(val);
+                    GameSettings.Save();
+                },
+                OnIncrease = () =>
+                {
+                    int val = getValue() + 1;
+                    if (val > max) val = min;
+                    setValue(val);
+                    GameSettings.Save();
+                },
+                Bounds = new Rectangle((int)leftMargin, (int)yPos, safeArea.Width - (int)leftMargin * 2, (int)(40 * (safeArea.Height / 720f)))
+            });
+            yPos += itemSpacing;
+        }
+
+        private void AddSliderSetting(string label, Func<float> getValue, Action<float> setValue, ref float yPos)
+        {
+            settingItems.Add(new SettingItem
+            {
+                Type = SettingType.Slider,
+                Label = label,
+                GetValue = () => $"{(int)(getValue() * 100)}%",
+                OnDecrease = () =>
+                {
+                    float val = getValue() - 0.1f;
+                    if (val < 0) val = 1.0f;
+                    setValue(val);
+                    GameSettings.Save();
+                },
+                OnIncrease = () =>
+                {
+                    float val = getValue() + 0.1f;
+                    if (val > 1.0f) val = 0f;
+                    setValue(val);
+                    GameSettings.Save();
+                },
+                Bounds = new Rectangle((int)leftMargin, (int)yPos, safeArea.Width - (int)leftMargin * 2, (int)(40 * (safeArea.Height / 720f)))
+            });
+            yPos += itemSpacing;
+        }
+
+        private void AddCheckboxSetting(string label, Func<bool> getValue, Action<bool> setValue, ref float yPos)
+        {
+            settingItems.Add(new SettingItem
+            {
+                Type = SettingType.Checkbox,
+                Label = label,
+                GetValue = () => getValue() ? "[X]" : "[ ]",
+                OnClick = () =>
+                {
+                    setValue(!getValue());
+                    GameSettings.Save();
+                },
+                Bounds = new Rectangle((int)leftMargin, (int)yPos, safeArea.Width - (int)leftMargin * 2, (int)(40 * (safeArea.Height / 720f)))
+            });
+            yPos += itemSpacing;
+        }
+
+        private void AddCycleSetting(string label, Func<string> getValue, Action onPrevious, Action onNext,
+            Action onChange, ref float yPos)
+        {
+            settingItems.Add(new SettingItem
+            {
+                Type = SettingType.Cycle,
+                Label = label,
+                GetValue = getValue,
+                OnDecrease = () =>
+                {
+                    onPrevious();
+                    onChange?.Invoke();
+                    GameSettings.Save();
+                },
+                OnIncrease = () =>
+                {
+                    onNext();
+                    onChange?.Invoke();
+                    GameSettings.Save();
+                },
+                Bounds = new Rectangle((int)leftMargin, (int)yPos, safeArea.Width - (int)leftMargin * 2, (int)(40 * (safeArea.Height / 720f)))
+            });
+            yPos += itemSpacing;
+        }
+
+        public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
+        {
+            base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
+
+            if (IsActive && !coveredByOtherScreen)
+            {
+                HandleInput();
+            }
+        }
+
+        private MouseState previousMouseState;
+        private KeyboardState previousKeyboardState;
+
+        private void HandleInput()
+        {
+            KeyboardState keyboardState = Keyboard.GetState();
+            MouseState mouseState = Mouse.GetState();
+
+            // Check for escape/back
+            if ((keyboardState.IsKeyDown(Keys.Escape) && !previousKeyboardState.IsKeyDown(Keys.Escape)) ||
+                inputHelper.IsEscape)
+            {
+                ExitScreen();
+                previousKeyboardState = keyboardState;
+                return;
+            }
+
+            // Get mouse position
+            Point mousePos = new Point(mouseState.X, mouseState.Y);
+
+            // Track button press state
+            pressedButtonIndex = -1;
+            isNextButtonPressed = false;
+            isPrevButtonPressed = false;
+            isBackButtonPressed = false;
+
+            // Check for mouse click (left button just pressed)
+            if (mouseState.LeftButton == ButtonState.Pressed &&
+                previousMouseState.LeftButton == ButtonState.Released)
+            {
+                // Check back button
+                if (backButtonBounds.Contains(mousePos))
+                {
+                    isBackButtonPressed = true;
+                    AudioManager.PlaySound("menu_select");
+                    ExitScreen();
+                    previousMouseState = mouseState;
+                    previousKeyboardState = keyboardState;
+                    return;
+                }
+
+                // Check navigation buttons
+                if (currentPage < TotalPages - 1 && nextButtonBounds.Contains(mousePos))
+                {
+                    isNextButtonPressed = true;
+                    currentPage++;
+                    BuildSettingItems();
+                    AudioManager.PlaySound("menu_select");
+                    previousMouseState = mouseState;
+                    previousKeyboardState = keyboardState;
+                    return;
+                }
+                else if (currentPage > 0 && prevButtonBounds.Contains(mousePos))
+                {
+                    isPrevButtonPressed = true;
+                    currentPage--;
+                    BuildSettingItems();
+                    AudioManager.PlaySound("menu_select");
+                    previousMouseState = mouseState;
+                    previousKeyboardState = keyboardState;
+                    return;
+                }
+
+                for (int i = 0; i < settingItems.Count; i++)
+                {
+                    SettingItem item = settingItems[i];
+                    if (item.Type == SettingType.Header) continue;
+
+                    if (item.Bounds.Contains(mousePos))
+                    {
+                        // Button bounds
+                        Rectangle leftButton = GetLeftButtonBounds(item.Bounds);
+                        Rectangle rightButton = GetRightButtonBounds(item.Bounds);
+
+                        if (item.Type == SettingType.Checkbox)
+                        {
+                            // Get checkbox bounds (centered in value column)
+                            Rectangle checkboxBounds = GetCheckboxButtonBounds(item.Bounds);
+                            if (checkboxBounds.Contains(mousePos))
+                            {
+                                item.OnClick?.Invoke();
+                                AudioManager.PlaySound("menu_select");
+                            }
+                        }
+                        else if (leftButton.Contains(mousePos))
+                        {
+                            item.OnDecrease?.Invoke();
+                            AudioManager.PlaySound("menu_select");
+                            pressedButtonIndex = i * 2; // Even indices for left buttons
+                        }
+                        else if (rightButton.Contains(mousePos))
+                        {
+                            item.OnIncrease?.Invoke();
+                            AudioManager.PlaySound("menu_select");
+                            pressedButtonIndex = i * 2 + 1; // Odd indices for right buttons
+                        }
+                        break;
+                    }
+                }
+            }
+
+            // Track hover state for visual feedback
+            hoveredIndex = -1;
+            for (int i = 0; i < settingItems.Count; i++)
+            {
+                if (settingItems[i].Type != SettingType.Header &&
+                    settingItems[i].Bounds.Contains(mousePos))
+                {
+                    hoveredIndex = i;
+                    break;
+                }
+            }
+
+            previousMouseState = mouseState;
+            previousKeyboardState = keyboardState;
+        }
+
+        private Rectangle GetLeftButtonBounds(Rectangle itemBounds)
+        {
+            return new Rectangle(
+                itemBounds.Right - (int)(buttonSpacing + buttonSize + rightButtonMargin),
+                itemBounds.Y + (itemBounds.Height - (int)buttonSize) / 2,
+                (int)buttonSize, (int)buttonSize);
+        }
+
+        private Rectangle GetRightButtonBounds(Rectangle itemBounds)
+        {
+            return new Rectangle(
+                itemBounds.Right - (int)buttonSize - (int)rightButtonMargin,
+                itemBounds.Y + (itemBounds.Height - (int)buttonSize) / 2,
+                (int)buttonSize, (int)buttonSize);
+        }
+
+        private Rectangle GetCheckboxButtonBounds(Rectangle itemBounds)
+        {
+            // Center in the value column, matching the position where counter values appear
+            int leftButtonRight = itemBounds.Right - (int)(buttonSpacing + buttonSize + rightButtonMargin) + (int)buttonSize;
+            int rightButtonLeft = itemBounds.Right - (int)buttonSize - (int)rightButtonMargin;
+            int centerX = (leftButtonRight + rightButtonLeft) / 2 - (int)buttonSize / 2;
+
+            return new Rectangle(
+                centerX,
+                itemBounds.Y + (itemBounds.Height - (int)buttonSize) / 2,
+                (int)buttonSize, (int)buttonSize);
+        }
+
+        public override void Draw(GameTime gameTime)
+        {
+            SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
+
+            spriteBatch.Begin();
+
+            // Draw background
+            spriteBatch.Draw(background, safeArea, Color.White * TransitionAlpha);
+
+            // Draw title
+            string title = Resources.SettingsTitle;
+            Vector2 titleSize = ScreenManager.Font.MeasureString(title);
+            Vector2 titlePos = new Vector2(safeArea.Center.X - titleSize.X / 2, safeArea.Top + 10);
+            spriteBatch.DrawString(ScreenManager.Font, title, titlePos, Color.White * TransitionAlpha);
+
+            // Draw card preview
+            Rectangle cardRect = new Rectangle((int)cardPreviewPosition.X, (int)cardPreviewPosition.Y,
+                (int)cardSize.X, (int)cardSize.Y);
+            spriteBatch.Draw(cardBackTexture, cardRect, Color.White * TransitionAlpha);
+
+            // Draw all setting items
+            for (int i = 0; i < settingItems.Count; i++)
+            {
+                SettingItem item = settingItems[i];
+                Color color = Color.White * TransitionAlpha;
+
+                if (item.Type == SettingType.Header)
+                {
+                    // Draw header with larger font
+                    Vector2 headerPos = new Vector2(item.Bounds.X, item.Bounds.Y);
+                    spriteBatch.DrawString(ScreenManager.Font, item.Label, headerPos,
+                        Color.Gold * TransitionAlpha);
+                }
+                else
+                {
+                    // Highlight if hovered
+                    if (i == hoveredIndex)
+                    {
+                        spriteBatch.Draw(background, item.Bounds, Color.White * 0.2f * TransitionAlpha);
+                    }
+
+                    // Draw label
+                    Vector2 labelPos = new Vector2(item.Bounds.X + 10,
+                        item.Bounds.Y + (item.Bounds.Height - ScreenManager.RegularFont.LineSpacing) / 2);
+                    spriteBatch.DrawString(ScreenManager.RegularFont, item.Label, labelPos, color);
+
+                    // Draw value and controls
+                    string valueText = item.GetValue?.Invoke() ?? "";
+                    Vector2 valueSize = ScreenManager.RegularFont.MeasureString(valueText);
+
+                    if (item.Type == SettingType.Checkbox)
+                    {
+                        // Draw checkbox as a button in the center of the value column
+                        bool isChecked = item.GetValue?.Invoke() == "[X]";
+                        Rectangle checkboxBounds = GetCheckboxButtonBounds(item.Bounds);
+
+                        Texture2D checkboxTexture = isChecked ? buttonPressedTexture : buttonRegularTexture;
+                        spriteBatch.Draw(checkboxTexture, checkboxBounds, Color.White * TransitionAlpha);
+
+                        // Draw checkmark if checked
+                        if (isChecked)
+                        {
+                            Vector2 checkSize = ScreenManager.RegularFont.MeasureString("X");
+                            Vector2 checkPos = new Vector2(
+                                checkboxBounds.X + (checkboxBounds.Width - checkSize.X) / 2,
+                                checkboxBounds.Y + (checkboxBounds.Height - checkSize.Y) / 2);
+                            spriteBatch.DrawString(ScreenManager.RegularFont, "X", checkPos, Color.Black * TransitionAlpha);
+                        }
+                    }
+                    else
+                    {
+                        // Get button bounds
+                        Rectangle leftButton = GetLeftButtonBounds(item.Bounds);
+                        Rectangle rightButton = GetRightButtonBounds(item.Bounds);
+
+                        // Draw left button [-]
+                        bool leftPressed = (pressedButtonIndex == i * 2);
+                        Texture2D leftTexture = leftPressed ? buttonPressedTexture : buttonRegularTexture;
+                        spriteBatch.Draw(leftTexture, leftButton, Color.White * TransitionAlpha);
+                        Vector2 minusSize = ScreenManager.RegularFont.MeasureString("-");
+                        Vector2 minusPos = new Vector2(
+                            leftButton.X + (leftButton.Width - minusSize.X) / 2,
+                            leftButton.Y + (leftButton.Height - minusSize.Y) / 2);
+                        spriteBatch.DrawString(ScreenManager.RegularFont, "-", minusPos, Color.Black * TransitionAlpha);
+
+                        // Draw value in center
+                        Vector2 valuePos = new Vector2(
+                            leftButton.Right + (rightButton.Left - leftButton.Right - valueSize.X) / 2,
+                            item.Bounds.Y + (item.Bounds.Height - ScreenManager.RegularFont.LineSpacing) / 2);
+                        spriteBatch.DrawString(ScreenManager.RegularFont, valueText, valuePos, color);
+
+                        // Draw right button [+]
+                        bool rightPressed = (pressedButtonIndex == i * 2 + 1);
+                        Texture2D rightTexture = rightPressed ? buttonPressedTexture : buttonRegularTexture;
+                        spriteBatch.Draw(rightTexture, rightButton, Color.White * TransitionAlpha);
+                        Vector2 plusSize = ScreenManager.RegularFont.MeasureString("+");
+                        Vector2 plusPos = new Vector2(
+                            rightButton.X + (rightButton.Width - plusSize.X) / 2,
+                            rightButton.Y + (rightButton.Height - plusSize.Y) / 2);
+                        spriteBatch.DrawString(ScreenManager.RegularFont, "+", plusPos, Color.Black * TransitionAlpha);
+                    }
+                }
+            }
+
+            // Draw back button
+            Texture2D backTexture = isBackButtonPressed ? buttonPressedTexture : buttonRegularTexture;
+            spriteBatch.Draw(backTexture, backButtonBounds, Color.Red * TransitionAlpha);
+            string backText = "X";
+            Vector2 backTextSize = ScreenManager.Font.MeasureString(backText);
+            Vector2 backTextPos = new Vector2(
+                backButtonBounds.X + (backButtonBounds.Width - backTextSize.X) / 2,
+                backButtonBounds.Y + (backButtonBounds.Height - backTextSize.Y) / 2);
+            spriteBatch.DrawString(ScreenManager.Font, backText, backTextPos, Color.White * TransitionAlpha);
+
+            // Draw page navigation buttons
+            if (currentPage > 0)
+            {
+                Texture2D prevTexture = isPrevButtonPressed ? buttonPressedTexture : buttonRegularTexture;
+                spriteBatch.Draw(prevTexture, prevButtonBounds, Color.White * TransitionAlpha);
+                string prevText = Resources.Previous;
+                Vector2 prevTextSize = ScreenManager.RegularFont.MeasureString(prevText);
+                Vector2 prevTextPos = new Vector2(
+                    prevButtonBounds.X + (prevButtonBounds.Width - prevTextSize.X) / 2,
+                    prevButtonBounds.Y + (prevButtonBounds.Height - prevTextSize.Y) / 2);
+                spriteBatch.DrawString(ScreenManager.RegularFont, prevText, prevTextPos, Color.Black * TransitionAlpha);
+            }
+
+            if (currentPage < TotalPages - 1)
+            {
+                Texture2D nextTexture = isNextButtonPressed ? buttonPressedTexture : buttonRegularTexture;
+                spriteBatch.Draw(nextTexture, nextButtonBounds, Color.White * TransitionAlpha);
+                string nextText = Resources.Next;
+                Vector2 nextTextSize = ScreenManager.RegularFont.MeasureString(nextText);
+                Vector2 nextTextPos = new Vector2(
+                    nextButtonBounds.X + (nextButtonBounds.Width - nextTextSize.X) / 2,
+                    nextButtonBounds.Y + (nextButtonBounds.Height - nextTextSize.Y) / 2);
+                spriteBatch.DrawString(ScreenManager.RegularFont, nextText, nextTextPos, Color.Black * TransitionAlpha);
+            }
+
+            // Draw page indicator
+            string pageText = string.Format(Resources.PageIndicator, currentPage + 1, TotalPages);
+            Vector2 pageTextSize = ScreenManager.RegularFont.MeasureString(pageText);
+            Vector2 pageTextPos = new Vector2(
+                safeArea.Center.X - pageTextSize.X / 2,
+                safeArea.Bottom - 55);
+            spriteBatch.DrawString(ScreenManager.RegularFont, pageText, pageTextPos, Color.White * TransitionAlpha);
+
+            spriteBatch.End();
+
+            base.Draw(gameTime);
+        }
+    }
+
+    enum SettingType
+    {
+        Header,
+        Counter,
+        Slider,
+        Cycle,
+        Checkbox
+    }
+
+    class SettingItem
+    {
+        public SettingType Type { get; set; }
+        public string Label { get; set; }
+        public Func<string> GetValue { get; set; }
+        public Action OnClick { get; set; }
+        public Action OnIncrease { get; set; }
+        public Action OnDecrease { get; set; }
+        public Rectangle Bounds { get; set; }
+    }
+}

+ 20 - 1
CardsStarterKit/Framework/Cards/CardPacket.cs

@@ -218,6 +218,25 @@ namespace CardsFramework
             cards = shuffledDeck;
         }
 
+        /// <summary>
+        /// Shuffles the cards in the packet using a specified seed for deterministic shuffling.
+        /// </summary>
+        /// <param name="seed">The seed for the random number generator.</param>
+        public void Shuffle(int seed)
+        {
+            Random random = new Random(seed);
+            List<TraditionalCard> shuffledDeck = new List<TraditionalCard>();
+
+            while (cards.Count > 0)
+            {
+                TraditionalCard card = cards[random.Next(0, cards.Count)];
+                cards.Remove(card);
+                shuffledDeck.Add(card);
+            }
+
+            cards = shuffledDeck;
+        }
+
         /// <summary>
         /// Removes the specified card from the packet. The first matching card
         /// will be removed.
@@ -289,4 +308,4 @@ namespace CardsFramework
             return dealtCards;
         }
     }
-}
+}

+ 9 - 1
CardsStarterKit/Framework/Cards/TraditionalCard.cs

@@ -67,9 +67,17 @@ namespace CardsFramework
     /// </remarks>
     public class TraditionalCard
     {
+        /// <summary>
+        /// Factory method for deserialization, creates a card with no holding collection.
+        /// </summary>
+        public static TraditionalCard Create(CardSuit type, CardValue value)
+        {
+            return new TraditionalCard(type, value, null);
+        }
+
         public CardSuit Type { get; set; }
         public CardValue Value { get; set; }
-        public CardPacket HoldingCardCollection; 
+        public CardPacket HoldingCardCollection;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="TraditionalCard"/> class.

+ 114 - 0
CardsStarterKit/Framework/UI/DeckDisplayComponent.cs

@@ -0,0 +1,114 @@
+//-----------------------------------------------------------------------------
+// DeckDisplayComponent.cs
+//
+// Displays a visual representation of the card deck during gameplay
+//-----------------------------------------------------------------------------
+
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace CardsFramework
+{
+    /// <summary>
+    /// Component that displays a visual representation of the deck at the dealer position.
+    /// Shows a stack of card backs to indicate where cards are being dealt from.
+    /// </summary>
+    public class DeckDisplayComponent : DrawableGameComponent
+    {
+        private readonly CardsGame cardGame;
+        private readonly SpriteBatch spriteBatch;
+        private readonly Matrix globalTransformation;
+        private readonly Vector2 position;
+        private Texture2D cardBackTexture;
+
+        /// <summary>
+        /// Number of card back layers to draw for the stacked effect
+        /// </summary>
+        public int StackLayers { get; set; } = 3;
+
+        /// <summary>
+        /// Offset between each layer to create depth
+        /// </summary>
+        public Vector2 LayerOffset { get; set; } = new Vector2(1, 1);
+
+        /// <summary>
+        /// Rotation angle in radians (default -45 degrees / clockwise for casino look)
+        /// </summary>
+        public float Rotation { get; set; } = MathHelper.ToRadians(-45);
+
+        /// <summary>
+        /// Whether the deck should be visible
+        /// </summary>
+        public new bool Visible { get; set; } = true;
+
+        /// <summary>
+        /// Creates a new deck display component
+        /// </summary>
+        /// <param name="game">The game instance</param>
+        /// <param name="cardGame">The card game instance</param>
+        /// <param name="position">Position where the deck should be displayed</param>
+        /// <param name="spriteBatch">Shared sprite batch for rendering</param>
+        /// <param name="globalTransformation">Transformation matrix for scaling</param>
+        public DeckDisplayComponent(
+            Game game,
+            CardsGame cardGame,
+            Vector2 position,
+            SpriteBatch spriteBatch,
+            Matrix globalTransformation)
+            : base(game)
+        {
+            this.cardGame = cardGame;
+            this.position = position;
+            this.spriteBatch = spriteBatch;
+            this.globalTransformation = globalTransformation;
+            
+            DrawOrder = -5000; // Draw behind dealt cards but in front of table
+        }
+
+        /// <summary>
+        /// Load the card back texture
+        /// </summary>
+        protected override void LoadContent()
+        {
+            base.LoadContent();
+            
+            // Get the card back texture from the card game's assets
+            if (cardGame.cardsAssets.ContainsKey("CardBack_" + cardGame.Theme))
+            {
+                cardBackTexture = cardGame.cardsAssets["CardBack_" + cardGame.Theme];
+            }
+        }
+
+        /// <summary>
+        /// Draw the deck as a stack of card backs
+        /// </summary>
+        public override void Draw(GameTime gameTime)
+        {
+            if (!Visible || cardBackTexture == null)
+                return;
+
+            spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, globalTransformation);
+
+            // Calculate origin point for rotation (center of the card)
+            Vector2 origin = new Vector2(cardBackTexture.Width / 2f, cardBackTexture.Height / 2f);
+
+            // Draw multiple layers to create a stacked deck effect
+            for (int i = 0; i < StackLayers; i++)
+            {
+                Vector2 layerPosition = position + (LayerOffset * i);
+                
+                // Draw with slight transparency on lower layers for depth
+                float alpha = 1.0f - (i * 0.1f);
+                Color color = Color.White * alpha;
+                
+                // Draw with rotation around the center origin
+                spriteBatch.Draw(cardBackTexture, layerPosition, null, color, Rotation, origin, 1.0f, SpriteEffects.None, 0f);
+            }
+
+            spriteBatch.End();
+
+            base.Draw(gameTime);
+        }
+    }
+}

+ 9 - 0
CardsStarterKit/Framework/UI/GameTable.cs

@@ -30,6 +30,15 @@ namespace CardsFramework
         public Rectangle TableBounds { get; private set; }
         public int Places { get; private set; }
 
+        /// <summary>
+        /// Updates the number of places/spots to display on the table.
+        /// </summary>
+        /// <param name="places">The new number of places.</param>
+        public void SetPlaces(int places)
+        {
+            Places = places;
+        }
+
         /// <summary>
         /// Returns the player position on the table according to the player index.
         /// </summary>

+ 139 - 0
CardsStarterKit/Framework/UI/RiffleShuffleAnimation.cs

@@ -0,0 +1,139 @@
+//-----------------------------------------------------------------------------
+// RiffleShuffleAnimation.cs
+//
+// Implements a classic riffle shuffle animation where the deck is split
+// in half and cards cascade together in an interleaving pattern
+//-----------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace CardsFramework
+{
+    /// <summary>
+    /// Riffle shuffle: splits deck into two halves, then interleaves them
+    /// with a cascading visual effect. This is the classic casino shuffle.
+    /// </summary>
+    public class RiffleShuffleAnimation : ShuffleAnimation
+    {
+        /// <summary>
+        /// How far apart the two halves split (in pixels)
+        /// </summary>
+        public float SplitDistance { get; set; } = 120f;
+
+        /// <summary>
+        /// Maximum rotation angle for cards during cascade (in radians)
+        /// </summary>
+        public float MaxRotation { get; set; } = 0.15f;
+
+        /// <summary>
+        /// Height of the cascade arc
+        /// </summary>
+        public float CascadeHeight { get; set; } = 60f;
+
+        /// <summary>
+        /// Number of times to repeat the shuffle
+        /// </summary>
+        public int ShuffleCycles { get; set; } = 2;
+
+        /// <summary>
+        /// Creates a new riffle shuffle animation
+        /// </summary>
+        public RiffleShuffleAnimation(CardsGame cardGame, Vector2 position, TimeSpan duration, Vector2 cardSize)
+            : base(cardGame, position, duration, cardSize)
+        {
+        }
+
+        /// <summary>
+        /// Creates the animated cards with riffle shuffle animations
+        /// </summary>
+        public override List<AnimatedCardsGameComponent> CreateAnimatedCards(
+            List<TraditionalCard> deck,
+            SpriteBatch spriteBatch,
+            Matrix globalTransformation)
+        {
+            var animatedCards = new List<AnimatedCardsGameComponent>();
+            
+            // Only show a subset of cards for a clear visual effect (every 3rd card)
+            int cardsToShow = Math.Min(18, deck.Count / 3); // Show ~18 cards max
+            int step = deck.Count / cardsToShow;
+            
+            // Split into two halves
+            int halfPoint = cardsToShow / 2;
+            
+            // Time calculations for animation phases
+            TimeSpan splitDuration = TimeSpan.FromMilliseconds(Duration.TotalMilliseconds * 0.2);
+            TimeSpan cascadeDuration = TimeSpan.FromMilliseconds(Duration.TotalMilliseconds * 0.6);
+            TimeSpan gatherDuration = TimeSpan.FromMilliseconds(Duration.TotalMilliseconds * 0.2);
+
+            for (int i = 0; i < cardsToShow; i++)
+            {
+                int deckIndex = i * step;
+                if (deckIndex >= deck.Count) break;
+                
+                // Determine which half this card belongs to
+                bool isLeftHalf = i < halfPoint;
+                int cardIndexInHalf = isLeftHalf ? i : (i - halfPoint);
+                int totalInHalf = halfPoint;
+                
+                // Create card component with slight vertical offset for depth
+                float depthOffset = i * 0.5f;
+                var cardComponent = CreateCardComponent(deck[deckIndex], spriteBatch, globalTransformation, 
+                    Position + new Vector2(0, depthOffset), true);
+                animatedCards.Add(cardComponent);
+
+                // Phase 1: Split the deck into two piles
+                Vector2 splitPosition = Position + new Vector2(
+                    isLeftHalf ? -SplitDistance : SplitDistance,
+                    depthOffset);
+                
+                AddTransition(
+                    cardComponent,
+                    splitPosition,
+                    TimeSpan.Zero,
+                    splitDuration);
+
+                // Phase 2: Cascade/riffle together with visible arc
+                // Stagger the cascade timing so cards drop one by one
+                double cascadeProgress = (double)cardIndexInHalf / totalInHalf;
+                TimeSpan cascadeDelay = splitDuration + 
+                    TimeSpan.FromMilliseconds(cascadeDuration.TotalMilliseconds * cascadeProgress * 0.7);
+                
+                // Create a high arc path for visibility
+                Vector2 midPoint = Position + new Vector2(
+                    isLeftHalf ? -SplitDistance / 2 : SplitDistance / 2,
+                    -CascadeHeight);
+                
+                Vector2 finalPosition = Position + new Vector2(
+                    (isLeftHalf ? -10 : 10) + Random.Next(-8, 8), // Slight spread
+                    depthOffset + Random.Next(-3, 3));
+
+                // Arc up
+                AddTransition(
+                    cardComponent,
+                    midPoint,
+                    cascadeDelay,
+                    TimeSpan.FromMilliseconds(cascadeDuration.TotalMilliseconds * 0.2));
+                
+                // Arc down to center
+                AddTransition(
+                    cardComponent,
+                    finalPosition,
+                    cascadeDelay + TimeSpan.FromMilliseconds(cascadeDuration.TotalMilliseconds * 0.2),
+                    TimeSpan.FromMilliseconds(cascadeDuration.TotalMilliseconds * 0.3));
+
+                // Phase 3: Gather into neat pile
+                TimeSpan gatherDelay = splitDuration + cascadeDuration;
+                AddTransition(
+                    cardComponent,
+                    Position,
+                    gatherDelay,
+                    gatherDuration);
+            }
+
+            return animatedCards;
+        }
+    }
+}

+ 138 - 0
CardsStarterKit/Framework/UI/ShuffleAnimation.cs

@@ -0,0 +1,138 @@
+//-----------------------------------------------------------------------------
+// ShuffleAnimation.cs
+//
+// Abstract base class for card shuffle animations
+//-----------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace CardsFramework
+{
+    /// <summary>
+    /// Abstract base class for shuffle animations. Defines the interface for 
+    /// creating different types of card shuffles (riffle, overhand, Hindu, etc.)
+    /// </summary>
+    public abstract class ShuffleAnimation
+    {
+        /// <summary>
+        /// The position where the shuffle animation takes place (typically dealer position)
+        /// </summary>
+        public Vector2 Position { get; set; }
+
+        /// <summary>
+        /// Total duration of the shuffle animation
+        /// </summary>
+        public TimeSpan Duration { get; set; }
+
+        /// <summary>
+        /// The card game this animation belongs to
+        /// </summary>
+        protected CardsGame CardGame { get; private set; }
+
+        /// <summary>
+        /// Random number generator for animation variations
+        /// </summary>
+        protected Random Random { get; private set; }
+
+        /// <summary>
+        /// Size of a single card
+        /// </summary>
+        protected Vector2 CardSize { get; private set; }
+
+        /// <summary>
+        /// Callback to invoke when the animation starts
+        /// </summary>
+        public Action OnAnimationStart { get; set; }
+
+        /// <summary>
+        /// Callback to invoke when the animation completes
+        /// </summary>
+        public Action OnAnimationComplete { get; set; }
+
+        /// <summary>
+        /// Creates a new shuffle animation
+        /// </summary>
+        /// <param name="cardGame">The card game instance</param>
+        /// <param name="position">Position where shuffle occurs</param>
+        /// <param name="duration">How long the animation should last</param>
+        /// <param name="cardSize">Size of each card</param>
+        protected ShuffleAnimation(CardsGame cardGame, Vector2 position, TimeSpan duration, Vector2 cardSize)
+        {
+            CardGame = cardGame;
+            Position = position;
+            Duration = duration;
+            CardSize = cardSize;
+            Random = new Random();
+        }
+
+        /// <summary>
+        /// Creates the animated card components for this shuffle animation.
+        /// Each shuffle type implements this differently.
+        /// </summary>
+        /// <param name="deck">The list of cards to shuffle</param>
+        /// <param name="spriteBatch">Shared sprite batch for rendering</param>
+        /// <param name="globalTransformation">Transformation matrix for scaling</param>
+        /// <returns>List of animated card components with animations applied</returns>
+        public abstract List<AnimatedCardsGameComponent> CreateAnimatedCards(
+            List<TraditionalCard> deck, 
+            SpriteBatch spriteBatch, 
+            Matrix globalTransformation);
+
+        /// <summary>
+        /// Helper method to create a standard card component
+        /// </summary>
+        protected AnimatedCardsGameComponent CreateCardComponent(
+            TraditionalCard card, 
+            SpriteBatch spriteBatch, 
+            Matrix globalTransformation,
+            Vector2 startPosition,
+            bool faceDown = true)
+        {
+            var cardComponent = new AnimatedCardsGameComponent(card, CardGame, spriteBatch, globalTransformation)
+            {
+                CurrentPosition = startPosition,
+                IsFaceDown = faceDown,
+                Visible = true
+            };
+            return cardComponent;
+        }
+
+        /// <summary>
+        /// Helper to add a transition animation to a card
+        /// </summary>
+        protected void AddTransition(
+            AnimatedCardsGameComponent card,
+            Vector2 destination,
+            TimeSpan startDelay,
+            TimeSpan duration)
+        {
+            var transition = new TransitionGameComponentAnimation(card.CurrentPosition, destination)
+            {
+                StartTime = DateTime.Now + startDelay,
+                Duration = duration
+            };
+            card.AddAnimation(transition);
+        }
+
+        /// <summary>
+        /// Helper to add a scale animation to a card
+        /// </summary>
+        protected void AddScale(
+            AnimatedCardsGameComponent card,
+            float startScale,
+            float endScale,
+            TimeSpan startDelay,
+            TimeSpan duration)
+        {
+            var scale = new ScaleGameComponentAnimation(startScale, endScale)
+            {
+                StartTime = DateTime.Now + startDelay,
+                Duration = duration
+            };
+            card.AddAnimation(scale);
+        }
+    }
+}

+ 165 - 0
CardsStarterKit/Framework/UI/ShuffleAnimationComponent.cs

@@ -0,0 +1,165 @@
+//-----------------------------------------------------------------------------
+// ShuffleAnimationComponent.cs
+//
+// Game component that manages a shuffle animation, creates and coordinates
+// all animated card components, and handles cleanup
+//-----------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace CardsFramework
+{
+    /// <summary>
+    /// Component that orchestrates a shuffle animation by creating animated
+    /// cards and managing their lifecycle
+    /// </summary>
+    public class ShuffleAnimationComponent : DrawableGameComponent
+    {
+        private readonly ShuffleAnimation shuffleAnimation;
+        private readonly List<TraditionalCard> deck;
+        private readonly SpriteBatch spriteBatch;
+        private readonly Matrix globalTransformation;
+        private List<AnimatedCardsGameComponent> animatedCards;
+        private TimeSpan elapsedTime;
+        private bool animationStarted = false;
+        private bool animationCompleted = false;
+
+        /// <summary>
+        /// Gets whether the shuffle animation has completed
+        /// </summary>
+        public bool IsComplete => animationCompleted;
+
+        /// <summary>
+        /// Creates a new shuffle animation component
+        /// </summary>
+        /// <param name="game">The game instance</param>
+        /// <param name="shuffleAnimation">The shuffle animation to perform</param>
+        /// <param name="deck">The deck of cards to shuffle</param>
+        /// <param name="spriteBatch">Shared sprite batch for rendering</param>
+        /// <param name="globalTransformation">Transformation matrix for scaling</param>
+        public ShuffleAnimationComponent(
+            Game game,
+            ShuffleAnimation shuffleAnimation,
+            List<TraditionalCard> deck,
+            SpriteBatch spriteBatch,
+            Matrix globalTransformation)
+            : base(game)
+        {
+            this.shuffleAnimation = shuffleAnimation;
+            this.deck = deck;
+            this.spriteBatch = spriteBatch;
+            this.globalTransformation = globalTransformation;
+            
+            // Component should not be visible initially
+            Visible = false;
+        }
+
+        /// <summary>
+        /// Initialize and start the animation
+        /// </summary>
+        public override void Initialize()
+        {
+            base.Initialize();
+            
+            // Create all animated cards
+            animatedCards = shuffleAnimation.CreateAnimatedCards(deck, spriteBatch, globalTransformation);
+            
+            // Add all card components to the game
+            foreach (var card in animatedCards)
+            {
+                Game.Components.Add(card);
+            }
+            
+            // Make component visible and trigger start callback
+            Visible = true;
+            animationStarted = true;
+            shuffleAnimation.OnAnimationStart?.Invoke();
+        }
+
+        /// <summary>
+        /// Update animation progress and check for completion
+        /// </summary>
+        public override void Update(GameTime gameTime)
+        {
+            base.Update(gameTime);
+
+            if (!animationStarted)
+                return;
+
+            elapsedTime += gameTime.ElapsedGameTime;
+
+            // Check if animation duration has elapsed
+            if (elapsedTime >= shuffleAnimation.Duration && !animationCompleted)
+            {
+                CompleteAnimation();
+            }
+        }
+
+        /// <summary>
+        /// Called when animation finishes
+        /// </summary>
+        private void CompleteAnimation()
+        {
+            animationCompleted = true;
+            
+            // CRITICAL: Hide cards immediately before doing anything else
+            // This prevents the "ghost deck" from appearing at shuffle position
+            if (animatedCards != null)
+            {
+                foreach (var card in animatedCards)
+                {
+                    card.Visible = false;
+                }
+            }
+            
+            // Invoke completion callback
+            shuffleAnimation.OnAnimationComplete?.Invoke();
+            
+            // Clean up all card components
+            CleanupCards();
+            
+            // Remove this component from the game
+            Game.Components.Remove(this);
+        }
+
+        /// <summary>
+        /// Removes all animated card components from the game
+        /// </summary>
+        private void CleanupCards()
+        {
+            if (animatedCards != null)
+            {
+                // First make all cards invisible
+                foreach (var card in animatedCards)
+                {
+                    card.Visible = false;
+                }
+
+                // Then remove them from the game
+                foreach (var card in animatedCards)
+                {
+                    if (Game.Components.Contains(card))
+                    {
+                        Game.Components.Remove(card);
+                    }
+                }
+                animatedCards.Clear();
+            }
+        }
+
+        /// <summary>
+        /// Dispose and cleanup
+        /// </summary>
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                CleanupCards();
+            }
+            base.Dispose(disposing);
+        }
+    }
+}

+ 18 - 9
CardsStarterKit/Framework/UI/TransitionGameComponentAnimation.cs

@@ -20,20 +20,20 @@ namespace CardsFramework
         Vector2 sourcePosition;
         Vector2 positionDelta;
         float percent = 0;
-        Vector2 destinationPosition; 
+        Vector2 destinationPosition;
 
         /// <summary>
         /// Initializes a new instance of the class.
         /// </summary>
         /// <param name="sourcePosition">The source position.</param>
         /// <param name="destinationPosition">The destination position.</param>
-        public TransitionGameComponentAnimation(Vector2 sourcePosition, 
+        public TransitionGameComponentAnimation(Vector2 sourcePosition,
             Vector2 destinationPosition)
         {
             this.destinationPosition = destinationPosition;
             this.sourcePosition = sourcePosition;
             positionDelta = destinationPosition - sourcePosition;
-        } 
+        }
 
         /// <summary>
         /// Runs the transition animation.
@@ -44,18 +44,27 @@ namespace CardsFramework
             if (IsStarted())
             {
                 // Calculate the animation's completion percentage.
-                percent += (float)(gameTime.ElapsedGameTime.TotalSeconds / 
-                    Duration.TotalSeconds);
+                percent += (float)(gameTime.ElapsedGameTime.TotalSeconds / Duration.TotalSeconds);
+                percent = MathHelper.Clamp(percent, 0f, 1f);
 
-                // Move the component towards the destination as the animation
-                // progresses
-                Component.CurrentPosition = sourcePosition + positionDelta * percent;
+                // Apply cubic ease-out for smooth sliding
+                float easedPercent = EaseOutCubic(percent);
+                Component.CurrentPosition = sourcePosition + positionDelta * easedPercent;
 
                 if (IsDone())
                 {
                     Component.CurrentPosition = destinationPosition;
                 }
             }
+
+        }
+
+        /// <summary>
+        /// Cubic ease-out function for smooth animation.
+        /// </summary>
+        private float EaseOutCubic(float t)
+        {
+            return 1f - (float)Math.Pow(1f - t, 2);
         }
     }
-}
+}

+ 42 - 2
CardsStarterKit/Framework/Utils/UIUtility.cs

@@ -22,7 +22,7 @@ namespace CardsFramework
         /// Indicates if the game is running on a desktop platform.
         /// </summary>
         public readonly static bool IsDesktop = OperatingSystem.IsMacOS() || OperatingSystem.IsLinux() || OperatingSystem.IsWindows();
-        
+
         /// <summary>
         /// Gets the name of a card asset.
         /// </summary>
@@ -37,5 +37,45 @@ namespace CardsFramework
                 CardValue.SecondJoker) ?
                     "" : card.Type.ToString(), card.Value);
         }
+
+        /// <summary>
+        /// Strips the GUID suffix from player names for display purposes.
+        /// Network player names are in format "PlayerName_12345678" for unique identification,
+        /// but we only want to show "PlayerName" to the user.
+        /// </summary>
+        /// <param name="name">The full player name with potential GUID suffix</param>
+        /// <returns>The display name without GUID suffix</returns>
+        public static string StripGuidSuffix(string name)
+        {
+            if (string.IsNullOrEmpty(name))
+                return name;
+
+            // Check if name contains underscore followed by 8 hex characters (GUID prefix)
+            int underscoreIndex = name.LastIndexOf('_');
+            if (underscoreIndex > 0 && underscoreIndex < name.Length - 1)
+            {
+                string suffix = name.Substring(underscoreIndex + 1);
+                // Verify it looks like a GUID prefix (8 hex characters)
+                if (suffix.Length >= 8 && IsHexString(suffix.Substring(0, 8)))
+                {
+                    return name.Substring(0, underscoreIndex);
+                }
+            }
+
+            return name;
+        }
+
+        /// <summary>
+        /// Checks if a string contains only hexadecimal characters
+        /// </summary>
+        private static bool IsHexString(string str)
+        {
+            foreach (char c in str)
+            {
+                if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
+                    return false;
+            }
+            return true;
+        }
     }
-}
+}

+ 1 - 0
CardsStarterKit/Platforms/Android/AndroidManifest.xml

@@ -8,6 +8,7 @@
   
   <uses-permission android:name="android.permission.INTERNET" />
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+  <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
   
   <application android:allowBackup="true" 

+ 1 - 1
CardsStarterKit/Platforms/Android/BlackJack.Android.csproj

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <TargetFramework>net8.0-android</TargetFramework>
+    <TargetFramework>net9.0-android</TargetFramework>
     <OutputType>Exe</OutputType>
     <RootNamespace>Blackjack.Android</RootNamespace>
     <AssemblyName>Blackjack</AssemblyName>

+ 1 - 1
CardsStarterKit/Platforms/iOS/BlackJack.iOS.csproj

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <TargetFramework>net8.0-ios</TargetFramework>
+    <TargetFramework>net9.0-ios</TargetFramework>
     <OutputType>Exe</OutputType>
     <RootNamespace>Blackjack.iOS</RootNamespace>
     <AssemblyName>Blackjack</AssemblyName>

+ 2 - 0
CardsStarterKit/Platforms/iOS/Info.plist

@@ -25,5 +25,7 @@
     <true/>
     <key>XSAppIconAssets</key>
 	<string>AppIcon.xcassets/AppIcon.appiconset</string>
+    <key>NSLocalNetworkUsageDescription</key>
+    <string>This app requires access to the local network to find and join multiplayer games.</string>
 </dict>
 </plist>

+ 159 - 0
CardsStarterKit/Tools/CJK_TRANSLATION_SUMMARY.md

@@ -0,0 +1,159 @@
+# CJK Translation and Character Extraction Summary
+
+## What Was Created
+
+### 1. Translation Files
+
+**Japanese (Resources.ja.resx)**
+- Complete translation of all 73 UI strings
+- Uses natural Japanese (mix of Kanji, Hiragana, and Katakana)
+- Cultural adaptations (e.g., "ライブ" for "LIVE")
+
+**Chinese Simplified (Resources.zh.resx)**
+- Complete translation of all 73 UI strings
+- Uses Simplified Chinese characters
+- Appropriate for mainland China users
+
+### 2. Character Analysis Results
+
+**Japanese Statistics:**
+- Total unique characters: 199
+- CJK characters (Kanji/Hiragana/Katakana): 171
+- ASCII/Latin characters: 28
+
+**Chinese Statistics:**
+- Total unique characters: 194
+- CJK characters (Hanzi): 174
+- ASCII/Latin characters: 20
+
+**Combined (for unified CJK font):**
+- **Total unique CJK characters: 306**
+- This is DRAMATICALLY smaller than a full CJK font (which would be 20,000+ characters!)
+
+### 3. Character Extraction Tool
+
+**File:** `Tools/ExtractCJKCharacters.py`
+
+**What it does:**
+1. Parses .resx XML files to extract all translated text
+2. Identifies CJK characters (Hiragana, Katakana, Kanji/Hanzi, CJK punctuation)
+3. Generates optimized character ranges for MonoGame .spritefont files
+4. Creates both individual (Japanese/Chinese) and combined character sets
+
+**Generated files:**
+- `japanese_characters.txt` - Raw list of 171 characters needed for Japanese
+- `chinese_characters.txt` - Raw list of 174 characters needed for Chinese
+- `cjk_characters.txt` - Combined list of 306 unique characters
+- `japanese_character_regions.xml` - XML ranges for Japanese .spritefont
+- `chinese_character_regions.xml` - XML ranges for Chinese .spritefont
+- `cjk_character_regions.xml` - **XML ranges for unified CJK .spritefont** (RECOMMENDED)
+
+## Next Steps to Implement CJK Font Support
+
+### Option 1: Unified CJK Font (Recommended)
+
+Create a single font that supports both Japanese and Chinese:
+
+1. **Find a CJK font** that includes both scripts:
+   - Noto Sans CJK (free, high quality)
+   - Source Han Sans (Adobe, free)
+   - Arial Unicode MS (if available on target platforms)
+
+2. **Create Regular_CJK.spritefont** in `Core/Content/Fonts/`:
+   ```xml
+   <?xml version="1.0" encoding="utf-8"?>
+   <XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
+     <Asset Type="Graphics:FontDescription">
+       <FontName>Noto Sans CJK</FontName>
+       <Size>14</Size>
+       <Spacing>0</Spacing>
+       <UseKerning>true</UseKerning>
+       <Style>Regular</Style>
+       <CharacterRegions>
+         <!-- ASCII characters (0020-007F) -->
+         <CharacterRegion>
+           <Start>&#x0020;</Start>
+           <End>&#x007F;</End>
+         </CharacterRegion>
+
+         <!-- Insert contents of cjk_character_regions.xml here -->
+         <!-- This covers all 306 CJK characters needed -->
+       </CharacterRegion>
+     </Asset>
+   </XnaContent>
+   ```
+
+3. **Modify font loading logic** to load CJK font when language is Japanese or Chinese
+
+### Option 2: Separate Japanese and Chinese Fonts
+
+Create individual fonts for each language (slightly more optimal but more maintenance):
+
+- `Regular_Japanese.spritefont` (171 characters)
+- `Regular_Chinese.spritefont` (174 characters)
+
+## Font Loading Strategy
+
+In your game's font loading code, add logic like:
+
+```csharp
+string fontName = "Regular"; // Default Latin font
+
+if (GameSettings.Instance.Language == "日本語")
+{
+    fontName = "Regular_CJK"; // or "Regular_Japanese"
+}
+else if (GameSettings.Instance.Language == "中文")
+{
+    fontName = "Regular_CJK"; // or "Regular_Chinese"
+}
+
+SpriteFont font = Content.Load<SpriteFont>($"Fonts/{fontName}");
+```
+
+## Adding Cyrillic Support for Russian
+
+For Russian support, extend your existing Latin font:
+
+1. Open `Regular.spritefont`
+2. Add Cyrillic character range:
+   ```xml
+   <CharacterRegion>
+     <Start>&#x0400;</Start>
+     <End>&#x04FF;</End>
+   </CharacterRegion>
+   ```
+3. Russian (along with existing European languages) will work with one font
+
+## Size Estimates
+
+**Latin + Cyrillic Font:**
+- ~500-600 characters
+- Texture size: ~1-2MB (depending on font size)
+
+**CJK Font (our optimized set):**
+- 306 characters + ASCII (~400 total)
+- Texture size: ~2-3MB (depending on font size)
+
+**Full CJK Font (if we hadn't optimized):**
+- 20,000+ characters
+- Texture size: ~50-100MB (HUGE!)
+
+## Testing the Translations
+
+To test Japanese/Chinese in-game:
+
+1. Create the CJK font file as described above
+2. Build the Content project to generate the .xnb font file
+3. Update GameSettings.cs to include Japanese and Chinese in the language list
+4. Add dynamic font loading based on selected language
+5. Set language to Japanese or Chinese in settings
+
+## Character Set Maintenance
+
+When you add new UI strings:
+
+1. Translate them to Japanese/Chinese in the .resx files
+2. Run `python3 Tools/ExtractCJKCharacters.py` again
+3. It will update the character lists to include any new characters
+4. Rebuild the .spritefont files with updated character ranges

+ 148 - 0
CardsStarterKit/Tools/ExtractCJKCharacters.py

@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+"""
+Extract unique CJK characters from Japanese and Chinese resource files.
+This creates a minimal character set for SpriteFont generation.
+"""
+
+import xml.etree.ElementTree as ET
+import sys
+from pathlib import Path
+
+def extract_characters_from_resx(file_path):
+    """Extract all text values from a .resx file and return unique characters."""
+    tree = ET.parse(file_path)
+    root = tree.getroot()
+
+    all_text = []
+    # Find all <data> elements and extract their <value> content
+    for data in root.findall(".//data"):
+        value = data.find("value")
+        if value is not None and value.text:
+            all_text.append(value.text)
+
+    # Combine all text and get unique characters
+    combined_text = ''.join(all_text)
+    unique_chars = sorted(set(combined_text))
+
+    return unique_chars
+
+def is_cjk_character(char):
+    """Check if a character is CJK (Chinese/Japanese/Korean)."""
+    code_point = ord(char)
+    # CJK Unified Ideographs
+    if 0x4E00 <= code_point <= 0x9FFF:
+        return True
+    # Hiragana
+    if 0x3040 <= code_point <= 0x309F:
+        return True
+    # Katakana
+    if 0x30A0 <= code_point <= 0x30FF:
+        return True
+    # Katakana Phonetic Extensions
+    if 0x31F0 <= code_point <= 0x31FF:
+        return True
+    # CJK Symbols and Punctuation
+    if 0x3000 <= code_point <= 0x303F:
+        return True
+    return False
+
+def generate_character_region_xml(chars):
+    """Generate XML character region entries for MonoGame .spritefont files."""
+    regions = []
+    current_start = None
+    current_end = None
+
+    for char in chars:
+        code_point = ord(char)
+        if current_start is None:
+            current_start = code_point
+            current_end = code_point
+        elif code_point == current_end + 1:
+            current_end = code_point
+        else:
+            # End current region and start new one
+            regions.append(f"    <Start>&#x{current_start:04X};</Start>\n    <End>&#x{current_end:04X};</End>")
+            current_start = code_point
+            current_end = code_point
+
+    # Add final region
+    if current_start is not None:
+        regions.append(f"    <Start>&#x{current_start:04X};</Start>\n    <End>&#x{current_end:04X};</End>")
+
+    return '\n'.join(regions)
+
+def main():
+    # Get the script directory
+    script_dir = Path(__file__).parent
+    resources_dir = script_dir.parent / "Core" / "Game"
+
+    # Process Japanese
+    ja_file = resources_dir / "Resources.ja.resx"
+    if ja_file.exists():
+        print(f"Processing {ja_file}...")
+        ja_chars = extract_characters_from_resx(ja_file)
+        ja_cjk_chars = [c for c in ja_chars if is_cjk_character(c)]
+
+        print(f"\nJapanese Statistics:")
+        print(f"  Total unique characters: {len(ja_chars)}")
+        print(f"  CJK characters: {len(ja_cjk_chars)}")
+        print(f"  ASCII/Latin: {len(ja_chars) - len(ja_cjk_chars)}")
+
+        # Write Japanese character list
+        ja_output = script_dir / "japanese_characters.txt"
+        with open(ja_output, 'w', encoding='utf-8') as f:
+            f.write(''.join(ja_cjk_chars))
+        print(f"  Saved to: {ja_output}")
+
+        # Generate XML for .spritefont
+        ja_xml_output = script_dir / "japanese_character_regions.xml"
+        with open(ja_xml_output, 'w', encoding='utf-8') as f:
+            f.write(generate_character_region_xml(ja_cjk_chars))
+        print(f"  XML regions saved to: {ja_xml_output}")
+
+    # Process Chinese
+    zh_file = resources_dir / "Resources.zh.resx"
+    if zh_file.exists():
+        print(f"\nProcessing {zh_file}...")
+        zh_chars = extract_characters_from_resx(zh_file)
+        zh_cjk_chars = [c for c in zh_chars if is_cjk_character(c)]
+
+        print(f"\nChinese Statistics:")
+        print(f"  Total unique characters: {len(zh_chars)}")
+        print(f"  CJK characters: {len(zh_cjk_chars)}")
+        print(f"  ASCII/Latin: {len(zh_chars) - len(zh_cjk_chars)}")
+
+        # Write Chinese character list
+        zh_output = script_dir / "chinese_characters.txt"
+        with open(zh_output, 'w', encoding='utf-8') as f:
+            f.write(''.join(zh_cjk_chars))
+        print(f"  Saved to: {zh_output}")
+
+        # Generate XML for .spritefont
+        zh_xml_output = script_dir / "chinese_character_regions.xml"
+        with open(zh_xml_output, 'w', encoding='utf-8') as f:
+            f.write(generate_character_region_xml(zh_cjk_chars))
+        print(f"  XML regions saved to: {zh_xml_output}")
+
+    # Combine both for unified CJK font
+    if ja_file.exists() and zh_file.exists():
+        print(f"\nCombining Japanese + Chinese...")
+        combined_cjk = sorted(set(ja_cjk_chars + zh_cjk_chars))
+
+        print(f"\nCombined CJK Statistics:")
+        print(f"  Total unique CJK characters: {len(combined_cjk)}")
+
+        # Write combined character list
+        combined_output = script_dir / "cjk_characters.txt"
+        with open(combined_output, 'w', encoding='utf-8') as f:
+            f.write(''.join(combined_cjk))
+        print(f"  Saved to: {combined_output}")
+
+        # Generate XML for .spritefont
+        combined_xml_output = script_dir / "cjk_character_regions.xml"
+        with open(combined_xml_output, 'w', encoding='utf-8') as f:
+            f.write(generate_character_region_xml(combined_cjk))
+        print(f"  XML regions saved to: {combined_xml_output}")
+
+if __name__ == "__main__":
+    main()

+ 366 - 0
CardsStarterKit/Tools/chinese_character_regions.xml

@@ -0,0 +1,366 @@
+    <Start>&#x3001;</Start>
+    <End>&#x3002;</End>
+    <Start>&#x4E00;</Start>
+    <End>&#x4E00;</End>
+    <Start>&#x4E0A;</Start>
+    <End>&#x4E0B;</End>
+    <Start>&#x4E0D;</Start>
+    <End>&#x4E0E;</End>
+    <Start>&#x4E2A;</Start>
+    <End>&#x4E2A;</End>
+    <Start>&#x4E2D;</Start>
+    <End>&#x4E2D;</End>
+    <Start>&#x4E39;</Start>
+    <End>&#x4E39;</End>
+    <Start>&#x4E3B;</Start>
+    <End>&#x4E3B;</End>
+    <Start>&#x4E4B;</Start>
+    <End>&#x4E4B;</End>
+    <Start>&#x4E50;</Start>
+    <End>&#x4E50;</End>
+    <Start>&#x4E70;</Start>
+    <End>&#x4E70;</End>
+    <Start>&#x4EBA;</Start>
+    <End>&#x4EBA;</End>
+    <Start>&#x4EF6;</Start>
+    <End>&#x4EF6;</End>
+    <Start>&#x4F1A;</Start>
+    <End>&#x4F1A;</End>
+    <Start>&#x4F4D;</Start>
+    <End>&#x4F4D;</End>
+    <Start>&#x4F7F;</Start>
+    <End>&#x4F7F;</End>
+    <Start>&#x4F8B;</Start>
+    <End>&#x4F8B;</End>
+    <Start>&#x4F9B;</Start>
+    <End>&#x4F9B;</End>
+    <Start>&#x4FDD;</Start>
+    <End>&#x4FDD;</End>
+    <Start>&#x500D;</Start>
+    <End>&#x500D;</End>
+    <Start>&#x505C;</Start>
+    <End>&#x505C;</End>
+    <Start>&#x5145;</Start>
+    <End>&#x5145;</End>
+    <Start>&#x5165;</Start>
+    <End>&#x5165;</End>
+    <Start>&#x5171;</Start>
+    <End>&#x5171;</End>
+    <Start>&#x5173;</Start>
+    <End>&#x5173;</End>
+    <Start>&#x51C6;</Start>
+    <End>&#x51C6;</End>
+    <Start>&#x51FA;</Start>
+    <End>&#x51FA;</End>
+    <Start>&#x5206;</Start>
+    <End>&#x5206;</End>
+    <Start>&#x521B;</Start>
+    <End>&#x521B;</End>
+    <Start>&#x5230;</Start>
+    <End>&#x5230;</End>
+    <Start>&#x5237;</Start>
+    <End>&#x5237;</End>
+    <Start>&#x529F;</Start>
+    <End>&#x52A0;</End>
+    <Start>&#x52A8;</Start>
+    <End>&#x52A8;</End>
+    <Start>&#x5355;</Start>
+    <End>&#x5355;</End>
+    <Start>&#x5361;</Start>
+    <End>&#x5361;</End>
+    <Start>&#x5373;</Start>
+    <End>&#x5373;</End>
+    <Start>&#x5385;</Start>
+    <End>&#x5385;</End>
+    <Start>&#x53BB;</Start>
+    <End>&#x53BB;</End>
+    <Start>&#x53D1;</Start>
+    <End>&#x53D1;</End>
+    <Start>&#x53D6;</Start>
+    <End>&#x53D6;</End>
+    <Start>&#x53EF;</Start>
+    <End>&#x53EF;</End>
+    <Start>&#x5408;</Start>
+    <End>&#x5408;</End>
+    <Start>&#x540E;</Start>
+    <End>&#x540E;</End>
+    <Start>&#x5417;</Start>
+    <End>&#x5417;</End>
+    <Start>&#x5426;</Start>
+    <End>&#x5426;</End>
+    <Start>&#x56DE;</Start>
+    <End>&#x56DE;</End>
+    <Start>&#x56FD;</Start>
+    <End>&#x56FD;</End>
+    <Start>&#x5728;</Start>
+    <End>&#x5728;</End>
+    <Start>&#x5730;</Start>
+    <End>&#x5730;</End>
+    <Start>&#x573A;</Start>
+    <End>&#x573A;</End>
+    <Start>&#x57DF;</Start>
+    <End>&#x57DF;</End>
+    <Start>&#x586B;</Start>
+    <End>&#x586B;</End>
+    <Start>&#x5907;</Start>
+    <End>&#x5907;</End>
+    <Start>&#x5927;</Start>
+    <End>&#x5927;</End>
+    <Start>&#x5931;</Start>
+    <End>&#x5931;</End>
+    <Start>&#x5956;</Start>
+    <End>&#x5956;</End>
+    <Start>&#x597D;</Start>
+    <End>&#x597D;</End>
+    <Start>&#x59AE;</Start>
+    <End>&#x59AE;</End>
+    <Start>&#x59CB;</Start>
+    <End>&#x59CB;</End>
+    <Start>&#x5B83;</Start>
+    <End>&#x5B83;</End>
+    <Start>&#x5B9A;</Start>
+    <End>&#x5B9A;</End>
+    <Start>&#x5BB6;</Start>
+    <End>&#x5BB6;</End>
+    <Start>&#x5C06;</Start>
+    <End>&#x5C06;</End>
+    <Start>&#x5C40;</Start>
+    <End>&#x5C40;</End>
+    <Start>&#x5DF2;</Start>
+    <End>&#x5DF2;</End>
+    <Start>&#x5E01;</Start>
+    <End>&#x5E01;</End>
+    <Start>&#x5E76;</Start>
+    <End>&#x5E76;</End>
+    <Start>&#x5E84;</Start>
+    <End>&#x5E84;</End>
+    <Start>&#x5EA6;</Start>
+    <End>&#x5EA6;</End>
+    <Start>&#x5EFA;</Start>
+    <End>&#x5EFA;</End>
+    <Start>&#x5F00;</Start>
+    <End>&#x5F00;</End>
+    <Start>&#x5F3A;</Start>
+    <End>&#x5F3A;</End>
+    <Start>&#x5F55;</Start>
+    <End>&#x5F55;</End>
+    <Start>&#x5F85;</Start>
+    <End>&#x5F85;</End>
+    <Start>&#x5FC5;</Start>
+    <End>&#x5FC5;</End>
+    <Start>&#x5FD7;</Start>
+    <End>&#x5FD7;</End>
+    <Start>&#x60A8;</Start>
+    <End>&#x60A8;</End>
+    <Start>&#x620F;</Start>
+    <End>&#x620F;</End>
+    <Start>&#x6216;</Start>
+    <End>&#x6216;</End>
+    <Start>&#x6240;</Start>
+    <End>&#x6240;</End>
+    <Start>&#x624B;</Start>
+    <End>&#x624B;</End>
+    <Start>&#x624D;</Start>
+    <End>&#x624D;</End>
+    <Start>&#x627E;</Start>
+    <End>&#x627E;</End>
+    <Start>&#x6301;</Start>
+    <End>&#x6301;</End>
+    <Start>&#x6309;</Start>
+    <End>&#x6309;</End>
+    <Start>&#x63A5;</Start>
+    <End>&#x63A5;</End>
+    <Start>&#x63D0;</Start>
+    <End>&#x63D0;</End>
+    <Start>&#x641C;</Start>
+    <End>&#x641C;</End>
+    <Start>&#x6548;</Start>
+    <End>&#x6548;</End>
+    <Start>&#x6570;</Start>
+    <End>&#x6570;</End>
+    <Start>&#x6587;</Start>
+    <End>&#x6587;</End>
+    <Start>&#x65B0;</Start>
+    <End>&#x65B0;</End>
+    <Start>&#x65F6;</Start>
+    <End>&#x65F6;</End>
+    <Start>&#x660E;</Start>
+    <End>&#x660E;</End>
+    <Start>&#x662F;</Start>
+    <End>&#x662F;</End>
+    <Start>&#x663E;</Start>
+    <End>&#x663E;</End>
+    <Start>&#x6682;</Start>
+    <End>&#x6682;</End>
+    <Start>&#x6700;</Start>
+    <End>&#x6700;</End>
+    <Start>&#x6709;</Start>
+    <End>&#x6709;</End>
+    <Start>&#x672A;</Start>
+    <End>&#x672A;</End>
+    <Start>&#x672C;</Start>
+    <End>&#x672C;</End>
+    <Start>&#x673A;</Start>
+    <End>&#x673A;</End>
+    <Start>&#x675F;</Start>
+    <End>&#x675F;</End>
+    <Start>&#x67E5;</Start>
+    <End>&#x67E5;</End>
+    <Start>&#x683C;</Start>
+    <End>&#x683C;</End>
+    <Start>&#x69FD;</Start>
+    <End>&#x69FD;</End>
+    <Start>&#x6B63;</Start>
+    <End>&#x6B64;</End>
+    <Start>&#x6CA1;</Start>
+    <End>&#x6CA1;</End>
+    <Start>&#x6CD5;</Start>
+    <End>&#x6CD5;</End>
+    <Start>&#x6D69;</Start>
+    <End>&#x6D69;</End>
+    <Start>&#x6D88;</Start>
+    <End>&#x6D88;</End>
+    <Start>&#x6E05;</Start>
+    <End>&#x6E05;</End>
+    <Start>&#x6E38;</Start>
+    <End>&#x6E38;</End>
+    <Start>&#x6EE1;</Start>
+    <End>&#x6EE1;</End>
+    <Start>&#x70B9;</Start>
+    <End>&#x70B9;</End>
+    <Start>&#x7248;</Start>
+    <End>&#x7248;</End>
+    <Start>&#x724C;</Start>
+    <End>&#x724C;</End>
+    <Start>&#x73A9;</Start>
+    <End>&#x73A9;</End>
+    <Start>&#x751F;</Start>
+    <End>&#x751F;</End>
+    <Start>&#x7528;</Start>
+    <End>&#x7528;</End>
+    <Start>&#x753B;</Start>
+    <End>&#x753B;</End>
+    <Start>&#x767B;</Start>
+    <End>&#x767B;</End>
+    <Start>&#x7684;</Start>
+    <End>&#x7684;</End>
+    <Start>&#x77E5;</Start>
+    <End>&#x77E5;</End>
+    <Start>&#x786E;</Start>
+    <End>&#x786E;</End>
+    <Start>&#x793A;</Start>
+    <End>&#x793A;</End>
+    <Start>&#x79BB;</Start>
+    <End>&#x79BB;</End>
+    <Start>&#x79D2;</Start>
+    <End>&#x79D2;</End>
+    <Start>&#x7A7A;</Start>
+    <End>&#x7A7A;</End>
+    <Start>&#x7B2C;</Start>
+    <End>&#x7B2C;</End>
+    <Start>&#x7B49;</Start>
+    <End>&#x7B49;</End>
+    <Start>&#x7B97;</Start>
+    <End>&#x7B97;</End>
+    <Start>&#x7D22;</Start>
+    <End>&#x7D22;</End>
+    <Start>&#x7EBF;</Start>
+    <End>&#x7EBF;</End>
+    <Start>&#x7EC4;</Start>
+    <End>&#x7EC4;</End>
+    <Start>&#x7ED3;</Start>
+    <End>&#x7ED3;</End>
+    <Start>&#x7EDC;</Start>
+    <End>&#x7EDC;</End>
+    <Start>&#x7EE7;</Start>
+    <End>&#x7EE7;</End>
+    <Start>&#x7EED;</Start>
+    <End>&#x7EED;</End>
+    <Start>&#x7F51;</Start>
+    <End>&#x7F51;</End>
+    <Start>&#x7F6E;</Start>
+    <End>&#x7F6E;</End>
+    <Start>&#x8005;</Start>
+    <End>&#x8005;</End>
+    <Start>&#x80CC;</Start>
+    <End>&#x80CC;</End>
+    <Start>&#x80FD;</Start>
+    <End>&#x80FD;</End>
+    <Start>&#x81EA;</Start>
+    <End>&#x81EA;</End>
+    <Start>&#x82B3;</Start>
+    <End>&#x82B3;</End>
+    <Start>&#x83DC;</Start>
+    <End>&#x83DC;</End>
+    <Start>&#x8981;</Start>
+    <End>&#x8981;</End>
+    <Start>&#x8A00;</Start>
+    <End>&#x8A00;</End>
+    <Start>&#x8BA1;</Start>
+    <End>&#x8BA1;</End>
+    <Start>&#x8BBE;</Start>
+    <End>&#x8BBF;</End>
+    <Start>&#x8BD5;</Start>
+    <End>&#x8BD5;</End>
+    <Start>&#x8BDD;</Start>
+    <End>&#x8BDD;</End>
+    <Start>&#x8BED;</Start>
+    <End>&#x8BED;</End>
+    <Start>&#x8BEF;</Start>
+    <End>&#x8BEF;</End>
+    <Start>&#x8D25;</Start>
+    <End>&#x8D25;</End>
+    <Start>&#x8D27;</Start>
+    <End>&#x8D27;</End>
+    <Start>&#x8D2D;</Start>
+    <End>&#x8D2D;</End>
+    <Start>&#x8D4C;</Start>
+    <End>&#x8D4C;</End>
+    <Start>&#x8E22;</Start>
+    <End>&#x8E22;</End>
+    <Start>&#x8F66;</Start>
+    <End>&#x8F66;</End>
+    <Start>&#x8F7D;</Start>
+    <End>&#x8F7D;</End>
+    <Start>&#x8FD4;</Start>
+    <End>&#x8FD4;</End>
+    <Start>&#x8FDE;</Start>
+    <End>&#x8FDE;</End>
+    <Start>&#x9000;</Start>
+    <End>&#x9000;</End>
+    <Start>&#x9002;</Start>
+    <End>&#x9002;</End>
+    <Start>&#x901F;</Start>
+    <End>&#x901F;</End>
+    <Start>&#x90FD;</Start>
+    <End>&#x90FD;</End>
+    <Start>&#x914D;</Start>
+    <End>&#x914D;</End>
+    <Start>&#x91CF;</Start>
+    <End>&#x91CF;</End>
+    <Start>&#x91D1;</Start>
+    <End>&#x91D1;</End>
+    <Start>&#x94AE;</Start>
+    <End>&#x94AE;</End>
+    <Start>&#x9519;</Start>
+    <End>&#x9519;</End>
+    <Start>&#x95ED;</Start>
+    <End>&#x95EE;</End>
+    <Start>&#x95F4;</Start>
+    <End>&#x95F4;</End>
+    <Start>&#x9664;</Start>
+    <End>&#x9664;</End>
+    <Start>&#x9669;</Start>
+    <End>&#x9669;</End>
+    <Start>&#x975E;</Start>
+    <End>&#x975E;</End>
+    <Start>&#x97F3;</Start>
+    <End>&#x97F3;</End>
+    <Start>&#x9875;</Start>
+    <End>&#x9875;</End>
+    <Start>&#x987B;</Start>
+    <End>&#x987B;</End>
+    <Start>&#x9891;</Start>
+    <End>&#x9891;</End>
+    <Start>&#x9898;</Start>
+    <End>&#x9898;</End>

+ 1 - 0
CardsStarterKit/Tools/chinese_characters.txt

@@ -0,0 +1 @@
+、。一上下不与个中丹主之乐买人件会位使例供保倍停充入共关准出分创到刷功加动单卡即厅去发取可合后吗否回国在地场域填备大失奖好妮始它定家将局已币并庄度建开强录待必志您戏或所手才找持按接提搜效数文新时明是显暂最有未本机束查格槽正此没法浩消清游满点版牌玩生用画登的知确示离秒空第等算索线组结络继续网置者背能自芳菜要言计设访试话语误败货购赌踢车载返连退适速都配量金钮错闭问间除险非音页须频题

+ 570 - 0
CardsStarterKit/Tools/cjk_character_regions.xml

@@ -0,0 +1,570 @@
+    <Start>&#x3001;</Start>
+    <End>&#x3002;</End>
+    <Start>&#x3042;</Start>
+    <End>&#x3042;</End>
+    <Start>&#x3044;</Start>
+    <End>&#x3044;</End>
+    <Start>&#x304B;</Start>
+    <End>&#x304D;</End>
+    <Start>&#x3053;</Start>
+    <End>&#x3053;</End>
+    <Start>&#x3055;</Start>
+    <End>&#x3055;</End>
+    <Start>&#x3057;</Start>
+    <End>&#x3057;</End>
+    <Start>&#x3059;</Start>
+    <End>&#x3059;</End>
+    <Start>&#x305B;</Start>
+    <End>&#x305B;</End>
+    <Start>&#x305D;</Start>
+    <End>&#x305D;</End>
+    <Start>&#x305F;</Start>
+    <End>&#x305F;</End>
+    <Start>&#x3063;</Start>
+    <End>&#x3064;</End>
+    <Start>&#x3066;</Start>
+    <End>&#x3068;</End>
+    <Start>&#x306A;</Start>
+    <End>&#x306B;</End>
+    <Start>&#x306E;</Start>
+    <End>&#x306F;</End>
+    <Start>&#x3078;</Start>
+    <End>&#x3079;</End>
+    <Start>&#x307E;</Start>
+    <End>&#x307F;</End>
+    <Start>&#x3081;</Start>
+    <End>&#x3082;</End>
+    <Start>&#x3088;</Start>
+    <End>&#x308D;</End>
+    <Start>&#x3092;</Start>
+    <End>&#x3093;</End>
+    <Start>&#x30A1;</Start>
+    <End>&#x30A4;</End>
+    <Start>&#x30A6;</Start>
+    <End>&#x30A6;</End>
+    <Start>&#x30A8;</Start>
+    <End>&#x30A8;</End>
+    <Start>&#x30AA;</Start>
+    <End>&#x30AB;</End>
+    <Start>&#x30AD;</Start>
+    <End>&#x30AD;</End>
+    <Start>&#x30AF;</Start>
+    <End>&#x30B0;</End>
+    <Start>&#x30B2;</Start>
+    <End>&#x30B2;</End>
+    <Start>&#x30B5;</Start>
+    <End>&#x30B5;</End>
+    <Start>&#x30B7;</Start>
+    <End>&#x30B9;</End>
+    <Start>&#x30BB;</Start>
+    <End>&#x30BB;</End>
+    <Start>&#x30BF;</Start>
+    <End>&#x30C0;</End>
+    <Start>&#x30C3;</Start>
+    <End>&#x30C3;</End>
+    <Start>&#x30C6;</Start>
+    <End>&#x30C9;</End>
+    <Start>&#x30CB;</Start>
+    <End>&#x30CB;</End>
+    <Start>&#x30CD;</Start>
+    <End>&#x30CD;</End>
+    <Start>&#x30CF;</Start>
+    <End>&#x30CF;</End>
+    <Start>&#x30D2;</Start>
+    <End>&#x30D3;</End>
+    <Start>&#x30D5;</Start>
+    <End>&#x30D7;</End>
+    <Start>&#x30DA;</Start>
+    <End>&#x30DC;</End>
+    <Start>&#x30DE;</Start>
+    <End>&#x30DE;</End>
+    <Start>&#x30E0;</Start>
+    <End>&#x30E1;</End>
+    <Start>&#x30E3;</Start>
+    <End>&#x30E5;</End>
+    <Start>&#x30E7;</Start>
+    <End>&#x30E7;</End>
+    <Start>&#x30E9;</Start>
+    <End>&#x30ED;</End>
+    <Start>&#x30EF;</Start>
+    <End>&#x30EF;</End>
+    <Start>&#x30F3;</Start>
+    <End>&#x30F3;</End>
+    <Start>&#x30FC;</Start>
+    <End>&#x30FC;</End>
+    <Start>&#x4E00;</Start>
+    <End>&#x4E00;</End>
+    <Start>&#x4E0A;</Start>
+    <End>&#x4E0B;</End>
+    <Start>&#x4E0D;</Start>
+    <End>&#x4E0E;</End>
+    <Start>&#x4E2A;</Start>
+    <End>&#x4E2A;</End>
+    <Start>&#x4E2D;</Start>
+    <End>&#x4E2D;</End>
+    <Start>&#x4E39;</Start>
+    <End>&#x4E39;</End>
+    <Start>&#x4E3B;</Start>
+    <End>&#x4E3B;</End>
+    <Start>&#x4E4B;</Start>
+    <End>&#x4E4B;</End>
+    <Start>&#x4E50;</Start>
+    <End>&#x4E50;</End>
+    <Start>&#x4E70;</Start>
+    <End>&#x4E70;</End>
+    <Start>&#x4E86;</Start>
+    <End>&#x4E86;</End>
+    <Start>&#x4EBA;</Start>
+    <End>&#x4EBA;</End>
+    <Start>&#x4EF6;</Start>
+    <End>&#x4EF6;</End>
+    <Start>&#x4F1A;</Start>
+    <End>&#x4F1A;</End>
+    <Start>&#x4F4D;</Start>
+    <End>&#x4F4D;</End>
+    <Start>&#x4F5C;</Start>
+    <End>&#x4F5C;</End>
+    <Start>&#x4F7F;</Start>
+    <End>&#x4F7F;</End>
+    <Start>&#x4F8B;</Start>
+    <End>&#x4F8B;</End>
+    <Start>&#x4F9B;</Start>
+    <End>&#x4F9B;</End>
+    <Start>&#x4FDD;</Start>
+    <End>&#x4FDD;</End>
+    <Start>&#x500B;</Start>
+    <End>&#x500B;</End>
+    <Start>&#x500D;</Start>
+    <End>&#x500D;</End>
+    <Start>&#x505C;</Start>
+    <End>&#x505C;</End>
+    <Start>&#x5065;</Start>
+    <End>&#x5065;</End>
+    <Start>&#x5099;</Start>
+    <End>&#x5099;</End>
+    <Start>&#x5145;</Start>
+    <End>&#x5145;</End>
+    <Start>&#x5165;</Start>
+    <End>&#x5165;</End>
+    <Start>&#x5171;</Start>
+    <End>&#x5171;</End>
+    <Start>&#x5173;</Start>
+    <End>&#x5173;</End>
+    <Start>&#x518D;</Start>
+    <End>&#x518D;</End>
+    <Start>&#x51C6;</Start>
+    <End>&#x51C6;</End>
+    <Start>&#x51FA;</Start>
+    <End>&#x51FA;</End>
+    <Start>&#x5206;</Start>
+    <End>&#x5207;</End>
+    <Start>&#x521B;</Start>
+    <End>&#x521B;</End>
+    <Start>&#x5229;</Start>
+    <End>&#x5229;</End>
+    <Start>&#x5230;</Start>
+    <End>&#x5230;</End>
+    <Start>&#x5237;</Start>
+    <End>&#x5237;</End>
+    <Start>&#x524D;</Start>
+    <End>&#x524D;</End>
+    <Start>&#x525B;</Start>
+    <End>&#x525B;</End>
+    <Start>&#x529F;</Start>
+    <End>&#x52A0;</End>
+    <Start>&#x52A8;</Start>
+    <End>&#x52A8;</End>
+    <Start>&#x52B9;</Start>
+    <End>&#x52B9;</End>
+    <Start>&#x52D5;</Start>
+    <End>&#x52D5;</End>
+    <Start>&#x5355;</Start>
+    <End>&#x5355;</End>
+    <Start>&#x5361;</Start>
+    <End>&#x5361;</End>
+    <Start>&#x5373;</Start>
+    <End>&#x5373;</End>
+    <Start>&#x5385;</Start>
+    <End>&#x5385;</End>
+    <Start>&#x53BB;</Start>
+    <End>&#x53BB;</End>
+    <Start>&#x53C2;</Start>
+    <End>&#x53C2;</End>
+    <Start>&#x53D1;</Start>
+    <End>&#x53D1;</End>
+    <Start>&#x53D6;</Start>
+    <End>&#x53D6;</End>
+    <Start>&#x53EF;</Start>
+    <End>&#x53EF;</End>
+    <Start>&#x5408;</Start>
+    <End>&#x5408;</End>
+    <Start>&#x540E;</Start>
+    <End>&#x540E;</End>
+    <Start>&#x5417;</Start>
+    <End>&#x5417;</End>
+    <Start>&#x5426;</Start>
+    <End>&#x5426;</End>
+    <Start>&#x54E1;</Start>
+    <End>&#x54E1;</End>
+    <Start>&#x56DE;</Start>
+    <End>&#x56DE;</End>
+    <Start>&#x56FD;</Start>
+    <End>&#x56FD;</End>
+    <Start>&#x5728;</Start>
+    <End>&#x5728;</End>
+    <Start>&#x5730;</Start>
+    <End>&#x5730;</End>
+    <Start>&#x573A;</Start>
+    <End>&#x573A;</End>
+    <Start>&#x57CB;</Start>
+    <End>&#x57CB;</End>
+    <Start>&#x57DF;</Start>
+    <End>&#x57DF;</End>
+    <Start>&#x586B;</Start>
+    <End>&#x586B;</End>
+    <Start>&#x5907;</Start>
+    <End>&#x5907;</End>
+    <Start>&#x5927;</Start>
+    <End>&#x5927;</End>
+    <Start>&#x592A;</Start>
+    <End>&#x592A;</End>
+    <Start>&#x5931;</Start>
+    <End>&#x5931;</End>
+    <Start>&#x5956;</Start>
+    <End>&#x5956;</End>
+    <Start>&#x597D;</Start>
+    <End>&#x597D;</End>
+    <Start>&#x59AE;</Start>
+    <End>&#x59AE;</End>
+    <Start>&#x59CB;</Start>
+    <End>&#x59CB;</End>
+    <Start>&#x5B50;</Start>
+    <End>&#x5B50;</End>
+    <Start>&#x5B83;</Start>
+    <End>&#x5B83;</End>
+    <Start>&#x5B9A;</Start>
+    <End>&#x5B9A;</End>
+    <Start>&#x5BB6;</Start>
+    <End>&#x5BB6;</End>
+    <Start>&#x5C06;</Start>
+    <End>&#x5C06;</End>
+    <Start>&#x5C40;</Start>
+    <End>&#x5C40;</End>
+    <Start>&#x5DF2;</Start>
+    <End>&#x5DF2;</End>
+    <Start>&#x5E01;</Start>
+    <End>&#x5E01;</End>
+    <Start>&#x5E76;</Start>
+    <End>&#x5E76;</End>
+    <Start>&#x5E84;</Start>
+    <End>&#x5E84;</End>
+    <Start>&#x5EA6;</Start>
+    <End>&#x5EA6;</End>
+    <Start>&#x5EFA;</Start>
+    <End>&#x5EFA;</End>
+    <Start>&#x5F00;</Start>
+    <End>&#x5F00;</End>
+    <Start>&#x5F3A;</Start>
+    <End>&#x5F3A;</End>
+    <Start>&#x5F55;</Start>
+    <End>&#x5F55;</End>
+    <Start>&#x5F85;</Start>
+    <End>&#x5F85;</End>
+    <Start>&#x5F8C;</Start>
+    <End>&#x5F8C;</End>
+    <Start>&#x5FC5;</Start>
+    <End>&#x5FC5;</End>
+    <Start>&#x5FD7;</Start>
+    <End>&#x5FD7;</End>
+    <Start>&#x6027;</Start>
+    <End>&#x6027;</End>
+    <Start>&#x6075;</Start>
+    <End>&#x6075;</End>
+    <Start>&#x60A8;</Start>
+    <End>&#x60A8;</End>
+    <Start>&#x620F;</Start>
+    <End>&#x6210;</End>
+    <Start>&#x6216;</Start>
+    <End>&#x6216;</End>
+    <Start>&#x623B;</Start>
+    <End>&#x623B;</End>
+    <Start>&#x6240;</Start>
+    <End>&#x6240;</End>
+    <Start>&#x624B;</Start>
+    <End>&#x624B;</End>
+    <Start>&#x624D;</Start>
+    <End>&#x624D;</End>
+    <Start>&#x627E;</Start>
+    <End>&#x627E;</End>
+    <Start>&#x6301;</Start>
+    <End>&#x6301;</End>
+    <Start>&#x6309;</Start>
+    <End>&#x6309;</End>
+    <Start>&#x63A5;</Start>
+    <End>&#x63A5;</End>
+    <Start>&#x63D0;</Start>
+    <End>&#x63D0;</End>
+    <Start>&#x641C;</Start>
+    <End>&#x641C;</End>
+    <Start>&#x653E;</Start>
+    <End>&#x653E;</End>
+    <Start>&#x6548;</Start>
+    <End>&#x6548;</End>
+    <Start>&#x6557;</Start>
+    <End>&#x6557;</End>
+    <Start>&#x6570;</Start>
+    <End>&#x6570;</End>
+    <Start>&#x6587;</Start>
+    <End>&#x6587;</End>
+    <Start>&#x65AD;</Start>
+    <End>&#x65AD;</End>
+    <Start>&#x65B0;</Start>
+    <End>&#x65B0;</End>
+    <Start>&#x65E2;</Start>
+    <End>&#x65E2;</End>
+    <Start>&#x65F6;</Start>
+    <End>&#x65F6;</End>
+    <Start>&#x660E;</Start>
+    <End>&#x660E;</End>
+    <Start>&#x662F;</Start>
+    <End>&#x662F;</End>
+    <Start>&#x663E;</Start>
+    <End>&#x663E;</End>
+    <Start>&#x6642;</Start>
+    <End>&#x6642;</End>
+    <Start>&#x6682;</Start>
+    <End>&#x6682;</End>
+    <Start>&#x66F4;</Start>
+    <End>&#x66F4;</End>
+    <Start>&#x6700;</Start>
+    <End>&#x6700;</End>
+    <Start>&#x6709;</Start>
+    <End>&#x6709;</End>
+    <Start>&#x672A;</Start>
+    <End>&#x672A;</End>
+    <Start>&#x672C;</Start>
+    <End>&#x672C;</End>
+    <Start>&#x673A;</Start>
+    <End>&#x673A;</End>
+    <Start>&#x675F;</Start>
+    <End>&#x675F;</End>
+    <Start>&#x679C;</Start>
+    <End>&#x679C;</End>
+    <Start>&#x67E5;</Start>
+    <End>&#x67E5;</End>
+    <Start>&#x683C;</Start>
+    <End>&#x683C;</End>
+    <Start>&#x691C;</Start>
+    <End>&#x691C;</End>
+    <Start>&#x697D;</Start>
+    <End>&#x697D;</End>
+    <Start>&#x69FD;</Start>
+    <End>&#x69FD;</End>
+    <Start>&#x6A5F;</Start>
+    <End>&#x6A5F;</End>
+    <Start>&#x6B21;</Start>
+    <End>&#x6B21;</End>
+    <Start>&#x6B62;</Start>
+    <End>&#x6B64;</End>
+    <Start>&#x6CA1;</Start>
+    <End>&#x6CA1;</End>
+    <Start>&#x6CD5;</Start>
+    <End>&#x6CD5;</End>
+    <Start>&#x6D69;</Start>
+    <End>&#x6D69;</End>
+    <Start>&#x6D88;</Start>
+    <End>&#x6D88;</End>
+    <Start>&#x6E05;</Start>
+    <End>&#x6E05;</End>
+    <Start>&#x6E38;</Start>
+    <End>&#x6E38;</End>
+    <Start>&#x6E80;</Start>
+    <End>&#x6E80;</End>
+    <Start>&#x6E96;</Start>
+    <End>&#x6E96;</End>
+    <Start>&#x6EE1;</Start>
+    <End>&#x6EE1;</End>
+    <Start>&#x70B9;</Start>
+    <End>&#x70B9;</End>
+    <Start>&#x7248;</Start>
+    <End>&#x7248;</End>
+    <Start>&#x724C;</Start>
+    <End>&#x724C;</End>
+    <Start>&#x73A9;</Start>
+    <End>&#x73A9;</End>
+    <Start>&#x751F;</Start>
+    <End>&#x751F;</End>
+    <Start>&#x7528;</Start>
+    <End>&#x7528;</End>
+    <Start>&#x753B;</Start>
+    <End>&#x753B;</End>
+    <Start>&#x767A;</Start>
+    <End>&#x767B;</End>
+    <Start>&#x7684;</Start>
+    <End>&#x7684;</End>
+    <Start>&#x77E5;</Start>
+    <End>&#x77E5;</End>
+    <Start>&#x786E;</Start>
+    <End>&#x786E;</End>
+    <Start>&#x793A;</Start>
+    <End>&#x793A;</End>
+    <Start>&#x79BB;</Start>
+    <End>&#x79BB;</End>
+    <Start>&#x79D2;</Start>
+    <End>&#x79D2;</End>
+    <Start>&#x7A7A;</Start>
+    <End>&#x7A7A;</End>
+    <Start>&#x7B2C;</Start>
+    <End>&#x7B2C;</End>
+    <Start>&#x7B49;</Start>
+    <End>&#x7B49;</End>
+    <Start>&#x7B97;</Start>
+    <End>&#x7B97;</End>
+    <Start>&#x7D22;</Start>
+    <End>&#x7D22;</End>
+    <Start>&#x7D42;</Start>
+    <End>&#x7D42;</End>
+    <Start>&#x7D9A;</Start>
+    <End>&#x7D9A;</End>
+    <Start>&#x7EBF;</Start>
+    <End>&#x7EBF;</End>
+    <Start>&#x7EC4;</Start>
+    <End>&#x7EC4;</End>
+    <Start>&#x7ED3;</Start>
+    <End>&#x7ED3;</End>
+    <Start>&#x7EDC;</Start>
+    <End>&#x7EDC;</End>
+    <Start>&#x7EE7;</Start>
+    <End>&#x7EE7;</End>
+    <Start>&#x7EED;</Start>
+    <End>&#x7EED;</End>
+    <Start>&#x7F51;</Start>
+    <End>&#x7F51;</End>
+    <Start>&#x7F6E;</Start>
+    <End>&#x7F6E;</End>
+    <Start>&#x7F8E;</Start>
+    <End>&#x7F8E;</End>
+    <Start>&#x8005;</Start>
+    <End>&#x8005;</End>
+    <Start>&#x80CC;</Start>
+    <End>&#x80CC;</End>
+    <Start>&#x80FD;</Start>
+    <End>&#x80FD;</End>
+    <Start>&#x81EA;</Start>
+    <End>&#x81EA;</End>
+    <Start>&#x82B1;</Start>
+    <End>&#x82B1;</End>
+    <Start>&#x82B3;</Start>
+    <End>&#x82B3;</End>
+    <Start>&#x83DC;</Start>
+    <End>&#x83DC;</End>
+    <Start>&#x8868;</Start>
+    <End>&#x8868;</End>
+    <Start>&#x88CF;</Start>
+    <End>&#x88CF;</End>
+    <Start>&#x8981;</Start>
+    <End>&#x8981;</End>
+    <Start>&#x898B;</Start>
+    <End>&#x898B;</End>
+    <Start>&#x8A00;</Start>
+    <End>&#x8A00;</End>
+    <Start>&#x8A2D;</Start>
+    <End>&#x8A2D;</End>
+    <Start>&#x8A66;</Start>
+    <End>&#x8A66;</End>
+    <Start>&#x8A9E;</Start>
+    <End>&#x8A9E;</End>
+    <Start>&#x8AAD;</Start>
+    <End>&#x8AAD;</End>
+    <Start>&#x8BA1;</Start>
+    <End>&#x8BA1;</End>
+    <Start>&#x8BBE;</Start>
+    <End>&#x8BBF;</End>
+    <Start>&#x8BD5;</Start>
+    <End>&#x8BD5;</End>
+    <Start>&#x8BDD;</Start>
+    <End>&#x8BDD;</End>
+    <Start>&#x8BED;</Start>
+    <End>&#x8BED;</End>
+    <Start>&#x8BEF;</Start>
+    <End>&#x8BEF;</End>
+    <Start>&#x8CA8;</Start>
+    <End>&#x8CA8;</End>
+    <Start>&#x8CDE;</Start>
+    <End>&#x8CDE;</End>
+    <Start>&#x8CFC;</Start>
+    <End>&#x8CFC;</End>
+    <Start>&#x8D25;</Start>
+    <End>&#x8D25;</End>
+    <Start>&#x8D27;</Start>
+    <End>&#x8D27;</End>
+    <Start>&#x8D2D;</Start>
+    <End>&#x8D2D;</End>
+    <Start>&#x8D4C;</Start>
+    <End>&#x8D4C;</End>
+    <Start>&#x8E22;</Start>
+    <End>&#x8E22;</End>
+    <Start>&#x8F14;</Start>
+    <End>&#x8F14;</End>
+    <Start>&#x8F66;</Start>
+    <End>&#x8F66;</End>
+    <Start>&#x8F7D;</Start>
+    <End>&#x8F7D;</End>
+    <Start>&#x8FBC;</Start>
+    <End>&#x8FBC;</End>
+    <Start>&#x8FD4;</Start>
+    <End>&#x8FD4;</End>
+    <Start>&#x8FDE;</Start>
+    <End>&#x8FDE;</End>
+    <Start>&#x8FFD;</Start>
+    <End>&#x8FFD;</End>
+    <Start>&#x9000;</Start>
+    <End>&#x9000;</End>
+    <Start>&#x9002;</Start>
+    <End>&#x9002;</End>
+    <Start>&#x901A;</Start>
+    <End>&#x901A;</End>
+    <Start>&#x901F;</Start>
+    <End>&#x901F;</End>
+    <Start>&#x9069;</Start>
+    <End>&#x9069;</End>
+    <Start>&#x90CE;</Start>
+    <End>&#x90CE;</End>
+    <Start>&#x90FD;</Start>
+    <End>&#x90FD;</End>
+    <Start>&#x914D;</Start>
+    <End>&#x914D;</End>
+    <Start>&#x91CF;</Start>
+    <End>&#x91CF;</End>
+    <Start>&#x91D1;</Start>
+    <End>&#x91D1;</End>
+    <Start>&#x94AE;</Start>
+    <End>&#x94AE;</End>
+    <Start>&#x9519;</Start>
+    <End>&#x9519;</End>
+    <Start>&#x958B;</Start>
+    <End>&#x958B;</End>
+    <Start>&#x9593;</Start>
+    <End>&#x9593;</End>
+    <Start>&#x95ED;</Start>
+    <End>&#x95EE;</End>
+    <Start>&#x95F4;</Start>
+    <End>&#x95F4;</End>
+    <Start>&#x9664;</Start>
+    <End>&#x9664;</End>
+    <Start>&#x9669;</Start>
+    <End>&#x9669;</End>
+    <Start>&#x975E;</Start>
+    <End>&#x975E;</End>
+    <Start>&#x9762;</Start>
+    <End>&#x9762;</End>
+    <Start>&#x97F3;</Start>
+    <End>&#x97F3;</End>
+    <Start>&#x9875;</Start>
+    <End>&#x9875;</End>
+    <Start>&#x987B;</Start>
+    <End>&#x987B;</End>
+    <Start>&#x9891;</Start>
+    <End>&#x9891;</End>
+    <Start>&#x9898;</Start>
+    <End>&#x9898;</End>

+ 1 - 0
CardsStarterKit/Tools/cjk_characters.txt

@@ -0,0 +1 @@
+、。あいかがきこさしすせそたっつてでとなにのはへべまみめもよらりるれろをんァアィイウエオカキクグゲサシジスセタダッテデトドニネハヒビフブプペホボマムメャヤュョラリルレロワンー一上下不与个中丹主之乐买了人件会位作使例供保個倍停健備充入共关再准出分切创利到刷前剛功加动効動单卡即厅去参发取可合后吗否員回国在地场埋域填备大太失奖好妮始子它定家将局已币并庄度建开强录待後必志性恵您戏成或戻所手才找持按接提搜放效敗数文断新既时明是显時暂更最有未本机束果查格検楽槽機次止正此没法浩消清游満準满点版牌玩生用画発登的知确示离秒空第等算索終続线组结络继续网置美者背能自花芳菜表裏要見言設試語読计设访试话语误貨賞購败货购赌踢輔车载込返连追退适通速適郎都配量金钮错開間闭问间除险非面音页须频题

+ 294 - 0
CardsStarterKit/Tools/japanese_character_regions.xml

@@ -0,0 +1,294 @@
+    <Start>&#x3001;</Start>
+    <End>&#x3002;</End>
+    <Start>&#x3042;</Start>
+    <End>&#x3042;</End>
+    <Start>&#x3044;</Start>
+    <End>&#x3044;</End>
+    <Start>&#x304B;</Start>
+    <End>&#x304D;</End>
+    <Start>&#x3053;</Start>
+    <End>&#x3053;</End>
+    <Start>&#x3055;</Start>
+    <End>&#x3055;</End>
+    <Start>&#x3057;</Start>
+    <End>&#x3057;</End>
+    <Start>&#x3059;</Start>
+    <End>&#x3059;</End>
+    <Start>&#x305B;</Start>
+    <End>&#x305B;</End>
+    <Start>&#x305D;</Start>
+    <End>&#x305D;</End>
+    <Start>&#x305F;</Start>
+    <End>&#x305F;</End>
+    <Start>&#x3063;</Start>
+    <End>&#x3064;</End>
+    <Start>&#x3066;</Start>
+    <End>&#x3068;</End>
+    <Start>&#x306A;</Start>
+    <End>&#x306B;</End>
+    <Start>&#x306E;</Start>
+    <End>&#x306F;</End>
+    <Start>&#x3078;</Start>
+    <End>&#x3079;</End>
+    <Start>&#x307E;</Start>
+    <End>&#x307F;</End>
+    <Start>&#x3081;</Start>
+    <End>&#x3082;</End>
+    <Start>&#x3088;</Start>
+    <End>&#x308D;</End>
+    <Start>&#x3092;</Start>
+    <End>&#x3093;</End>
+    <Start>&#x30A1;</Start>
+    <End>&#x30A4;</End>
+    <Start>&#x30A6;</Start>
+    <End>&#x30A6;</End>
+    <Start>&#x30A8;</Start>
+    <End>&#x30A8;</End>
+    <Start>&#x30AA;</Start>
+    <End>&#x30AB;</End>
+    <Start>&#x30AD;</Start>
+    <End>&#x30AD;</End>
+    <Start>&#x30AF;</Start>
+    <End>&#x30B0;</End>
+    <Start>&#x30B2;</Start>
+    <End>&#x30B2;</End>
+    <Start>&#x30B5;</Start>
+    <End>&#x30B5;</End>
+    <Start>&#x30B7;</Start>
+    <End>&#x30B9;</End>
+    <Start>&#x30BB;</Start>
+    <End>&#x30BB;</End>
+    <Start>&#x30BF;</Start>
+    <End>&#x30C0;</End>
+    <Start>&#x30C3;</Start>
+    <End>&#x30C3;</End>
+    <Start>&#x30C6;</Start>
+    <End>&#x30C9;</End>
+    <Start>&#x30CB;</Start>
+    <End>&#x30CB;</End>
+    <Start>&#x30CD;</Start>
+    <End>&#x30CD;</End>
+    <Start>&#x30CF;</Start>
+    <End>&#x30CF;</End>
+    <Start>&#x30D2;</Start>
+    <End>&#x30D3;</End>
+    <Start>&#x30D5;</Start>
+    <End>&#x30D7;</End>
+    <Start>&#x30DA;</Start>
+    <End>&#x30DC;</End>
+    <Start>&#x30DE;</Start>
+    <End>&#x30DE;</End>
+    <Start>&#x30E0;</Start>
+    <End>&#x30E1;</End>
+    <Start>&#x30E3;</Start>
+    <End>&#x30E5;</End>
+    <Start>&#x30E7;</Start>
+    <End>&#x30E7;</End>
+    <Start>&#x30E9;</Start>
+    <End>&#x30ED;</End>
+    <Start>&#x30EF;</Start>
+    <End>&#x30EF;</End>
+    <Start>&#x30F3;</Start>
+    <End>&#x30F3;</End>
+    <Start>&#x30FC;</Start>
+    <End>&#x30FC;</End>
+    <Start>&#x4E00;</Start>
+    <End>&#x4E00;</End>
+    <Start>&#x4E0D;</Start>
+    <End>&#x4E0D;</End>
+    <Start>&#x4E2D;</Start>
+    <End>&#x4E2D;</End>
+    <Start>&#x4E86;</Start>
+    <End>&#x4E86;</End>
+    <Start>&#x4F5C;</Start>
+    <End>&#x4F5C;</End>
+    <Start>&#x4FDD;</Start>
+    <End>&#x4FDD;</End>
+    <Start>&#x500B;</Start>
+    <End>&#x500B;</End>
+    <Start>&#x505C;</Start>
+    <End>&#x505C;</End>
+    <Start>&#x5065;</Start>
+    <End>&#x5065;</End>
+    <Start>&#x5099;</Start>
+    <End>&#x5099;</End>
+    <Start>&#x5165;</Start>
+    <End>&#x5165;</End>
+    <Start>&#x518D;</Start>
+    <End>&#x518D;</End>
+    <Start>&#x51FA;</Start>
+    <End>&#x51FA;</End>
+    <Start>&#x5207;</Start>
+    <End>&#x5207;</End>
+    <Start>&#x5229;</Start>
+    <End>&#x5229;</End>
+    <Start>&#x524D;</Start>
+    <End>&#x524D;</End>
+    <Start>&#x525B;</Start>
+    <End>&#x525B;</End>
+    <Start>&#x52A0;</Start>
+    <End>&#x52A0;</End>
+    <Start>&#x52B9;</Start>
+    <End>&#x52B9;</End>
+    <Start>&#x52D5;</Start>
+    <End>&#x52D5;</End>
+    <Start>&#x53C2;</Start>
+    <End>&#x53C2;</End>
+    <Start>&#x53EF;</Start>
+    <End>&#x53EF;</End>
+    <Start>&#x54E1;</Start>
+    <End>&#x54E1;</End>
+    <Start>&#x57CB;</Start>
+    <End>&#x57CB;</End>
+    <Start>&#x5927;</Start>
+    <End>&#x5927;</End>
+    <Start>&#x592A;</Start>
+    <End>&#x592A;</End>
+    <Start>&#x5931;</Start>
+    <End>&#x5931;</End>
+    <Start>&#x59CB;</Start>
+    <End>&#x59CB;</End>
+    <Start>&#x5B50;</Start>
+    <End>&#x5B50;</End>
+    <Start>&#x5B9A;</Start>
+    <End>&#x5B9A;</End>
+    <Start>&#x5EA6;</Start>
+    <End>&#x5EA6;</End>
+    <Start>&#x5F85;</Start>
+    <End>&#x5F85;</End>
+    <Start>&#x5F8C;</Start>
+    <End>&#x5F8C;</End>
+    <Start>&#x5FC5;</Start>
+    <End>&#x5FC5;</End>
+    <Start>&#x6027;</Start>
+    <End>&#x6027;</End>
+    <Start>&#x6075;</Start>
+    <End>&#x6075;</End>
+    <Start>&#x6210;</Start>
+    <End>&#x6210;</End>
+    <Start>&#x623B;</Start>
+    <End>&#x623B;</End>
+    <Start>&#x624B;</Start>
+    <End>&#x624B;</End>
+    <Start>&#x6301;</Start>
+    <End>&#x6301;</End>
+    <Start>&#x63A5;</Start>
+    <End>&#x63A5;</End>
+    <Start>&#x653E;</Start>
+    <End>&#x653E;</End>
+    <Start>&#x6557;</Start>
+    <End>&#x6557;</End>
+    <Start>&#x6570;</Start>
+    <End>&#x6570;</End>
+    <Start>&#x65AD;</Start>
+    <End>&#x65AD;</End>
+    <Start>&#x65B0;</Start>
+    <End>&#x65B0;</End>
+    <Start>&#x65E2;</Start>
+    <End>&#x65E2;</End>
+    <Start>&#x660E;</Start>
+    <End>&#x660E;</End>
+    <Start>&#x6642;</Start>
+    <End>&#x6642;</End>
+    <Start>&#x66F4;</Start>
+    <End>&#x66F4;</End>
+    <Start>&#x6700;</Start>
+    <End>&#x6700;</End>
+    <Start>&#x679C;</Start>
+    <End>&#x679C;</End>
+    <Start>&#x691C;</Start>
+    <End>&#x691C;</End>
+    <Start>&#x697D;</Start>
+    <End>&#x697D;</End>
+    <Start>&#x6A5F;</Start>
+    <End>&#x6A5F;</End>
+    <Start>&#x6B21;</Start>
+    <End>&#x6B21;</End>
+    <Start>&#x6B62;</Start>
+    <End>&#x6B62;</End>
+    <Start>&#x6E80;</Start>
+    <End>&#x6E80;</End>
+    <Start>&#x6E96;</Start>
+    <End>&#x6E96;</End>
+    <Start>&#x7248;</Start>
+    <End>&#x7248;</End>
+    <Start>&#x751F;</Start>
+    <End>&#x751F;</End>
+    <Start>&#x7528;</Start>
+    <End>&#x7528;</End>
+    <Start>&#x767A;</Start>
+    <End>&#x767A;</End>
+    <Start>&#x793A;</Start>
+    <End>&#x793A;</End>
+    <Start>&#x79D2;</Start>
+    <End>&#x79D2;</End>
+    <Start>&#x7A7A;</Start>
+    <End>&#x7A7A;</End>
+    <Start>&#x7D22;</Start>
+    <End>&#x7D22;</End>
+    <Start>&#x7D42;</Start>
+    <End>&#x7D42;</End>
+    <Start>&#x7D9A;</Start>
+    <End>&#x7D9A;</End>
+    <Start>&#x7F8E;</Start>
+    <End>&#x7F8E;</End>
+    <Start>&#x80FD;</Start>
+    <End>&#x80FD;</End>
+    <Start>&#x81EA;</Start>
+    <End>&#x81EA;</End>
+    <Start>&#x82B1;</Start>
+    <End>&#x82B1;</End>
+    <Start>&#x8868;</Start>
+    <End>&#x8868;</End>
+    <Start>&#x88CF;</Start>
+    <End>&#x88CF;</End>
+    <Start>&#x8981;</Start>
+    <End>&#x8981;</End>
+    <Start>&#x898B;</Start>
+    <End>&#x898B;</End>
+    <Start>&#x8A00;</Start>
+    <End>&#x8A00;</End>
+    <Start>&#x8A2D;</Start>
+    <End>&#x8A2D;</End>
+    <Start>&#x8A66;</Start>
+    <End>&#x8A66;</End>
+    <Start>&#x8A9E;</Start>
+    <End>&#x8A9E;</End>
+    <Start>&#x8AAD;</Start>
+    <End>&#x8AAD;</End>
+    <Start>&#x8CA8;</Start>
+    <End>&#x8CA8;</End>
+    <Start>&#x8CDE;</Start>
+    <End>&#x8CDE;</End>
+    <Start>&#x8CFC;</Start>
+    <End>&#x8CFC;</End>
+    <Start>&#x8F14;</Start>
+    <End>&#x8F14;</End>
+    <Start>&#x8FBC;</Start>
+    <End>&#x8FBC;</End>
+    <Start>&#x8FFD;</Start>
+    <End>&#x8FFD;</End>
+    <Start>&#x9000;</Start>
+    <End>&#x9000;</End>
+    <Start>&#x901A;</Start>
+    <End>&#x901A;</End>
+    <Start>&#x901F;</Start>
+    <End>&#x901F;</End>
+    <Start>&#x9069;</Start>
+    <End>&#x9069;</End>
+    <Start>&#x90CE;</Start>
+    <End>&#x90CE;</End>
+    <Start>&#x914D;</Start>
+    <End>&#x914D;</End>
+    <Start>&#x91CF;</Start>
+    <End>&#x91CF;</End>
+    <Start>&#x91D1;</Start>
+    <End>&#x91D1;</End>
+    <Start>&#x958B;</Start>
+    <End>&#x958B;</End>
+    <Start>&#x9593;</Start>
+    <End>&#x9593;</End>
+    <Start>&#x9762;</Start>
+    <End>&#x9762;</End>
+    <Start>&#x97F3;</Start>
+    <End>&#x97F3;</End>

+ 1 - 0
CardsStarterKit/Tools/japanese_characters.txt

@@ -0,0 +1 @@
+、。あいかがきこさしすせそたっつてでとなにのはへべまみめもよらりるれろをんァアィイウエオカキクグゲサシジスセタダッテデトドニネハヒビフブプペホボマムメャヤュョラリルレロワンー一不中了作保個停健備入再出切利前剛加効動参可員埋大太失始子定度待後必性恵成戻手持接放敗数断新既明時更最果検楽機次止満準版生用発示秒空索終続美能自花表裏要見言設試語読貨賞購輔込追退通速適郎配量金開間面音

+ 1392 - 0
CardsStarterKit/Tutorials/01_MagicTrick_NineCardTrick.md

@@ -0,0 +1,1392 @@
+# Tutorial: Building a 9-Card Magic Trick Game
+
+## Overview
+
+In this tutorial, you'll learn how to extend the CardsStarterKit framework to create a simple interactive magic trick called the "9-Card Mind Reader." This trick teaches you the fundamentals of:
+
+- Extending the `CardsGame` base class
+- Creating custom game rules with `GameRule`
+- Managing game state machines
+- Working with card animations
+- Creating interactive UI with buttons
+- Handling single-player gameplay
+
+**Target Audience:** MonoGame developers new to card games
+
+**Estimated Time:** 2-3 hours
+
+**Difficulty:** Beginner
+
+---
+
+## The Magic Trick Explained
+
+### How It Works
+
+The 9-Card Mind Reader is a classic mathematical card trick:
+
+1. **Setup:** Lay out 9 cards face-up in a 3x3 grid
+2. **Selection:** The spectator (player) mentally selects one card
+3. **Reveal Phase 1:** The magician picks up the cards in 3 columns and asks "Which pile is your card in?"
+4. **Rearrange:** The magician places the selected pile in the middle and lays out the cards again in 3 columns
+5. **Reveal Phase 2:** The magician asks again "Which pile?"
+6. **Final Reveal:** The magician dramatically reveals the middle card - which is always the selected card!
+
+### The Secret
+
+By placing the selected pile in the middle position twice, the card always ends up in the center position (5th card). Simple mathematics makes it foolproof!
+
+---
+
+## Part 1: Project Structure Setup
+
+### Step 1.1: Create the Directory Structure
+
+We'll organize our magic trick code similarly to the Blackjack implementation:
+
+```
+Core/Game/MagicTrick/
+├── Game/
+│   ├── MagicTrickCardGame.cs
+│   ├── MagicTrickGameState.cs
+├── Players/
+│   └── MagicTrickPlayer.cs
+├── Rules/
+│   ├── CardSelectionRule.cs
+│   └── RevealRule.cs
+└── UI/
+    └── (We'll use existing Button class)
+```
+
+Create these directories:
+
+```bash
+mkdir -p Core/Game/MagicTrick/Game
+mkdir -p Core/Game/MagicTrick/Players
+mkdir -p Core/Game/MagicTrick/Rules
+mkdir -p Core/Game/MagicTrick/UI
+```
+
+---
+
+## Part 2: Define Game State
+
+### Step 2.1: Create the Game State Enum
+
+The magic trick has distinct phases, so we'll use a state machine to control flow.
+
+**Create:** `Core/Game/MagicTrick/Game/MagicTrickGameState.cs`
+
+```csharp
+namespace CardsFramework.MagicTrick
+{
+    /// <summary>
+    /// Represents the various states of the magic trick game
+    /// </summary>
+    public enum MagicTrickGameState
+    {
+        /// <summary>
+        /// Initial setup - dealing 9 cards
+        /// </summary>
+        Dealing,
+
+        /// <summary>
+        /// Player is selecting a card mentally
+        /// </summary>
+        PlayerSelecting,
+
+        /// <summary>
+        /// First pile selection phase
+        /// </summary>
+        FirstPileSelection,
+
+        /// <summary>
+        /// Cards being rearranged after first selection
+        /// </summary>
+        FirstRearrange,
+
+        /// <summary>
+        /// Second pile selection phase
+        /// </summary>
+        SecondPileSelection,
+
+        /// <summary>
+        /// Final rearrangement
+        /// </summary>
+        SecondRearrange,
+
+        /// <summary>
+        /// Revealing the selected card
+        /// </summary>
+        Revealing,
+
+        /// <summary>
+        /// Trick complete - show result
+        /// </summary>
+        Complete
+    }
+}
+```
+
+**Why This Approach?**
+- Each state represents a distinct phase of the trick
+- Makes the game loop clear and maintainable
+- Easy to add animations and UI changes per state
+- Follows the same pattern as BlackjackGameState
+
+---
+
+## Part 3: Create the Player Class
+
+### Step 3.1: Define MagicTrickPlayer
+
+Our player needs minimal state - just tracking which pile they selected.
+
+**Create:** `Core/Game/MagicTrick/Players/MagicTrickPlayer.cs`
+
+```csharp
+using CardsFramework.Players;
+using CardsFramework.Game;
+
+namespace CardsFramework.MagicTrick
+{
+    /// <summary>
+    /// Represents the spectator in the magic trick
+    /// </summary>
+    public class MagicTrickPlayer : Player
+    {
+        /// <summary>
+        /// Which pile (0, 1, or 2) the player selected in the current round
+        /// </summary>
+        public int SelectedPile { get; set; }
+
+        /// <summary>
+        /// Whether the player has made their selection
+        /// </summary>
+        public bool HasSelected { get; set; }
+
+        /// <summary>
+        /// Creates a new magic trick player
+        /// </summary>
+        /// <param name="name">Player's name</param>
+        /// <param name="game">Reference to the game instance</param>
+        public MagicTrickPlayer(string name, CardsGame game)
+            : base(name, game)
+        {
+            SelectedPile = -1;
+            HasSelected = false;
+        }
+
+        /// <summary>
+        /// Resets the player state for a new trick
+        /// </summary>
+        public void ResetSelection()
+        {
+            SelectedPile = -1;
+            HasSelected = false;
+        }
+    }
+}
+```
+
+**Key Points:**
+- Extends `Player` base class (gives us Name, Game, Hand)
+- `SelectedPile`: Tracks which of the 3 piles contains their card (0=left, 1=middle, 2=right)
+- `HasSelected`: Simple flag to prevent double-selection
+- `ResetSelection()`: Prepares for a new trick
+
+---
+
+## Part 4: Create Game Rules
+
+### Step 4.1: Card Selection Rule
+
+This rule checks if the player has made their pile selection and triggers the next phase.
+
+**Create:** `Core/Game/MagicTrick/Rules/CardSelectionRule.cs`
+
+```csharp
+using System;
+using CardsFramework.Rules;
+
+namespace CardsFramework.MagicTrick
+{
+    /// <summary>
+    /// Event arguments for card selection events
+    /// </summary>
+    public class CardSelectionEventArgs : EventArgs
+    {
+        public MagicTrickPlayer Player { get; set; }
+        public int SelectedPile { get; set; }
+    }
+
+    /// <summary>
+    /// Rule that fires when the player selects a pile
+    /// </summary>
+    public class CardSelectionRule : GameRule
+    {
+        private readonly MagicTrickPlayer player;
+        private bool previousHasSelected;
+
+        public CardSelectionRule(MagicTrickPlayer player)
+        {
+            this.player = player;
+            this.previousHasSelected = false;
+        }
+
+        /// <summary>
+        /// Checks if the player has made a new selection
+        /// </summary>
+        public override void Check()
+        {
+            // Only fire event when selection changes from false to true
+            if (player.HasSelected && !previousHasSelected)
+            {
+                previousHasSelected = true;
+
+                // Fire the event
+                FireRuleMatch(new CardSelectionEventArgs
+                {
+                    Player = player,
+                    SelectedPile = player.SelectedPile
+                });
+            }
+
+            // Reset tracking when starting a new selection phase
+            if (!player.HasSelected)
+            {
+                previousHasSelected = false;
+            }
+        }
+    }
+}
+```
+
+**How It Works:**
+- Inherits from `GameRule` base class
+- Tracks previous state to detect state changes
+- Fires `RuleMatch` event only when selection transitions from false → true
+- Resets when player starts a new selection phase
+- Event includes which pile was selected
+
+### Step 4.2: Reveal Rule
+
+This rule determines when we're ready to reveal the selected card.
+
+**Create:** `Core/Game/MagicTrick/Rules/RevealRule.cs`
+
+```csharp
+using System;
+using CardsFramework.Rules;
+using CardsFramework.Cards;
+
+namespace CardsFramework.MagicTrick
+{
+    /// <summary>
+    /// Event arguments for reveal events
+    /// </summary>
+    public class RevealEventArgs : EventArgs
+    {
+        public TraditionalCard RevealedCard { get; set; }
+    }
+
+    /// <summary>
+    /// Rule that fires when it's time to reveal the selected card
+    /// </summary>
+    public class RevealRule : GameRule
+    {
+        private readonly MagicTrickCardGame game;
+        private bool hasRevealed;
+
+        public RevealRule(MagicTrickCardGame game)
+        {
+            this.game = game;
+            this.hasRevealed = false;
+        }
+
+        /// <summary>
+        /// Checks if we're in the revealing state
+        /// </summary>
+        public override void Check()
+        {
+            if (game.State == MagicTrickGameState.Revealing && !hasRevealed)
+            {
+                hasRevealed = true;
+
+                // The 5th card (index 4) is always the selected card after two rounds
+                if (game.TableCards.Count >= 5)
+                {
+                    FireRuleMatch(new RevealEventArgs
+                    {
+                        RevealedCard = game.TableCards[4]
+                    });
+                }
+            }
+
+            // Reset for next trick
+            if (game.State == MagicTrickGameState.Dealing)
+            {
+                hasRevealed = false;
+            }
+        }
+    }
+}
+```
+
+**Key Points:**
+- Fires when game enters `Revealing` state
+- The magic happens here: `TableCards[4]` is always at index 4 after two rearrangements
+- One-shot rule: Only fires once until reset
+- Passes the revealed card to event handlers
+
+---
+
+## Part 5: Create the Main Game Class
+
+### Step 5.1: MagicTrickCardGame - Part 1 (Fields and Constructor)
+
+This is the core of our implementation. We'll build it in sections.
+
+**Create:** `Core/Game/MagicTrick/Game/MagicTrickCardGame.cs`
+
+```csharp
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using CardsFramework.Cards;
+using CardsFramework.Game;
+using CardsFramework.Players;
+using CardsFramework.UI;
+using CardsFramework.Rules;
+using GameStateManagement;
+
+namespace CardsFramework.MagicTrick
+{
+    /// <summary>
+    /// Main game class for the 9-card magic trick
+    /// </summary>
+    public class MagicTrickCardGame : CardsGame
+    {
+        #region Fields
+
+        // Game state
+        private MagicTrickGameState currentState;
+        public MagicTrickGameState State
+        {
+            get { return currentState; }
+            set { currentState = value; }
+        }
+
+        // The 9 cards currently on the table
+        public List<TraditionalCard> TableCards { get; private set; }
+
+        // Animated components for displaying cards in 3x3 grid
+        private List<AnimatedCardsGameComponent> animatedCards;
+
+        // UI Components
+        private Button buttonPile1;
+        private Button buttonPile2;
+        private Button buttonPile3;
+        private Button buttonContinue;
+        private Button buttonNewTrick;
+
+        // The player (spectator)
+        private MagicTrickPlayer player;
+
+        // Game rules
+        private CardSelectionRule cardSelectionRule;
+        private RevealRule revealRule;
+
+        // Display text for instructions
+        private string instructionText;
+        private Vector2 instructionPosition;
+        private SpriteFont instructionFont;
+
+        // Card layout constants
+        private const int CardsPerRow = 3;
+        private const int TotalCards = 9;
+        private const float CardSpacingX = 120f;
+        private const float CardSpacingY = 160f;
+        private Vector2 gridStartPosition;
+
+        #endregion
+
+        #region Initialization
+
+        /// <summary>
+        /// Creates a new magic trick game
+        /// </summary>
+        /// <param name="gameTable">The game table for layout</param>
+        public MagicTrickCardGame(GameTable gameTable)
+            : base(
+                decks: 1,                           // Only need 1 deck
+                jokersInDeck: 0,                    // No jokers
+                suits: CardSuit.AllSuits,           // All suits available
+                cardValues: CardValue.NonJokers,    // Standard cards only
+                minimumPlayers: 1,                  // Single player
+                maximumPlayers: 1,                  // Single player
+                gameTable: gameTable,
+                theme: "Default")
+        {
+            TableCards = new List<TraditionalCard>();
+            animatedCards = new List<AnimatedCardsGameComponent>();
+            instructionText = "";
+        }
+
+        /// <summary>
+        /// Initializes the game components
+        /// </summary>
+        public override void Initialize()
+        {
+            base.Initialize();
+
+            // Calculate grid start position (centered on screen)
+            int screenWidth = GraphicsDevice.Viewport.Width;
+            int screenHeight = GraphicsDevice.Viewport.Height;
+
+            gridStartPosition = new Vector2(
+                (screenWidth - (CardSpacingX * (CardsPerRow - 1))) / 2 - 50,
+                100
+            );
+
+            instructionPosition = new Vector2(screenWidth / 2, 50);
+
+            // Start with dealing state
+            currentState = MagicTrickGameState.Dealing;
+        }
+
+        /// <summary>
+        /// Loads content and creates UI components
+        /// </summary>
+        public override void LoadContent()
+        {
+            base.LoadContent();
+
+            // Load instruction font
+            instructionFont = Game.Content.Load<SpriteFont>(@"Fonts\Regular");
+
+            // Create buttons for pile selection
+            Texture2D buttonTexture = Game.Content.Load<Texture2D>(@"Images\UI\ButtonRegular");
+            Texture2D buttonPressedTexture = Game.Content.Load<Texture2D>(@"Images\UI\ButtonPressed");
+
+            int screenWidth = GraphicsDevice.Viewport.Width;
+            int screenHeight = GraphicsDevice.Viewport.Height;
+            int buttonWidth = 200;
+            int buttonHeight = 60;
+            int buttonY = screenHeight - 250;
+
+            // Pile 1 button (left)
+            buttonPile1 = new Button(
+                new Rectangle((screenWidth / 2) - buttonWidth - 120, buttonY, buttonWidth, buttonHeight),
+                buttonTexture,
+                buttonPressedTexture,
+                instructionFont,
+                "Left Pile",
+                Color.White
+            );
+            buttonPile1.Click += ButtonPile1_Click;
+            buttonPile1.Visible = false;
+            Game.Components.Add(buttonPile1);
+
+            // Pile 2 button (middle)
+            buttonPile2 = new Button(
+                new Rectangle((screenWidth / 2) - buttonWidth / 2, buttonY, buttonWidth, buttonHeight),
+                buttonTexture,
+                buttonPressedTexture,
+                instructionFont,
+                "Middle Pile",
+                Color.White
+            );
+            buttonPile2.Click += ButtonPile2_Click;
+            buttonPile2.Visible = false;
+            Game.Components.Add(buttonPile2);
+
+            // Pile 3 button (right)
+            buttonPile3 = new Button(
+                new Rectangle((screenWidth / 2) + 120, buttonY, buttonWidth, buttonHeight),
+                buttonTexture,
+                buttonPressedTexture,
+                instructionFont,
+                "Right Pile",
+                Color.White
+            );
+            buttonPile3.Click += ButtonPile3_Click;
+            buttonPile3.Visible = false;
+            Game.Components.Add(buttonPile3);
+
+            // Continue button (for advancing through states)
+            buttonContinue = new Button(
+                new Rectangle((screenWidth / 2) - buttonWidth / 2, buttonY + 80, buttonWidth, buttonHeight),
+                buttonTexture,
+                buttonPressedTexture,
+                instructionFont,
+                "Continue",
+                Color.LightGreen
+            );
+            buttonContinue.Click += ButtonContinue_Click;
+            buttonContinue.Visible = false;
+            Game.Components.Add(buttonContinue);
+
+            // New Trick button (restart)
+            buttonNewTrick = new Button(
+                new Rectangle((screenWidth / 2) - buttonWidth / 2, buttonY + 160, buttonWidth, buttonHeight),
+                buttonTexture,
+                buttonPressedTexture,
+                instructionFont,
+                "New Trick",
+                Color.LightBlue
+            );
+            buttonNewTrick.Click += ButtonNewTrick_Click;
+            buttonNewTrick.Visible = false;
+            Game.Components.Add(buttonNewTrick);
+        }
+
+        #endregion
+```
+
+**What We've Set Up:**
+- **Fields**: Track game state, cards, UI components
+- **Constructor**: Initializes with 1 deck, no jokers, single player
+- **Initialize**: Calculates card grid positioning
+- **LoadContent**: Creates 5 buttons (3 pile buttons, continue, new trick)
+
+### Step 5.2: MagicTrickCardGame - Part 2 (Player Management)
+
+Add these methods to handle players:
+
+```csharp
+        #region Player Management
+
+        /// <summary>
+        /// Adds a player to the game
+        /// </summary>
+        public override void AddPlayer(Player newPlayer)
+        {
+            if (!(newPlayer is MagicTrickPlayer))
+            {
+                throw new ArgumentException("Player must be of type MagicTrickPlayer");
+            }
+
+            base.AddPlayer(newPlayer);
+            player = (MagicTrickPlayer)newPlayer;
+
+            // Initialize rules now that we have a player
+            cardSelectionRule = new CardSelectionRule(player);
+            cardSelectionRule.RuleMatch += CardSelectionRule_RuleMatch;
+            Rules.Add(cardSelectionRule);
+
+            revealRule = new RevealRule(this);
+            revealRule.RuleMatch += RevealRule_RuleMatch;
+            Rules.Add(revealRule);
+        }
+
+        /// <summary>
+        /// Gets the current player
+        /// </summary>
+        public override Player GetCurrentPlayer()
+        {
+            return player;
+        }
+
+        #endregion
+```
+
+**Key Points:**
+- Validates player type
+- Initializes rules after player is added (rules need player reference)
+- Wires up rule event handlers
+
+### Step 5.3: MagicTrickCardGame - Part 3 (Dealing Cards)
+
+```csharp
+        #region Card Management
+
+        /// <summary>
+        /// Deals 9 cards into a 3x3 grid
+        /// </summary>
+        public override void Deal()
+        {
+            // Clear previous cards
+            ClearTable();
+
+            // Shuffle the deck
+            dealer.Shuffle();
+
+            // Deal 9 cards to the table
+            for (int i = 0; i < TotalCards; i++)
+            {
+                TraditionalCard card = dealer[i];
+                TableCards.Add(card);
+
+                // Create animated component for this card
+                AnimatedCardsGameComponent animatedCard = new AnimatedCardsGameComponent(card, Game);
+                animatedCard.LoadContent();
+
+                // Calculate position in 3x3 grid
+                int row = i / CardsPerRow;
+                int col = i % CardsPerRow;
+
+                Vector2 targetPosition = gridStartPosition + new Vector2(
+                    col * CardSpacingX,
+                    row * CardSpacingY
+                );
+
+                animatedCard.CurrentPosition = targetPosition;
+                animatedCard.IsFaceDown = false; // Show cards face-up
+
+                animatedCards.Add(animatedCard);
+                Game.Components.Add(animatedCard);
+            }
+        }
+
+        /// <summary>
+        /// Clears all cards from the table
+        /// </summary>
+        private void ClearTable()
+        {
+            // Remove animated components
+            foreach (var animatedCard in animatedCards)
+            {
+                Game.Components.Remove(animatedCard);
+            }
+
+            animatedCards.Clear();
+            TableCards.Clear();
+        }
+
+        /// <summary>
+        /// Rearranges cards after pile selection
+        /// </summary>
+        /// <param name="selectedPile">Which pile (0, 1, 2) contains the player's card</param>
+        private void RearrangeCards(int selectedPile)
+        {
+            // Collect cards by column (pile)
+            List<TraditionalCard> pile1 = new List<TraditionalCard>(); // Left column
+            List<TraditionalCard> pile2 = new List<TraditionalCard>(); // Middle column
+            List<TraditionalCard> pile3 = new List<TraditionalCard>(); // Right column
+
+            // Group cards into columns
+            for (int i = 0; i < TableCards.Count; i++)
+            {
+                int col = i % CardsPerRow;
+
+                if (col == 0)
+                    pile1.Add(TableCards[i]);
+                else if (col == 1)
+                    pile2.Add(TableCards[i]);
+                else
+                    pile3.Add(TableCards[i]);
+            }
+
+            // Rearrange: Put selected pile in the middle
+            // This is the key to the trick!
+            List<TraditionalCard> rearranged = new List<TraditionalCard>();
+
+            if (selectedPile == 0) // Left pile selected
+            {
+                rearranged.AddRange(pile2); // Other pile first
+                rearranged.AddRange(pile1); // Selected pile in middle
+                rearranged.AddRange(pile3); // Other pile last
+            }
+            else if (selectedPile == 1) // Middle pile selected
+            {
+                rearranged.AddRange(pile1);
+                rearranged.AddRange(pile2); // Already in middle
+                rearranged.AddRange(pile3);
+            }
+            else // Right pile selected
+            {
+                rearranged.AddRange(pile1);
+                rearranged.AddRange(pile3); // Selected pile in middle
+                rearranged.AddRange(pile2);
+            }
+
+            // Update table cards
+            TableCards.Clear();
+            TableCards.AddRange(rearranged);
+
+            // Redeal cards in new order
+            RedealCards();
+        }
+
+        /// <summary>
+        /// Redeals cards to table in their current order
+        /// </summary>
+        private void RedealCards()
+        {
+            // Remove old animated components
+            foreach (var animatedCard in animatedCards)
+            {
+                Game.Components.Remove(animatedCard);
+            }
+            animatedCards.Clear();
+
+            // Create new animated components in new positions
+            for (int i = 0; i < TableCards.Count; i++)
+            {
+                AnimatedCardsGameComponent animatedCard = new AnimatedCardsGameComponent(TableCards[i], Game);
+                animatedCard.LoadContent();
+
+                // Calculate position in 3x3 grid
+                int row = i / CardsPerRow;
+                int col = i % CardsPerRow;
+
+                Vector2 targetPosition = gridStartPosition + new Vector2(
+                    col * CardSpacingX,
+                    row * CardSpacingY
+                );
+
+                animatedCard.CurrentPosition = targetPosition;
+                animatedCard.IsFaceDown = false;
+
+                animatedCards.Add(animatedCard);
+                Game.Components.Add(animatedCard);
+            }
+        }
+
+        #endregion
+```
+
+**The Magic Explained:**
+- `RearrangeCards()` is where the trick happens!
+- We collect cards by **column** (each column = a pile)
+- We place the selected pile in the **middle** of the new arrangement
+- After doing this **twice**, the selected card mathematically ends up at position 4 (center)
+
+### Step 5.4: MagicTrickCardGame - Part 4 (Game Flow)
+
+```csharp
+        #region Game Flow
+
+        /// <summary>
+        /// Starts the trick
+        /// </summary>
+        public override void StartPlaying()
+        {
+            currentState = MagicTrickGameState.Dealing;
+            Deal();
+
+            // Move to selection phase after brief delay
+            System.Threading.Tasks.Task.Delay(1000).ContinueWith(t =>
+            {
+                currentState = MagicTrickGameState.PlayerSelecting;
+            });
+        }
+
+        /// <summary>
+        /// Updates the game state each frame
+        /// </summary>
+        public override void Update(GameTime gameTime)
+        {
+            base.Update(gameTime);
+
+            // Check rules
+            CheckRules();
+
+            // Update UI based on current state
+            UpdateUIForState();
+        }
+
+        /// <summary>
+        /// Updates button visibility and instruction text based on state
+        /// </summary>
+        private void UpdateUIForState()
+        {
+            // Hide all buttons first
+            buttonPile1.Visible = false;
+            buttonPile2.Visible = false;
+            buttonPile3.Visible = false;
+            buttonContinue.Visible = false;
+            buttonNewTrick.Visible = false;
+
+            switch (currentState)
+            {
+                case MagicTrickGameState.Dealing:
+                    instructionText = "Watch carefully as the cards are dealt...";
+                    break;
+
+                case MagicTrickGameState.PlayerSelecting:
+                    instructionText = "Mentally select one card. Remember it!\nClick Continue when ready.";
+                    buttonContinue.Visible = true;
+                    break;
+
+                case MagicTrickGameState.FirstPileSelection:
+                    instructionText = "Which pile contains your card?\n(Cards in each column are a pile)";
+                    buttonPile1.Visible = true;
+                    buttonPile2.Visible = true;
+                    buttonPile3.Visible = true;
+                    break;
+
+                case MagicTrickGameState.FirstRearrange:
+                    instructionText = "Watch as I rearrange the cards...";
+                    break;
+
+                case MagicTrickGameState.SecondPileSelection:
+                    instructionText = "Now, which pile contains your card?";
+                    buttonPile1.Visible = true;
+                    buttonPile2.Visible = true;
+                    buttonPile3.Visible = true;
+                    break;
+
+                case MagicTrickGameState.SecondRearrange:
+                    instructionText = "One more rearrangement...";
+                    break;
+
+                case MagicTrickGameState.Revealing:
+                    instructionText = "Your card is...";
+                    break;
+
+                case MagicTrickGameState.Complete:
+                    // Get the revealed card for display
+                    if (TableCards.Count >= 5)
+                    {
+                        TraditionalCard revealedCard = TableCards[4];
+                        instructionText = $"Your card is the {revealedCard.Value} of {revealedCard.Type}!\n\nDid I guess correctly?";
+                    }
+                    buttonNewTrick.Visible = true;
+                    break;
+            }
+        }
+
+        #endregion
+```
+
+**State Machine Logic:**
+- Each state has specific UI configuration
+- Instructions guide the player through the trick
+- Buttons appear/hide based on what's needed
+- The state flows: Dealing → Selecting → FirstPile → Rearrange → SecondPile → Rearrange → Reveal → Complete
+
+### Step 5.5: MagicTrickCardGame - Part 5 (Event Handlers)
+
+```csharp
+        #region Event Handlers
+
+        /// <summary>
+        /// Handles pile 1 (left) button click
+        /// </summary>
+        private void ButtonPile1_Click(object sender, EventArgs e)
+        {
+            SelectPile(0);
+        }
+
+        /// <summary>
+        /// Handles pile 2 (middle) button click
+        /// </summary>
+        private void ButtonPile2_Click(object sender, EventArgs e)
+        {
+            SelectPile(1);
+        }
+
+        /// <summary>
+        /// Handles pile 3 (right) button click
+        /// </summary>
+        private void ButtonPile3_Click(object sender, EventArgs e)
+        {
+            SelectPile(2);
+        }
+
+        /// <summary>
+        /// Handles pile selection
+        /// </summary>
+        private void SelectPile(int pileIndex)
+        {
+            player.SelectedPile = pileIndex;
+            player.HasSelected = true;
+
+            // Rule will detect this change and fire event
+        }
+
+        /// <summary>
+        /// Handles continue button click
+        /// </summary>
+        private void ButtonContinue_Click(object sender, EventArgs e)
+        {
+            if (currentState == MagicTrickGameState.PlayerSelecting)
+            {
+                currentState = MagicTrickGameState.FirstPileSelection;
+            }
+        }
+
+        /// <summary>
+        /// Handles new trick button click
+        /// </summary>
+        private void ButtonNewTrick_Click(object sender, EventArgs e)
+        {
+            player.ResetSelection();
+            StartPlaying();
+        }
+
+        /// <summary>
+        /// Handles card selection rule match
+        /// </summary>
+        private void CardSelectionRule_RuleMatch(object sender, EventArgs e)
+        {
+            CardSelectionEventArgs args = (CardSelectionEventArgs)e;
+
+            if (currentState == MagicTrickGameState.FirstPileSelection)
+            {
+                // First selection made
+                currentState = MagicTrickGameState.FirstRearrange;
+
+                // Rearrange cards with selected pile in middle
+                RearrangeCards(args.SelectedPile);
+
+                // Reset for next selection
+                player.ResetSelection();
+
+                // Move to second selection after delay
+                System.Threading.Tasks.Task.Delay(1500).ContinueWith(t =>
+                {
+                    currentState = MagicTrickGameState.SecondPileSelection;
+                });
+            }
+            else if (currentState == MagicTrickGameState.SecondPileSelection)
+            {
+                // Second selection made
+                currentState = MagicTrickGameState.SecondRearrange;
+
+                // Rearrange again
+                RearrangeCards(args.SelectedPile);
+
+                // Move to reveal after delay
+                System.Threading.Tasks.Task.Delay(1500).ContinueWith(t =>
+                {
+                    currentState = MagicTrickGameState.Revealing;
+                });
+            }
+        }
+
+        /// <summary>
+        /// Handles reveal rule match
+        /// </summary>
+        private void RevealRule_RuleMatch(object sender, EventArgs e)
+        {
+            RevealEventArgs args = (RevealEventArgs)e;
+
+            // Highlight the revealed card (center card)
+            if (animatedCards.Count >= 5)
+            {
+                // You could add a scale animation or glow effect here
+                // For simplicity, we'll just move to complete state
+            }
+
+            // Move to complete state
+            System.Threading.Tasks.Task.Delay(2000).ContinueWith(t =>
+            {
+                currentState = MagicTrickGameState.Complete;
+            });
+        }
+
+        #endregion
+```
+
+**Event Flow:**
+1. Player clicks pile button → `SelectPile()` → Updates player state
+2. `CardSelectionRule` detects state change → Fires `RuleMatch`
+3. `CardSelectionRule_RuleMatch()` → Rearranges cards → Advances state
+4. After 2 selections → `RevealRule` fires → Shows result
+
+### Step 5.6: MagicTrickCardGame - Part 6 (Drawing and Utilities)
+
+```csharp
+        #region Drawing
+
+        /// <summary>
+        /// Draws the game
+        /// </summary>
+        public override void Draw(GameTime gameTime)
+        {
+            base.Draw(gameTime);
+
+            // Draw instruction text
+            if (!string.IsNullOrEmpty(instructionText) && instructionFont != null)
+            {
+                SpriteBatch spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
+
+                if (spriteBatch != null)
+                {
+                    // Measure text for centering
+                    Vector2 textSize = instructionFont.MeasureString(instructionText);
+                    Vector2 centeredPosition = new Vector2(
+                        instructionPosition.X - textSize.X / 2,
+                        instructionPosition.Y
+                    );
+
+                    // Draw text with shadow for readability
+                    spriteBatch.DrawString(instructionFont, instructionText,
+                        centeredPosition + new Vector2(2, 2), Color.Black);
+                    spriteBatch.DrawString(instructionFont, instructionText,
+                        centeredPosition, Color.White);
+                }
+            }
+        }
+
+        #endregion
+
+        #region Utilities
+
+        /// <summary>
+        /// Gets the value of a card (not used in magic trick, but required by base class)
+        /// </summary>
+        public override int CardValue(TraditionalCard card)
+        {
+            // Magic trick doesn't need card values, but we implement for completeness
+            switch (card.Value)
+            {
+                case CardValue.Ace:
+                    return 1;
+                case CardValue.Two:
+                    return 2;
+                case CardValue.Three:
+                    return 3;
+                case CardValue.Four:
+                    return 4;
+                case CardValue.Five:
+                    return 5;
+                case CardValue.Six:
+                    return 6;
+                case CardValue.Seven:
+                    return 7;
+                case CardValue.Eight:
+                    return 8;
+                case CardValue.Nine:
+                    return 9;
+                case CardValue.Ten:
+                case CardValue.Jack:
+                case CardValue.Queen:
+                case CardValue.King:
+                    return 10;
+                default:
+                    return 0;
+            }
+        }
+
+        #endregion
+    }
+}
+```
+
+**Drawing:**
+- Centers instruction text on screen
+- Adds shadow for readability
+- Cards draw themselves via `AnimatedCardsGameComponent`
+
+---
+
+## Part 6: Integrate with Screen System
+
+### Step 6.1: Create a Screen for the Magic Trick
+
+**Create:** `Core/Game/Screens/MagicTrickGameplayScreen.cs`
+
+```csharp
+using System;
+using Microsoft.Xna.Framework;
+using CardsFramework.MagicTrick;
+using CardsFramework.UI;
+using GameStateManagement;
+
+namespace CardsStarterKit
+{
+    /// <summary>
+    /// Screen that hosts the magic trick game
+    /// </summary>
+    public class MagicTrickGameplayScreen : GameScreen
+    {
+        private MagicTrickCardGame magicTrickGame;
+
+        public MagicTrickGameplayScreen()
+        {
+            EnabledGestures = Microsoft.Xna.Framework.Input.Touch.GestureType.Tap;
+        }
+
+        /// <summary>
+        /// Loads content and initializes the game
+        /// </summary>
+        public override void LoadContent()
+        {
+            base.LoadContent();
+
+            // Create game table
+            GameTable gameTable = new GameTable(ScreenManager.Game, 1); // 1 player position
+
+            // Create the magic trick game
+            magicTrickGame = new MagicTrickCardGame(gameTable);
+            ScreenManager.Game.Components.Add(magicTrickGame);
+            magicTrickGame.Initialize();
+            magicTrickGame.LoadContent();
+
+            // Add the player
+            MagicTrickPlayer player = new MagicTrickPlayer("You", magicTrickGame);
+            magicTrickGame.AddPlayer(player);
+
+            // Start the trick
+            magicTrickGame.StartPlaying();
+        }
+
+        /// <summary>
+        /// Handles input
+        /// </summary>
+        public override void HandleInput(InputState input)
+        {
+            base.HandleInput(input);
+
+            // Handle back button / escape to return to menu
+            if (input.IsPauseGame(null))
+            {
+                ScreenManager.AddScreen(new PauseScreen(), null);
+            }
+        }
+
+        /// <summary>
+        /// Updates the screen
+        /// </summary>
+        public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
+        {
+            base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
+        }
+
+        /// <summary>
+        /// Cleanup when exiting
+        /// </summary>
+        public override void UnloadContent()
+        {
+            if (magicTrickGame != null)
+            {
+                ScreenManager.Game.Components.Remove(magicTrickGame);
+            }
+
+            base.UnloadContent();
+        }
+    }
+}
+```
+
+**Integration:**
+- Creates `GameTable` for layout
+- Instantiates `MagicTrickCardGame`
+- Adds single player
+- Starts the trick
+- Handles pause/back navigation
+
+### Step 6.2: Add Menu Entry
+
+**Modify:** `Core/Game/Screens/MainMenuScreen.cs`
+
+Find the constructor where menu entries are added and add:
+
+```csharp
+// Add magic trick menu entry
+MenuEntry magicTrickMenuEntry = new MenuEntry("Magic Trick");
+magicTrickMenuEntry.Selected += MagicTrickMenuEntrySelected;
+menuEntries.Add(magicTrickMenuEntry);
+```
+
+Then add the event handler method:
+
+```csharp
+/// <summary>
+/// Handles Magic Trick menu selection
+/// </summary>
+private void MagicTrickMenuEntrySelected(object sender, EventArgs e)
+{
+    ScreenManager.AddScreen(new MagicTrickGameplayScreen(), null);
+}
+```
+
+---
+
+## Part 7: Testing Your Magic Trick
+
+### Step 7.1: Build and Run
+
+```bash
+dotnet build
+dotnet run --project Platforms/Desktop/CardsStarterKit.Desktop.csproj
+```
+
+### Step 7.2: Test the Flow
+
+1. **Launch:** Select "Magic Trick" from main menu
+2. **Deal:** 9 cards appear in a 3x3 grid
+3. **Select:** Mentally pick a card (e.g., 7 of Hearts in top-right)
+4. **First Question:** Click which pile (column) contains your card
+5. **Rearrange:** Watch cards rearrange
+6. **Second Question:** Again, click which pile contains your card
+7. **Reveal:** The center card should be your selected card!
+
+### Step 7.3: Verify the Math
+
+Try this test:
+- Pick the card at position [0,2] (top-right)
+- That's in pile 2 (right column)
+- After first rearrange, it should move
+- After second rearrange, it should be at index 4 (center)
+
+The math always works!
+
+---
+
+## Part 8: Enhancements (Optional)
+
+### Enhancement 1: Add Card Highlighting
+
+In the `Revealing` state, highlight the center card:
+
+```csharp
+// In RevealRule_RuleMatch method:
+if (animatedCards.Count >= 5)
+{
+    AnimatedCardsGameComponent centerCard = animatedCards[4];
+
+    // Add scale animation to make it pulse
+    ScaleGameComponentAnimation scaleAnim = new ScaleGameComponentAnimation(centerCard)
+    {
+        Duration = TimeSpan.FromSeconds(1),
+        ScaleFactor = 1.2f,
+        IsLooped = true,
+        AnimationCycles = 3
+    };
+
+    centerCard.AnimationsList.Add(scaleAnim);
+}
+```
+
+### Enhancement 2: Add Shuffle Animation
+
+Make the rearrangement more dramatic:
+
+```csharp
+// In RearrangeCards method, before RedealCards():
+// Animate cards flying off screen then back
+foreach (var animatedCard in animatedCards)
+{
+    Vector2 offscreenPos = new Vector2(-500, animatedCard.CurrentPosition.Y);
+
+    TransitionGameComponentAnimation transition = new TransitionGameComponentAnimation(
+        animatedCard,
+        offscreenPos,
+        TimeSpan.FromSeconds(0.5)
+    );
+
+    animatedCard.AnimationsList.Add(transition);
+}
+
+// Then delay RedealCards() until animation completes
+```
+
+### Enhancement 3: Add Sound Effects
+
+```csharp
+// In LoadContent:
+SoundEffect cardDealSound = Game.Content.Load<SoundEffect>(@"Sounds\CardPlace");
+SoundEffect revealSound = Game.Content.Load<SoundEffect>(@"Sounds\Reveal");
+
+// Play at appropriate times:
+cardDealSound.Play(); // When dealing
+revealSound.Play();   // When revealing
+```
+
+---
+
+## Key Takeaways
+
+### What You Learned
+
+1. **Extending CardsGame:**
+   - Override `Deal()`, `AddPlayer()`, `GetCurrentPlayer()`
+   - Use constructor to specify deck configuration
+   - Implement state machines for game flow
+
+2. **Creating Game Rules:**
+   - Inherit from `GameRule`
+   - Override `Check()` method
+   - Fire `RuleMatch` events when conditions are met
+   - Track state changes to prevent duplicate events
+
+3. **Working with Animations:**
+   - Use `AnimatedCardsGameComponent` for card rendering
+   - Position cards in grids using calculated vectors
+   - Cards automatically handle face-up/face-down rendering
+
+4. **UI Management:**
+   - Create buttons with `Button` class
+   - Show/hide buttons based on game state
+   - Use `SpriteBatch` for custom text rendering
+
+5. **Game Flow:**
+   - Use state enums to control phases
+   - Update UI based on current state
+   - Use async delays for timed transitions
+
+### The Magic Trick Pattern
+
+This trick demonstrates a key card game concept: **controlled card positioning through mathematical manipulation**. The same pattern appears in:
+
+- Card sorting algorithms
+- Deck cutting tricks
+- Dealing patterns in games like Bridge
+
+### Next Steps
+
+Try modifying the trick:
+- Use 21 cards in a 7x3 grid (requires 3 selections)
+- Add a "shuffle" phase between selections
+- Let the player choose the reveal method
+- Create different grid layouts
+
+This framework makes it easy to experiment with card logic without worrying about rendering or input handling!
+
+---
+
+## Troubleshooting
+
+### Cards Don't Appear
+- Check that `LoadContent()` was called
+- Verify card textures exist in Content/Images/Cards/
+- Ensure `IsFaceDown = false` is set
+
+### Buttons Don't Respond
+- Confirm `EnabledGestures` includes `GestureType.Tap`
+- Check button `Visible` property
+- Verify `Game.Components.Add(button)` was called
+
+### Wrong Card Revealed
+- Debug the `RearrangeCards()` logic
+- Print `TableCards` order after each rearrange
+- Verify columns are collected correctly (index % 3)
+
+### State Machine Stuck
+- Add debug output in `Update()` to track state changes
+- Check that all state transitions are implemented
+- Verify async tasks complete properly
+
+---
+
+## Complete File Checklist
+
+- [ ] `Core/Game/MagicTrick/Game/MagicTrickGameState.cs`
+- [ ] `Core/Game/MagicTrick/Game/MagicTrickCardGame.cs`
+- [ ] `Core/Game/MagicTrick/Players/MagicTrickPlayer.cs`
+- [ ] `Core/Game/MagicTrick/Rules/CardSelectionRule.cs`
+- [ ] `Core/Game/MagicTrick/Rules/RevealRule.cs`
+- [ ] `Core/Game/Screens/MagicTrickGameplayScreen.cs`
+- [ ] Modified `Core/Game/Screens/MainMenuScreen.cs`
+
+---
+
+## Conclusion
+
+Congratulations! You've built a complete interactive magic trick using the CardsStarterKit framework. You now understand:
+
+- How to extend the framework for custom card games
+- How to use the rule system for game logic
+- How to manage game state and flow
+- How to create interactive UI with buttons
+- How card animations work
+
+This foundation prepares you to build more complex card games. The next tutorial covers **Gin Rummy**, which introduces multi-phase gameplay, meld detection, scoring, and AI opponents.
+
+**Happy coding and enjoy amazing your friends with your magic trick!**

+ 2345 - 0
CardsStarterKit/Tutorials/02_GinRummy_Implementation.md

@@ -0,0 +1,2345 @@
+# Tutorial: Implementing Gin Rummy
+
+## Overview
+
+In this tutorial, you'll build a complete Gin Rummy card game using the CardsStarterKit framework. This is a more complex implementation than the magic trick, teaching you:
+
+- Managing larger hands (10 cards vs 2)
+- Implementing meld detection (sets and runs)
+- Creating intermediate AI opponents
+- Turn-based gameplay with draw/discard mechanics
+- Calculating deadwood and scoring
+- Multi-player support
+- Advanced UI for card organization
+
+**Target Audience:** MonoGame developers new to card games
+
+**Estimated Time:** 6-8 hours
+
+**Difficulty:** Intermediate
+
+---
+
+## What is Gin Rummy?
+
+### Game Rules Summary
+
+**Objective:** Form melds (sets/runs) and minimize deadwood (unmatched cards).
+
+**Setup:**
+- 2-4 players (we'll support up to 4)
+- Each player receives 10 cards
+- Remaining cards form the stock pile
+- Top card of stock is flipped to start discard pile
+
+**Gameplay Flow:**
+1. **Draw Phase:** Player draws from stock or discard pile
+2. **Discard Phase:** Player discards one card to discard pile
+3. **Optional Knock:** If deadwood ≤ 10 points, player can knock
+4. **Gin:** If deadwood = 0, player declares "Gin!"
+
+**Melds:**
+- **Set:** 3-4 cards of same rank (e.g., 7♥ 7♠ 7♣)
+- **Run:** 3+ consecutive cards of same suit (e.g., 4♠ 5♠ 6♠)
+
+**Deadwood Points:**
+- Ace = 1 point
+- 2-10 = face value
+- J, Q, K = 10 points
+
+**Scoring (Single Round - Tutorial Focus):**
+- **Gin:** Knocker gets opponent's deadwood + 25 bonus
+- **Knock:** Knocker gets difference in deadwood
+- **Undercut:** If opponent has less/equal deadwood, opponent gets difference + 25 bonus
+
+### What We'll Build
+
+This tutorial focuses on **single-round gameplay** to keep it manageable. At the end, we'll discuss extending it to full match scoring (first to 100 points).
+
+---
+
+## Part 1: Project Structure
+
+### Step 1.1: Create Directory Structure
+
+```bash
+mkdir -p Core/Game/GinRummy/Game
+mkdir -p Core/Game/GinRummy/Players
+mkdir -p Core/Game/GinRummy/Rules
+mkdir -p Core/Game/GinRummy/UI
+mkdir -p Core/Game/GinRummy/AI
+```
+
+Your structure will look like:
+
+```
+Core/Game/GinRummy/
+├── Game/
+│   ├── GinRummyCardGame.cs
+│   ├── GinRummyGameState.cs
+│   ├── Meld.cs
+│   └── MeldDetector.cs
+├── Players/
+│   ├── GinRummyPlayer.cs
+│   └── GinRummyAIPlayer.cs
+├── Rules/
+│   ├── KnockRule.cs
+│   ├── GinRule.cs
+│   └── TurnCompleteRule.cs
+├── UI/
+│   └── HandOrganizer.cs
+└── AI/
+    └── GinRummyAI.cs
+```
+
+---
+
+## Part 2: Core Data Structures
+
+### Step 2.1: Define Game State
+
+**Create:** `Core/Game/GinRummy/Game/GinRummyGameState.cs`
+
+```csharp
+namespace CardsFramework.GinRummy
+{
+    /// <summary>
+    /// States of a Gin Rummy game
+    /// </summary>
+    public enum GinRummyGameState
+    {
+        /// <summary>
+        /// Dealing initial hands
+        /// </summary>
+        Dealing,
+
+        /// <summary>
+        /// Player's turn - drawing phase
+        /// </summary>
+        Drawing,
+
+        /// <summary>
+        /// Player's turn - discarding phase
+        /// </summary>
+        Discarding,
+
+        /// <summary>
+        /// Player knocked - showing hands
+        /// </summary>
+        Knocked,
+
+        /// <summary>
+        /// Player got Gin - showing hands
+        /// </summary>
+        Gin,
+
+        /// <summary>
+        /// Calculating scores
+        /// </summary>
+        Scoring,
+
+        /// <summary>
+        /// Round complete - showing results
+        /// </summary>
+        RoundEnd,
+
+        /// <summary>
+        /// Waiting between turns
+        /// </summary>
+        Waiting
+    }
+}
+```
+
+### Step 2.2: Define Meld Structure
+
+**Create:** `Core/Game/GinRummy/Game/Meld.cs`
+
+```csharp
+using System.Collections.Generic;
+using System.Linq;
+using CardsFramework.Cards;
+
+namespace CardsFramework.GinRummy
+{
+    /// <summary>
+    /// Types of melds in Gin Rummy
+    /// </summary>
+    public enum MeldType
+    {
+        /// <summary>
+        /// 3+ cards of same rank (e.g., 7♥ 7♠ 7♣)
+        /// </summary>
+        Set,
+
+        /// <summary>
+        /// 3+ consecutive cards of same suit (e.g., 4♠ 5♠ 6♠)
+        /// </summary>
+        Run
+    }
+
+    /// <summary>
+    /// Represents a meld (set or run) of cards
+    /// </summary>
+    public class Meld
+    {
+        /// <summary>
+        /// Type of this meld
+        /// </summary>
+        public MeldType Type { get; set; }
+
+        /// <summary>
+        /// Cards in this meld
+        /// </summary>
+        public List<TraditionalCard> Cards { get; set; }
+
+        /// <summary>
+        /// Total point value of cards in this meld
+        /// </summary>
+        public int PointValue
+        {
+            get
+            {
+                return Cards.Sum(card => GetCardPoints(card));
+            }
+        }
+
+        public Meld()
+        {
+            Cards = new List<TraditionalCard>();
+        }
+
+        /// <summary>
+        /// Creates a meld from a list of cards
+        /// </summary>
+        public Meld(MeldType type, List<TraditionalCard> cards)
+        {
+            Type = type;
+            Cards = new List<TraditionalCard>(cards);
+        }
+
+        /// <summary>
+        /// Gets the point value of a card for deadwood calculation
+        /// </summary>
+        public static int GetCardPoints(TraditionalCard card)
+        {
+            switch (card.Value)
+            {
+                case CardValue.Ace:
+                    return 1;
+                case CardValue.Two:
+                    return 2;
+                case CardValue.Three:
+                    return 3;
+                case CardValue.Four:
+                    return 4;
+                case CardValue.Five:
+                    return 5;
+                case CardValue.Six:
+                    return 6;
+                case CardValue.Seven:
+                    return 7;
+                case CardValue.Eight:
+                    return 8;
+                case CardValue.Nine:
+                    return 9;
+                case CardValue.Ten:
+                case CardValue.Jack:
+                case CardValue.Queen:
+                case CardValue.King:
+                    return 10;
+                default:
+                    return 0;
+            }
+        }
+
+        /// <summary>
+        /// Checks if this meld is valid
+        /// </summary>
+        public bool IsValid()
+        {
+            if (Cards.Count < 3)
+                return false;
+
+            if (Type == MeldType.Set)
+                return IsValidSet();
+            else
+                return IsValidRun();
+        }
+
+        /// <summary>
+        /// Validates a set (same rank)
+        /// </summary>
+        private bool IsValidSet()
+        {
+            if (Cards.Count < 3 || Cards.Count > 4)
+                return false;
+
+            CardValue firstValue = Cards[0].Value;
+
+            // All cards must have the same value
+            return Cards.All(card => card.Value == firstValue);
+        }
+
+        /// <summary>
+        /// Validates a run (consecutive same suit)
+        /// </summary>
+        private bool IsValidRun()
+        {
+            if (Cards.Count < 3)
+                return false;
+
+            CardSuit suit = Cards[0].Type;
+
+            // All cards must be same suit
+            if (!Cards.All(card => card.Type == suit))
+                return false;
+
+            // Sort cards by value
+            var sortedCards = Cards.OrderBy(card => GetCardNumericValue(card)).ToList();
+
+            // Check consecutive values
+            for (int i = 1; i < sortedCards.Count; i++)
+            {
+                int prevValue = GetCardNumericValue(sortedCards[i - 1]);
+                int currValue = GetCardNumericValue(sortedCards[i]);
+
+                if (currValue != prevValue + 1)
+                    return false;
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Gets numeric value for sorting (Ace=1, King=13)
+        /// </summary>
+        private static int GetCardNumericValue(TraditionalCard card)
+        {
+            switch (card.Value)
+            {
+                case CardValue.Ace: return 1;
+                case CardValue.Two: return 2;
+                case CardValue.Three: return 3;
+                case CardValue.Four: return 4;
+                case CardValue.Five: return 5;
+                case CardValue.Six: return 6;
+                case CardValue.Seven: return 7;
+                case CardValue.Eight: return 8;
+                case CardValue.Nine: return 9;
+                case CardValue.Ten: return 10;
+                case CardValue.Jack: return 11;
+                case CardValue.Queen: return 12;
+                case CardValue.King: return 13;
+                default: return 0;
+            }
+        }
+
+        public override string ToString()
+        {
+            string cardList = string.Join(", ", Cards.Select(c => $"{c.Value} of {c.Type}"));
+            return $"{Type}: [{cardList}]";
+        }
+    }
+}
+```
+
+**Key Features:**
+- `IsValid()`: Validates sets (same rank) and runs (consecutive, same suit)
+- `PointValue`: Sums card values for scoring
+- `GetCardPoints()`: Standard Gin Rummy point values
+
+### Step 2.3: Create Meld Detector
+
+This is the brain of Gin Rummy - finding optimal melds to minimize deadwood.
+
+**Create:** `Core/Game/GinRummy/Game/MeldDetector.cs`
+
+```csharp
+using System.Collections.Generic;
+using System.Linq;
+using CardsFramework.Cards;
+
+namespace CardsFramework.GinRummy
+{
+    /// <summary>
+    /// Detects valid melds in a hand and calculates deadwood
+    /// </summary>
+    public class MeldDetector
+    {
+        /// <summary>
+        /// Finds the optimal set of melds that minimizes deadwood
+        /// </summary>
+        public static List<Meld> FindOptimalMelds(List<TraditionalCard> cards)
+        {
+            // Find all possible melds
+            List<Meld> allPossibleMelds = FindAllPossibleMelds(cards);
+
+            // Find combination with minimum deadwood
+            return FindBestMeldCombination(cards, allPossibleMelds);
+        }
+
+        /// <summary>
+        /// Finds all possible valid melds in the hand
+        /// </summary>
+        private static List<Meld> FindAllPossibleMelds(List<TraditionalCard> cards)
+        {
+            List<Meld> melds = new List<Meld>();
+
+            // Find all sets (3-4 of same rank)
+            melds.AddRange(FindSets(cards));
+
+            // Find all runs (3+ consecutive same suit)
+            melds.AddRange(FindRuns(cards));
+
+            return melds;
+        }
+
+        /// <summary>
+        /// Finds all possible sets in the hand
+        /// </summary>
+        private static List<Meld> FindSets(List<TraditionalCard> cards)
+        {
+            List<Meld> sets = new List<Meld>();
+
+            // Group by card value
+            var grouped = cards.GroupBy(card => card.Value)
+                               .Where(g => g.Count() >= 3);
+
+            foreach (var group in grouped)
+            {
+                var cardList = group.ToList();
+
+                // Sets of 3
+                if (cardList.Count >= 3)
+                {
+                    sets.Add(new Meld(MeldType.Set, cardList.Take(3).ToList()));
+                }
+
+                // Sets of 4
+                if (cardList.Count == 4)
+                {
+                    sets.Add(new Meld(MeldType.Set, cardList));
+                }
+            }
+
+            return sets;
+        }
+
+        /// <summary>
+        /// Finds all possible runs in the hand
+        /// </summary>
+        private static List<Meld> FindRuns(List<TraditionalCard> cards)
+        {
+            List<Meld> runs = new List<Meld>();
+
+            // Group by suit
+            var bySuit = cards.GroupBy(card => card.Type);
+
+            foreach (var suitGroup in bySuit)
+            {
+                var sortedCards = suitGroup.OrderBy(card => GetCardNumericValue(card)).ToList();
+
+                // Find consecutive sequences
+                for (int i = 0; i < sortedCards.Count; i++)
+                {
+                    List<TraditionalCard> sequence = new List<TraditionalCard> { sortedCards[i] };
+
+                    // Build consecutive sequence
+                    for (int j = i + 1; j < sortedCards.Count; j++)
+                    {
+                        int prevValue = GetCardNumericValue(sortedCards[j - 1]);
+                        int currValue = GetCardNumericValue(sortedCards[j]);
+
+                        if (currValue == prevValue + 1)
+                        {
+                            sequence.Add(sortedCards[j]);
+                        }
+                        else
+                        {
+                            break;
+                        }
+                    }
+
+                    // Add all possible runs of length 3+
+                    if (sequence.Count >= 3)
+                    {
+                        // Add runs of different lengths
+                        for (int length = 3; length <= sequence.Count; length++)
+                        {
+                            runs.Add(new Meld(MeldType.Run, sequence.Take(length).ToList()));
+                        }
+                    }
+                }
+            }
+
+            return runs;
+        }
+
+        /// <summary>
+        /// Finds the best combination of non-overlapping melds
+        /// </summary>
+        private static List<Meld> FindBestMeldCombination(
+            List<TraditionalCard> allCards,
+            List<Meld> possibleMelds)
+        {
+            List<Meld> bestCombination = new List<Meld>();
+            int minDeadwood = CalculateDeadwood(allCards, new List<Meld>());
+
+            // Try all combinations of melds
+            FindBestCombinationRecursive(
+                allCards,
+                possibleMelds,
+                new List<Meld>(),
+                ref bestCombination,
+                ref minDeadwood);
+
+            return bestCombination;
+        }
+
+        /// <summary>
+        /// Recursive helper to find best non-overlapping meld combination
+        /// </summary>
+        private static void FindBestCombinationRecursive(
+            List<TraditionalCard> allCards,
+            List<Meld> possibleMelds,
+            List<Meld> currentCombination,
+            ref List<Meld> bestCombination,
+            ref int minDeadwood)
+        {
+            // Calculate deadwood for current combination
+            int deadwood = CalculateDeadwood(allCards, currentCombination);
+
+            // Update best if this is better
+            if (deadwood < minDeadwood)
+            {
+                minDeadwood = deadwood;
+                bestCombination = new List<Meld>(currentCombination);
+            }
+
+            // Try adding each remaining meld
+            for (int i = 0; i < possibleMelds.Count; i++)
+            {
+                Meld meld = possibleMelds[i];
+
+                // Check if this meld overlaps with current combination
+                if (!OverlapsWithCombination(meld, currentCombination))
+                {
+                    // Add this meld and recurse
+                    currentCombination.Add(meld);
+
+                    FindBestCombinationRecursive(
+                        allCards,
+                        possibleMelds.Skip(i + 1).ToList(),
+                        currentCombination,
+                        ref bestCombination,
+                        ref minDeadwood);
+
+                    // Backtrack
+                    currentCombination.RemoveAt(currentCombination.Count - 1);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Checks if a meld uses any cards already in the combination
+        /// </summary>
+        private static bool OverlapsWithCombination(Meld meld, List<Meld> combination)
+        {
+            var usedCards = new HashSet<TraditionalCard>();
+
+            foreach (var existingMeld in combination)
+            {
+                foreach (var card in existingMeld.Cards)
+                {
+                    usedCards.Add(card);
+                }
+            }
+
+            return meld.Cards.Any(card => usedCards.Contains(card));
+        }
+
+        /// <summary>
+        /// Calculates deadwood (unmatched cards) point value
+        /// </summary>
+        public static int CalculateDeadwood(List<TraditionalCard> allCards, List<Meld> melds)
+        {
+            // Get all cards in melds
+            var meldedCards = new HashSet<TraditionalCard>();
+            foreach (var meld in melds)
+            {
+                foreach (var card in meld.Cards)
+                {
+                    meldedCards.Add(card);
+                }
+            }
+
+            // Calculate deadwood points
+            int deadwood = 0;
+            foreach (var card in allCards)
+            {
+                if (!meldedCards.Contains(card))
+                {
+                    deadwood += Meld.GetCardPoints(card);
+                }
+            }
+
+            return deadwood;
+        }
+
+        /// <summary>
+        /// Gets numeric value for card ordering
+        /// </summary>
+        private static int GetCardNumericValue(TraditionalCard card)
+        {
+            return Meld.GetCardNumericValue(card);
+        }
+    }
+}
+```
+
+**Algorithm Explanation:**
+1. **Find All Possible Melds:** Identify every valid set and run
+2. **Try Combinations:** Recursively try non-overlapping meld combinations
+3. **Minimize Deadwood:** Keep the combination with lowest deadwood points
+4. **Backtracking:** Classic combinatorial optimization problem
+
+This is computationally intensive for 10 cards but acceptable for gameplay.
+
+---
+
+## Part 3: Player Classes
+
+### Step 3.1: GinRummyPlayer
+
+**Create:** `Core/Game/GinRummy/Players/GinRummyPlayer.cs`
+
+```csharp
+using System.Collections.Generic;
+using CardsFramework.Cards;
+using CardsFramework.Players;
+using CardsFramework.Game;
+
+namespace CardsFramework.GinRummy
+{
+    /// <summary>
+    /// Represents a player in Gin Rummy
+    /// </summary>
+    public class GinRummyPlayer : Player
+    {
+        #region Properties
+
+        /// <summary>
+        /// Current melds in player's hand
+        /// </summary>
+        public List<Meld> Melds { get; set; }
+
+        /// <summary>
+        /// Deadwood point value
+        /// </summary>
+        public int Deadwood { get; set; }
+
+        /// <summary>
+        /// Whether player has knocked
+        /// </summary>
+        public bool HasKnocked { get; set; }
+
+        /// <summary>
+        /// Whether player has gin
+        /// </summary>
+        public bool HasGin { get; set; }
+
+        /// <summary>
+        /// Score for this round
+        /// </summary>
+        public int RoundScore { get; set; }
+
+        /// <summary>
+        /// Total score across all rounds (for future multi-round support)
+        /// </summary>
+        public int TotalScore { get; set; }
+
+        /// <summary>
+        /// Whether it's currently this player's turn
+        /// </summary>
+        public bool IsMyTurn { get; set; }
+
+        /// <summary>
+        /// Whether player has drawn this turn
+        /// </summary>
+        public bool HasDrawn { get; set; }
+
+        #endregion
+
+        #region Initialization
+
+        public GinRummyPlayer(string name, CardsGame game)
+            : base(name, game)
+        {
+            Melds = new List<Meld>();
+            Deadwood = 0;
+            HasKnocked = false;
+            HasGin = false;
+            RoundScore = 0;
+            TotalScore = 0;
+            IsMyTurn = false;
+            HasDrawn = false;
+        }
+
+        #endregion
+
+        #region Methods
+
+        /// <summary>
+        /// Analyzes hand to find optimal melds and calculate deadwood
+        /// </summary>
+        public void AnalyzeHand()
+        {
+            List<TraditionalCard> handCards = new List<TraditionalCard>();
+
+            for (int i = 0; i < Hand.Count; i++)
+            {
+                handCards.Add(Hand[i]);
+            }
+
+            // Find optimal melds
+            Melds = MeldDetector.FindOptimalMelds(handCards);
+
+            // Calculate deadwood
+            Deadwood = MeldDetector.CalculateDeadwood(handCards, Melds);
+
+            // Check for Gin (no deadwood)
+            HasGin = (Deadwood == 0 && handCards.Count == 10);
+        }
+
+        /// <summary>
+        /// Checks if player can knock (deadwood <= 10)
+        /// </summary>
+        public bool CanKnock()
+        {
+            return Deadwood <= 10 && Hand.Count == 10;
+        }
+
+        /// <summary>
+        /// Resets player state for new round
+        /// </summary>
+        public void ResetForNewRound()
+        {
+            Melds.Clear();
+            Deadwood = 0;
+            HasKnocked = false;
+            HasGin = false;
+            RoundScore = 0;
+            IsMyTurn = false;
+            HasDrawn = false;
+        }
+
+        #endregion
+    }
+}
+```
+
+**Key Methods:**
+- `AnalyzeHand()`: Uses `MeldDetector` to find melds and deadwood
+- `CanKnock()`: Checks if knock is legal
+- `ResetForNewRound()`: Prepares for next round
+
+### Step 3.2: GinRummyAIPlayer
+
+**Create:** `Core/Game/GinRummy/Players/GinRummyAIPlayer.cs`
+
+```csharp
+using CardsFramework.Game;
+
+namespace CardsFramework.GinRummy
+{
+    /// <summary>
+    /// AI-controlled Gin Rummy player
+    /// </summary>
+    public class GinRummyAIPlayer : GinRummyPlayer
+    {
+        /// <summary>
+        /// AI decision-making component
+        /// </summary>
+        public GinRummyAI AI { get; private set; }
+
+        public GinRummyAIPlayer(string name, CardsGame game)
+            : base(name, game)
+        {
+            AI = new GinRummyAI(this);
+        }
+    }
+}
+```
+
+Simple wrapper - the AI logic will be in a separate class.
+
+---
+
+## Part 4: AI Implementation
+
+### Step 4.1: Intermediate AI
+
+**Create:** `Core/Game/GinRummy/AI/GinRummyAI.cs`
+
+```csharp
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using CardsFramework.Cards;
+
+namespace CardsFramework.GinRummy
+{
+    /// <summary>
+    /// Intermediate AI for Gin Rummy
+    /// </summary>
+    public class GinRummyAI
+    {
+        private GinRummyPlayer player;
+        private Random random;
+
+        public GinRummyAI(GinRummyPlayer player)
+        {
+            this.player = player;
+            this.random = new Random();
+        }
+
+        /// <summary>
+        /// Decides whether to draw from stock or discard pile
+        /// </summary>
+        public bool ShouldDrawFromDiscard(TraditionalCard topDiscard)
+        {
+            if (topDiscard == null)
+                return false;
+
+            // Simulate adding this card to hand
+            List<TraditionalCard> testHand = GetCurrentHandCards();
+            testHand.Add(topDiscard);
+
+            // Find melds with this card
+            var meldsWithDiscard = MeldDetector.FindOptimalMelds(testHand);
+            int deadwoodWithDiscard = MeldDetector.CalculateDeadwood(testHand, meldsWithDiscard);
+
+            // Compare to current deadwood
+            return deadwoodWithDiscard < player.Deadwood;
+        }
+
+        /// <summary>
+        /// Decides which card to discard
+        /// </summary>
+        public TraditionalCard SelectCardToDiscard()
+        {
+            List<TraditionalCard> handCards = GetCurrentHandCards();
+
+            // Analyze hand
+            var melds = MeldDetector.FindOptimalMelds(handCards);
+            var meldedCards = GetMeldedCards(melds);
+
+            // Get deadwood cards
+            var deadwoodCards = handCards.Where(card => !meldedCards.Contains(card)).ToList();
+
+            if (deadwoodCards.Count > 0)
+            {
+                // Discard highest value deadwood card
+                return deadwoodCards.OrderByDescending(card => Meld.GetCardPoints(card)).First();
+            }
+            else
+            {
+                // No deadwood - discard card that breaks the smallest meld
+                return SelectCardFromMelds(melds);
+            }
+        }
+
+        /// <summary>
+        /// Decides whether to knock
+        /// </summary>
+        public bool ShouldKnock()
+        {
+            // Knock if deadwood is very low
+            if (player.HasGin)
+                return true;
+
+            if (player.Deadwood <= 5)
+                return true;
+
+            // Knock with higher deadwood with some probability
+            if (player.Deadwood <= 10)
+            {
+                // 50% chance if deadwood is 6-10
+                return random.NextDouble() < 0.5;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Evaluates how useful a card is for melds
+        /// </summary>
+        private int EvaluateCardUsefulness(TraditionalCard card, List<TraditionalCard> hand)
+        {
+            int usefulness = 0;
+
+            // Check for potential sets (same rank)
+            int sameRankCount = hand.Count(c => c.Value == card.Value);
+            usefulness += sameRankCount * 10;
+
+            // Check for potential runs (consecutive same suit)
+            var sameSuit = hand.Where(c => c.Type == card.Type).ToList();
+
+            foreach (var otherCard in sameSuit)
+            {
+                int valueDiff = Math.Abs(
+                    GetCardNumericValue(card) - GetCardNumericValue(otherCard));
+
+                if (valueDiff == 1)
+                    usefulness += 15; // Adjacent card
+                else if (valueDiff == 2)
+                    usefulness += 5;  // One card away
+            }
+
+            return usefulness;
+        }
+
+        /// <summary>
+        /// Selects a card to discard from melds (when no deadwood)
+        /// </summary>
+        private TraditionalCard SelectCardFromMelds(List<Meld> melds)
+        {
+            // Find the smallest meld
+            var smallestMeld = melds.OrderBy(m => m.Cards.Count).First();
+
+            // Discard highest value card from smallest meld
+            return smallestMeld.Cards.OrderByDescending(card => Meld.GetCardPoints(card)).First();
+        }
+
+        /// <summary>
+        /// Gets all cards currently in melds
+        /// </summary>
+        private HashSet<TraditionalCard> GetMeldedCards(List<Meld> melds)
+        {
+            var meldedCards = new HashSet<TraditionalCard>();
+
+            foreach (var meld in melds)
+            {
+                foreach (var card in meld.Cards)
+                {
+                    meldedCards.Add(card);
+                }
+            }
+
+            return meldedCards;
+        }
+
+        /// <summary>
+        /// Gets current hand as list
+        /// </summary>
+        private List<TraditionalCard> GetCurrentHandCards()
+        {
+            List<TraditionalCard> cards = new List<TraditionalCard>();
+
+            for (int i = 0; i < player.Hand.Count; i++)
+            {
+                cards.Add(player.Hand[i]);
+            }
+
+            return cards;
+        }
+
+        /// <summary>
+        /// Gets numeric value for card
+        /// </summary>
+        private int GetCardNumericValue(TraditionalCard card)
+        {
+            switch (card.Value)
+            {
+                case CardValue.Ace: return 1;
+                case CardValue.Two: return 2;
+                case CardValue.Three: return 3;
+                case CardValue.Four: return 4;
+                case CardValue.Five: return 5;
+                case CardValue.Six: return 6;
+                case CardValue.Seven: return 7;
+                case CardValue.Eight: return 8;
+                case CardValue.Nine: return 9;
+                case CardValue.Ten: return 10;
+                case CardValue.Jack: return 11;
+                case CardValue.Queen: return 12;
+                case CardValue.King: return 13;
+                default: return 0;
+            }
+        }
+    }
+}
+```
+
+**AI Strategy:**
+1. **Drawing:** Takes discard if it reduces deadwood
+2. **Discarding:** Discards highest-value deadwood card
+3. **Knocking:** Knocks with Gin or very low deadwood, probabilistic for medium deadwood
+
+This creates a competent but not unbeatable opponent.
+
+---
+
+## Part 5: Game Rules
+
+### Step 5.1: Knock Rule
+
+**Create:** `Core/Game/GinRummy/Rules/KnockRule.cs`
+
+```csharp
+using System;
+using CardsFramework.Rules;
+
+namespace CardsFramework.GinRummy
+{
+    public class KnockEventArgs : EventArgs
+    {
+        public GinRummyPlayer Player { get; set; }
+    }
+
+    /// <summary>
+    /// Rule that fires when a player knocks
+    /// </summary>
+    public class KnockRule : GameRule
+    {
+        private readonly GinRummyCardGame game;
+
+        public KnockRule(GinRummyCardGame game)
+        {
+            this.game = game;
+        }
+
+        public override void Check()
+        {
+            foreach (var player in game.Players)
+            {
+                if (player is GinRummyPlayer ginPlayer)
+                {
+                    if (ginPlayer.HasKnocked && !ginPlayer.HasGin)
+                    {
+                        FireRuleMatch(new KnockEventArgs { Player = ginPlayer });
+                        return;
+                    }
+                }
+            }
+        }
+    }
+}
+```
+
+### Step 5.2: Gin Rule
+
+**Create:** `Core/Game/GinRummy/Rules/GinRule.cs`
+
+```csharp
+using System;
+using CardsFramework.Rules;
+
+namespace CardsFramework.GinRummy
+{
+    public class GinEventArgs : EventArgs
+    {
+        public GinRummyPlayer Player { get; set; }
+    }
+
+    /// <summary>
+    /// Rule that fires when a player gets Gin
+    /// </summary>
+    public class GinRule : GameRule
+    {
+        private readonly GinRummyCardGame game;
+
+        public GinRule(GinRummyCardGame game)
+        {
+            this.game = game;
+        }
+
+        public override void Check()
+        {
+            foreach (var player in game.Players)
+            {
+                if (player is GinRummyPlayer ginPlayer)
+                {
+                    if (ginPlayer.HasGin)
+                    {
+                        FireRuleMatch(new GinEventArgs { Player = ginPlayer });
+                        return;
+                    }
+                }
+            }
+        }
+    }
+}
+```
+
+### Step 5.3: Turn Complete Rule
+
+**Create:** `Core/Game/GinRummy/Rules/TurnCompleteRule.cs`
+
+```csharp
+using System;
+using CardsFramework.Rules;
+
+namespace CardsFramework.GinRummy
+{
+    public class TurnCompleteEventArgs : EventArgs
+    {
+        public GinRummyPlayer Player { get; set; }
+    }
+
+    /// <summary>
+    /// Rule that fires when a player completes their turn
+    /// </summary>
+    public class TurnCompleteRule : GameRule
+    {
+        private readonly GinRummyCardGame game;
+        private int previousHandCount = -1;
+
+        public TurnCompleteRule(GinRummyCardGame game)
+        {
+            this.game = game;
+        }
+
+        public override void Check()
+        {
+            var currentPlayer = game.GetCurrentGinRummyPlayer();
+
+            if (currentPlayer != null && currentPlayer.IsMyTurn)
+            {
+                // Turn is complete when player has drawn and discarded
+                // (hand back to 10 cards after temporarily having 11)
+                if (currentPlayer.HasDrawn && currentPlayer.Hand.Count == 10)
+                {
+                    if (previousHandCount == 11)
+                    {
+                        FireRuleMatch(new TurnCompleteEventArgs { Player = currentPlayer });
+                        previousHandCount = 10;
+                    }
+                }
+                else if (currentPlayer.Hand.Count == 11)
+                {
+                    previousHandCount = 11;
+                }
+            }
+        }
+    }
+}
+```
+
+**How It Works:**
+- Detects when hand goes from 11 cards (after draw) to 10 cards (after discard)
+- Signals turn completion to advance to next player
+
+---
+
+## Part 6: Main Game Class (Part 1/3)
+
+### Step 6.1: Fields and Initialization
+
+**Create:** `Core/Game/GinRummy/Game/GinRummyCardGame.cs`
+
+```csharp
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using CardsFramework.Cards;
+using CardsFramework.Game;
+using CardsFramework.Players;
+using CardsFramework.UI;
+using CardsFramework.Rules;
+using GameStateManagement;
+
+namespace CardsFramework.GinRummy
+{
+    public class GinRummyCardGame : CardsGame
+    {
+        #region Fields
+
+        // Game state
+        private GinRummyGameState currentState;
+        public GinRummyGameState State
+        {
+            get { return currentState; }
+            set { currentState = value; }
+        }
+
+        // Stock and discard piles
+        private List<TraditionalCard> discardPile;
+        public TraditionalCard TopDiscard
+        {
+            get { return discardPile.Count > 0 ? discardPile[discardPile.Count - 1] : null; }
+        }
+
+        // UI Components
+        private List<AnimatedHandGameComponent> animatedHands;
+        private AnimatedCardsGameComponent discardPileComponent;
+        private DeckDisplayComponent stockPileComponent;
+
+        private Button buttonDrawStock;
+        private Button buttonDrawDiscard;
+        private Button buttonKnock;
+        private Button buttonGin;
+        private Button buttonNewRound;
+
+        // Current turn management
+        private int currentPlayerIndex;
+
+        // Game rules
+        private KnockRule knockRule;
+        private GinRule ginRule;
+        private TurnCompleteRule turnCompleteRule;
+
+        // Display
+        private SpriteFont gameFont;
+        private string statusText;
+
+        #endregion
+
+        #region Initialization
+
+        public GinRummyCardGame(GameTable gameTable)
+            : base(
+                decks: 1,
+                jokersInDeck: 0,
+                suits: CardSuit.AllSuits,
+                cardValues: CardValue.NonJokers,
+                minimumPlayers: 2,
+                maximumPlayers: 4,
+                gameTable: gameTable,
+                theme: "Default")
+        {
+            discardPile = new List<TraditionalCard>();
+            animatedHands = new List<AnimatedHandGameComponent>();
+            currentPlayerIndex = 0;
+            statusText = "";
+        }
+
+        public override void Initialize()
+        {
+            base.Initialize();
+            currentState = GinRummyGameState.Dealing;
+        }
+
+        public override void LoadContent()
+        {
+            base.LoadContent();
+
+            gameFont = Game.Content.Load<SpriteFont>(@"Fonts\Regular");
+
+            // Load button textures
+            Texture2D buttonTexture = Game.Content.Load<Texture2D>(@"Images\UI\ButtonRegular");
+            Texture2D buttonPressedTexture = Game.Content.Load<Texture2D>(@"Images\UI\ButtonPressed");
+
+            int screenWidth = GraphicsDevice.Viewport.Width;
+            int screenHeight = GraphicsDevice.Viewport.Height;
+            int buttonWidth = 200;
+            int buttonHeight = 60;
+
+            // Draw Stock button
+            buttonDrawStock = new Button(
+                new Rectangle(screenWidth / 2 - buttonWidth - 120, screenHeight - 150, buttonWidth, buttonHeight),
+                buttonTexture,
+                buttonPressedTexture,
+                gameFont,
+                "Draw from Stock",
+                Color.White
+            );
+            buttonDrawStock.Click += ButtonDrawStock_Click;
+            buttonDrawStock.Visible = false;
+            Game.Components.Add(buttonDrawStock);
+
+            // Draw Discard button
+            buttonDrawDiscard = new Button(
+                new Rectangle(screenWidth / 2 + 120 - buttonWidth, screenHeight - 150, buttonWidth, buttonHeight),
+                buttonTexture,
+                buttonPressedTexture,
+                gameFont,
+                "Draw from Discard",
+                Color.White
+            );
+            buttonDrawDiscard.Click += ButtonDrawDiscard_Click;
+            buttonDrawDiscard.Visible = false;
+            Game.Components.Add(buttonDrawDiscard);
+
+            // Knock button
+            buttonKnock = new Button(
+                new Rectangle(screenWidth / 2 - buttonWidth / 2, screenHeight - 230, buttonWidth, buttonHeight),
+                buttonTexture,
+                buttonPressedTexture,
+                gameFont,
+                "Knock",
+                Color.Yellow
+            );
+            buttonKnock.Click += ButtonKnock_Click;
+            buttonKnock.Visible = false;
+            Game.Components.Add(buttonKnock);
+
+            // Gin button
+            buttonGin = new Button(
+                new Rectangle(screenWidth / 2 - buttonWidth / 2, screenHeight - 310, buttonWidth, buttonHeight),
+                buttonTexture,
+                buttonPressedTexture,
+                gameFont,
+                "Gin!",
+                Color.Green
+            );
+            buttonGin.Click += ButtonGin_Click;
+            buttonGin.Visible = false;
+            Game.Components.Add(buttonGin);
+
+            // New Round button
+            buttonNewRound = new Button(
+                new Rectangle(screenWidth / 2 - buttonWidth / 2, screenHeight - 150, buttonWidth, buttonHeight),
+                buttonTexture,
+                buttonPressedTexture,
+                gameFont,
+                "New Round",
+                Color.LightBlue
+            );
+            buttonNewRound.Click += ButtonNewRound_Click;
+            buttonNewRound.Visible = false;
+            Game.Components.Add(buttonNewRound);
+
+            // Create stock pile display
+            stockPileComponent = new DeckDisplayComponent(dealer, gameTable, Game);
+            stockPileComponent.LoadContent();
+            Game.Components.Add(stockPileComponent);
+
+            // Initialize rules
+            knockRule = new KnockRule(this);
+            knockRule.RuleMatch += KnockRule_RuleMatch;
+            Rules.Add(knockRule);
+
+            ginRule = new GinRule(this);
+            ginRule.RuleMatch += GinRule_RuleMatch;
+            Rules.Add(ginRule);
+
+            turnCompleteRule = new TurnCompleteRule(this);
+            turnCompleteRule.RuleMatch += TurnCompleteRule_RuleMatch;
+            Rules.Add(turnCompleteRule);
+        }
+
+        #endregion
+```
+
+### Step 6.2: Player Management
+
+```csharp
+        #region Player Management
+
+        public override void AddPlayer(Player newPlayer)
+        {
+            if (!(newPlayer is GinRummyPlayer))
+            {
+                throw new ArgumentException("Player must be of type GinRummyPlayer");
+            }
+
+            base.AddPlayer(newPlayer);
+
+            // Create animated hand for this player
+            AnimatedHandGameComponent animatedHand = new AnimatedHandGameComponent(
+                newPlayer.Hand,
+                gameTable[Players.Count - 1],
+                Game
+            );
+            animatedHand.LoadContent();
+            animatedHands.Add(animatedHand);
+            Game.Components.Add(animatedHand);
+        }
+
+        public override Player GetCurrentPlayer()
+        {
+            if (Players.Count == 0)
+                return null;
+
+            return Players[currentPlayerIndex];
+        }
+
+        public GinRummyPlayer GetCurrentGinRummyPlayer()
+        {
+            return GetCurrentPlayer() as GinRummyPlayer;
+        }
+
+        private void NextPlayer()
+        {
+            var currentPlayer = GetCurrentGinRummyPlayer();
+            if (currentPlayer != null)
+            {
+                currentPlayer.IsMyTurn = false;
+                currentPlayer.HasDrawn = false;
+            }
+
+            currentPlayerIndex = (currentPlayerIndex + 1) % Players.Count;
+
+            var nextPlayer = GetCurrentGinRummyPlayer();
+            if (nextPlayer != null)
+            {
+                nextPlayer.IsMyTurn = true;
+            }
+
+            statusText = $"{nextPlayer.Name}'s turn";
+        }
+
+        #endregion
+```
+
+### Step 6.3: Dealing
+
+```csharp
+        #region Card Management
+
+        public override void Deal()
+        {
+            // Shuffle deck
+            dealer.Shuffle();
+
+            // Deal 10 cards to each player
+            for (int i = 0; i < 10; i++)
+            {
+                foreach (var player in Players)
+                {
+                    dealer[0].MoveToHand(player.Hand);
+                }
+            }
+
+            // Flip top card to start discard pile
+            TraditionalCard topCard = dealer[0];
+            discardPile.Add(topCard);
+
+            // Create discard pile component
+            Vector2 discardPosition = new Vector2(
+                GraphicsDevice.Viewport.Width / 2 + 100,
+                GraphicsDevice.Viewport.Height / 2
+            );
+
+            discardPileComponent = new AnimatedCardsGameComponent(topCard, Game);
+            discardPileComponent.CurrentPosition = discardPosition;
+            discardPileComponent.IsFaceDown = false;
+            discardPileComponent.LoadContent();
+            Game.Components.Add(discardPileComponent);
+
+            // Analyze all hands
+            foreach (var player in Players)
+            {
+                if (player is GinRummyPlayer ginPlayer)
+                {
+                    ginPlayer.AnalyzeHand();
+                }
+            }
+
+            // Start first player's turn
+            var firstPlayer = GetCurrentGinRummyPlayer();
+            if (firstPlayer != null)
+            {
+                firstPlayer.IsMyTurn = true;
+                statusText = $"{firstPlayer.Name}'s turn";
+            }
+
+            currentState = GinRummyGameState.Drawing;
+        }
+
+        public void DrawFromStock()
+        {
+            var currentPlayer = GetCurrentGinRummyPlayer();
+            if (currentPlayer == null || !currentPlayer.IsMyTurn)
+                return;
+
+            if (dealer.Count > 0)
+            {
+                TraditionalCard card = dealer[0];
+                card.MoveToHand(currentPlayer.Hand);
+                currentPlayer.HasDrawn = true;
+                currentPlayer.AnalyzeHand();
+
+                currentState = GinRummyGameState.Discarding;
+            }
+        }
+
+        public void DrawFromDiscard()
+        {
+            var currentPlayer = GetCurrentGinRummyPlayer();
+            if (currentPlayer == null || !currentPlayer.IsMyTurn)
+                return;
+
+            if (discardPile.Count > 0)
+            {
+                TraditionalCard card = discardPile[discardPile.Count - 1];
+                discardPile.RemoveAt(discardPile.Count - 1);
+                card.MoveToHand(currentPlayer.Hand);
+                currentPlayer.HasDrawn = true;
+                currentPlayer.AnalyzeHand();
+
+                // Update discard pile visual
+                UpdateDiscardPileVisual();
+
+                currentState = GinRummyGameState.Discarding;
+            }
+        }
+
+        public void DiscardCard(TraditionalCard card)
+        {
+            var currentPlayer = GetCurrentGinRummyPlayer();
+            if (currentPlayer == null || !currentPlayer.IsMyTurn || !currentPlayer.HasDrawn)
+                return;
+
+            // Remove from hand
+            currentPlayer.Hand.LostCard -= null; // Detach events if any
+            discardPile.Add(card);
+
+            // Update visual
+            UpdateDiscardPileVisual();
+
+            // Analyze hand after discard
+            currentPlayer.AnalyzeHand();
+
+            // Check if player can knock or has gin
+            if (currentPlayer.HasGin)
+            {
+                currentState = GinRummyGameState.Gin;
+            }
+            else
+            {
+                currentState = GinRummyGameState.Waiting;
+            }
+        }
+
+        private void UpdateDiscardPileVisual()
+        {
+            if (discardPileComponent != null)
+            {
+                Game.Components.Remove(discardPileComponent);
+            }
+
+            if (TopDiscard != null)
+            {
+                Vector2 discardPosition = new Vector2(
+                    GraphicsDevice.Viewport.Width / 2 + 100,
+                    GraphicsDevice.Viewport.Height / 2
+                );
+
+                discardPileComponent = new AnimatedCardsGameComponent(TopDiscard, Game);
+                discardPileComponent.CurrentPosition = discardPosition;
+                discardPileComponent.IsFaceDown = false;
+                discardPileComponent.LoadContent();
+                Game.Components.Add(discardPileComponent);
+            }
+        }
+
+        #endregion
+```
+
+---
+
+## Part 7: Main Game Class (Part 2/3) - Game Flow
+
+```csharp
+        #region Game Flow
+
+        public override void StartPlaying()
+        {
+            currentState = GinRummyGameState.Dealing;
+            Deal();
+        }
+
+        public override void Update(GameTime gameTime)
+        {
+            base.Update(gameTime);
+
+            CheckRules();
+            UpdateUIForState();
+            ProcessAITurns(gameTime);
+        }
+
+        private void UpdateUIForState()
+        {
+            // Hide all buttons initially
+            buttonDrawStock.Visible = false;
+            buttonDrawDiscard.Visible = false;
+            buttonKnock.Visible = false;
+            buttonGin.Visible = false;
+            buttonNewRound.Visible = false;
+
+            var currentPlayer = GetCurrentGinRummyPlayer();
+
+            switch (currentState)
+            {
+                case GinRummyGameState.Drawing:
+                    // Only show draw buttons for human player
+                    if (currentPlayer != null && !(currentPlayer is GinRummyAIPlayer))
+                    {
+                        buttonDrawStock.Visible = true;
+                        buttonDrawDiscard.Visible = (TopDiscard != null);
+                    }
+                    break;
+
+                case GinRummyGameState.Discarding:
+                    // Show knock/gin buttons if eligible
+                    if (currentPlayer != null && !(currentPlayer is GinRummyAIPlayer))
+                    {
+                        if (currentPlayer.HasGin)
+                        {
+                            buttonGin.Visible = true;
+                        }
+                        else if (currentPlayer.CanKnock())
+                        {
+                            buttonKnock.Visible = true;
+                        }
+
+                        statusText = $"{currentPlayer.Name}: Select a card to discard";
+                    }
+                    break;
+
+                case GinRummyGameState.RoundEnd:
+                    buttonNewRound.Visible = true;
+                    break;
+            }
+        }
+
+        private void ProcessAITurns(GameTime gameTime)
+        {
+            var currentPlayer = GetCurrentGinRummyPlayer();
+
+            if (currentPlayer == null || !(currentPlayer is GinRummyAIPlayer))
+                return;
+
+            GinRummyAIPlayer aiPlayer = (GinRummyAIPlayer)currentPlayer;
+
+            // AI drawing phase
+            if (currentState == GinRummyGameState.Drawing)
+            {
+                // Small delay for realism
+                System.Threading.Tasks.Task.Delay(1000).ContinueWith(t =>
+                {
+                    if (aiPlayer.AI.ShouldDrawFromDiscard(TopDiscard))
+                    {
+                        DrawFromDiscard();
+                    }
+                    else
+                    {
+                        DrawFromStock();
+                    }
+                });
+            }
+            // AI discarding phase
+            else if (currentState == GinRummyGameState.Discarding)
+            {
+                // Check if AI should knock
+                if (aiPlayer.AI.ShouldKnock())
+                {
+                    if (aiPlayer.HasGin)
+                    {
+                        aiPlayer.HasGin = true;
+                        currentState = GinRummyGameState.Gin;
+                    }
+                    else
+                    {
+                        aiPlayer.HasKnocked = true;
+                        currentState = GinRummyGameState.Knocked;
+                    }
+                    return;
+                }
+
+                // Select card to discard
+                System.Threading.Tasks.Task.Delay(1000).ContinueWith(t =>
+                {
+                    TraditionalCard cardToDiscard = aiPlayer.AI.SelectCardToDiscard();
+                    DiscardCard(cardToDiscard);
+                });
+            }
+        }
+
+        #endregion
+```
+
+---
+
+## Part 8: Main Game Class (Part 3/3) - Event Handlers and Scoring
+
+```csharp
+        #region Event Handlers
+
+        private void ButtonDrawStock_Click(object sender, EventArgs e)
+        {
+            DrawFromStock();
+        }
+
+        private void ButtonDrawDiscard_Click(object sender, EventArgs e)
+        {
+            DrawFromDiscard();
+        }
+
+        private void ButtonKnock_Click(object sender, EventArgs e)
+        {
+            var currentPlayer = GetCurrentGinRummyPlayer();
+            if (currentPlayer != null && currentPlayer.CanKnock())
+            {
+                currentPlayer.HasKnocked = true;
+                currentState = GinRummyGameState.Knocked;
+            }
+        }
+
+        private void ButtonGin_Click(object sender, EventArgs e)
+        {
+            var currentPlayer = GetCurrentGinRummyPlayer();
+            if (currentPlayer != null && currentPlayer.HasGin)
+            {
+                currentState = GinRummyGameState.Gin;
+            }
+        }
+
+        private void ButtonNewRound_Click(object sender, EventArgs e)
+        {
+            ResetForNewRound();
+            StartPlaying();
+        }
+
+        private void KnockRule_RuleMatch(object sender, EventArgs e)
+        {
+            KnockEventArgs args = (KnockEventArgs)e;
+            statusText = $"{args.Player.Name} knocked!";
+
+            currentState = GinRummyGameState.Scoring;
+            CalculateScores();
+        }
+
+        private void GinRule_RuleMatch(object sender, EventArgs e)
+        {
+            GinEventArgs args = (GinEventArgs)e;
+            statusText = $"{args.Player.Name} got Gin!";
+
+            currentState = GinRummyGameState.Scoring;
+            CalculateScores();
+        }
+
+        private void TurnCompleteRule_RuleMatch(object sender, EventArgs e)
+        {
+            NextPlayer();
+            currentState = GinRummyGameState.Drawing;
+        }
+
+        #endregion
+
+        #region Scoring
+
+        private void CalculateScores()
+        {
+            var knocker = Players.OfType<GinRummyPlayer>().FirstOrDefault(p => p.HasKnocked || p.HasGin);
+
+            if (knocker == null)
+                return;
+
+            // Get opponents
+            var opponents = Players.OfType<GinRummyPlayer>().Where(p => p != knocker).ToList();
+
+            if (knocker.HasGin)
+            {
+                // Gin: Knocker gets all opponents' deadwood + 25 bonus
+                int totalOpponentDeadwood = opponents.Sum(opp => opp.Deadwood);
+                knocker.RoundScore = totalOpponentDeadwood + 25;
+
+                statusText = $"{knocker.Name} wins with Gin!\n" +
+                            $"Score: {knocker.RoundScore} points";
+            }
+            else
+            {
+                // Regular knock: Check for undercut
+                bool undercut = false;
+
+                foreach (var opponent in opponents)
+                {
+                    if (opponent.Deadwood <= knocker.Deadwood)
+                    {
+                        // Undercut! Opponent wins
+                        undercut = true;
+                        int difference = knocker.Deadwood - opponent.Deadwood;
+                        opponent.RoundScore = difference + 25;
+
+                        statusText = $"{opponent.Name} undercuts {knocker.Name}!\n" +
+                                    $"{opponent.Name} scores {opponent.RoundScore} points";
+                        break;
+                    }
+                }
+
+                if (!undercut)
+                {
+                    // Knocker wins
+                    int totalDifference = opponents.Sum(opp => opp.Deadwood) - knocker.Deadwood;
+                    knocker.RoundScore = totalDifference;
+
+                    statusText = $"{knocker.Name} wins!\n" +
+                                $"Score: {knocker.RoundScore} points";
+                }
+            }
+
+            currentState = GinRummyGameState.RoundEnd;
+        }
+
+        private void ResetForNewRound()
+        {
+            // Clear discard pile
+            discardPile.Clear();
+
+            // Remove visual components
+            if (discardPileComponent != null)
+            {
+                Game.Components.Remove(discardPileComponent);
+            }
+
+            // Reset all players
+            foreach (var player in Players)
+            {
+                if (player is GinRummyPlayer ginPlayer)
+                {
+                    // Clear hand
+                    while (ginPlayer.Hand.Count > 0)
+                    {
+                        ginPlayer.Hand[0].MoveToHand(dealer);
+                    }
+
+                    ginPlayer.ResetForNewRound();
+                }
+            }
+
+            // Reshuffle dealer
+            dealer.Shuffle();
+
+            currentPlayerIndex = 0;
+            statusText = "";
+        }
+
+        #endregion
+
+        #region Utilities
+
+        public override int CardValue(TraditionalCard card)
+        {
+            return Meld.GetCardPoints(card);
+        }
+
+        public override void Draw(GameTime gameTime)
+        {
+            base.Draw(gameTime);
+
+            // Draw status text
+            if (!string.IsNullOrEmpty(statusText) && gameFont != null)
+            {
+                SpriteBatch spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
+
+                if (spriteBatch != null)
+                {
+                    Vector2 position = new Vector2(20, 20);
+
+                    // Draw with shadow
+                    spriteBatch.DrawString(gameFont, statusText, position + new Vector2(2, 2), Color.Black);
+                    spriteBatch.DrawString(gameFont, statusText, position, Color.White);
+                }
+            }
+        }
+
+        #endregion
+    }
+}
+```
+
+---
+
+## Part 9: UI Enhancement - Hand Organizer
+
+**Create:** `Core/Game/GinRummy/UI/HandOrganizer.cs`
+
+```csharp
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using CardsFramework.Cards;
+
+namespace CardsFramework.GinRummy
+{
+    /// <summary>
+    /// Organizes cards in hand for better visualization
+    /// </summary>
+    public class HandOrganizer
+    {
+        /// <summary>
+        /// Sorts and groups cards for display
+        /// </summary>
+        public static List<TraditionalCard> OrganizeHand(List<TraditionalCard> cards, List<Meld> melds)
+        {
+            List<TraditionalCard> organized = new List<TraditionalCard>();
+
+            // Get cards in melds
+            var meldedCards = new HashSet<TraditionalCard>();
+            foreach (var meld in melds)
+            {
+                foreach (var card in meld.Cards)
+                {
+                    meldedCards.Add(card);
+                }
+            }
+
+            // Add melded cards first (grouped by meld)
+            foreach (var meld in melds)
+            {
+                organized.AddRange(meld.Cards);
+            }
+
+            // Add remaining cards (deadwood) sorted
+            var deadwood = cards.Where(c => !meldedCards.Contains(c))
+                               .OrderBy(c => c.Type)
+                               .ThenBy(c => GetCardNumericValue(c))
+                               .ToList();
+
+            organized.AddRange(deadwood);
+
+            return organized;
+        }
+
+        private static int GetCardNumericValue(TraditionalCard card)
+        {
+            switch (card.Value)
+            {
+                case CardValue.Ace: return 1;
+                case CardValue.Two: return 2;
+                case CardValue.Three: return 3;
+                case CardValue.Four: return 4;
+                case CardValue.Five: return 5;
+                case CardValue.Six: return 6;
+                case CardValue.Seven: return 7;
+                case CardValue.Eight: return 8;
+                case CardValue.Nine: return 9;
+                case CardValue.Ten: return 10;
+                case CardValue.Jack: return 11;
+                case CardValue.Queen: return 12;
+                case CardValue.King: return 13;
+                default: return 0;
+            }
+        }
+
+        /// <summary>
+        /// Calculates card positions for visual display
+        /// </summary>
+        public static List<Vector2> CalculateCardPositions(
+            int cardCount,
+            Vector2 startPosition,
+            float cardSpacing,
+            List<int> meldBreaks = null)
+        {
+            List<Vector2> positions = new List<Vector2>();
+
+            float currentX = startPosition.X;
+
+            for (int i = 0; i < cardCount; i++)
+            {
+                positions.Add(new Vector2(currentX, startPosition.Y));
+
+                // Add extra space between melds
+                if (meldBreaks != null && meldBreaks.Contains(i))
+                {
+                    currentX += cardSpacing + 20; // Extra gap
+                }
+                else
+                {
+                    currentX += cardSpacing;
+                }
+            }
+
+            return positions;
+        }
+    }
+}
+```
+
+This helper organizes cards visually, grouping melds together and separating deadwood.
+
+---
+
+## Part 10: Screen Integration
+
+### Step 10.1: Create Gameplay Screen
+
+**Create:** `Core/Game/Screens/GinRummyGameplayScreen.cs`
+
+```csharp
+using System;
+using Microsoft.Xna.Framework;
+using CardsFramework.GinRummy;
+using CardsFramework.UI;
+using GameStateManagement;
+
+namespace CardsStarterKit
+{
+    public class GinRummyGameplayScreen : GameScreen
+    {
+        private GinRummyCardGame ginRummyGame;
+
+        public GinRummyGameplayScreen()
+        {
+            EnabledGestures = Microsoft.Xna.Framework.Input.Touch.GestureType.Tap;
+        }
+
+        public override void LoadContent()
+        {
+            base.LoadContent();
+
+            // Create game table (4 player positions)
+            GameTable gameTable = new GameTable(ScreenManager.Game, 4);
+
+            // Create game
+            ginRummyGame = new GinRummyCardGame(gameTable);
+            ScreenManager.Game.Components.Add(ginRummyGame);
+            ginRummyGame.Initialize();
+            ginRummyGame.LoadContent();
+
+            // Add players
+            GinRummyPlayer humanPlayer = new GinRummyPlayer("You", ginRummyGame);
+            ginRummyGame.AddPlayer(humanPlayer);
+
+            GinRummyAIPlayer ai1 = new GinRummyAIPlayer("AI 1", ginRummyGame);
+            ginRummyGame.AddPlayer(ai1);
+
+            GinRummyAIPlayer ai2 = new GinRummyAIPlayer("AI 2", ginRummyGame);
+            ginRummyGame.AddPlayer(ai2);
+
+            // Start game
+            ginRummyGame.StartPlaying();
+        }
+
+        public override void HandleInput(InputState input)
+        {
+            base.HandleInput(input);
+
+            if (input.IsPauseGame(null))
+            {
+                ScreenManager.AddScreen(new PauseScreen(), null);
+            }
+
+            // Handle card selection for discarding
+            HandleCardSelection(input);
+        }
+
+        private void HandleCardSelection(InputState input)
+        {
+            if (ginRummyGame.State != GinRummyGameState.Discarding)
+                return;
+
+            var currentPlayer = ginRummyGame.GetCurrentGinRummyPlayer();
+
+            if (currentPlayer == null || currentPlayer is GinRummyAIPlayer)
+                return;
+
+            // Check for tap on cards in hand
+            // (Implementation depends on your touch/mouse handling)
+            // You would iterate through animated hand components and check bounds
+        }
+
+        public override void UnloadContent()
+        {
+            if (ginRummyGame != null)
+            {
+                ScreenManager.Game.Components.Remove(ginRummyGame);
+            }
+
+            base.UnloadContent();
+        }
+    }
+}
+```
+
+### Step 10.2: Add Menu Entry
+
+**Modify:** `Core/Game/Screens/MainMenuScreen.cs`
+
+```csharp
+// Add in constructor
+MenuEntry ginRummyMenuEntry = new MenuEntry("Gin Rummy");
+ginRummyMenuEntry.Selected += GinRummyMenuEntrySelected;
+menuEntries.Add(ginRummyMenuEntry);
+
+// Add event handler
+private void GinRummyMenuEntrySelected(object sender, EventArgs e)
+{
+    ScreenManager.AddScreen(new GinRummyGameplayScreen(), null);
+}
+```
+
+---
+
+## Part 11: Testing
+
+### Step 11.1: Build and Run
+
+```bash
+dotnet build
+dotnet run --project Platforms/Desktop/CardsStarterKit.Desktop.csproj
+```
+
+### Step 11.2: Test Scenarios
+
+1. **Basic Gameplay:**
+   - Draw from stock/discard
+   - Discard cards
+   - Verify turn rotation
+
+2. **Meld Detection:**
+   - Check that sets are recognized (3 7s, etc.)
+   - Check that runs are recognized (4♠ 5♠ 6♠)
+   - Verify deadwood calculation
+
+3. **Knocking:**
+   - Get deadwood ≤ 10 and test knock
+   - Verify scoring
+
+4. **Gin:**
+   - Form all melds (0 deadwood)
+   - Verify Gin declaration and bonus
+
+5. **AI Behavior:**
+   - Watch AI draw and discard decisions
+   - Verify AI knocks appropriately
+
+---
+
+## Part 12: Enhancements and Next Steps
+
+### Enhancement 1: Card Clicking for Human Player
+
+Add touch/mouse handling in the screen to let human players click cards to discard:
+
+```csharp
+private void HandleCardSelection(InputState input)
+{
+    // Get tap position
+    foreach (var gesture in input.Gestures)
+    {
+        if (gesture.GestureType == GestureType.Tap)
+        {
+            // Check which card was tapped
+            // (Iterate through animated hand components and check bounds)
+        }
+    }
+}
+```
+
+### Enhancement 2: Visual Meld Highlighting
+
+Highlight cards that are part of melds:
+
+```csharp
+// In AnimatedHandGameComponent or custom renderer:
+foreach (var card in meldedCards)
+{
+    // Draw green border or glow effect
+}
+```
+
+### Enhancement 3: Multi-Round Scoring
+
+To extend to full matches (first to 100 points):
+
+```csharp
+// Add to GinRummyPlayer:
+public int MatchScore { get; set; }
+
+// Add game state:
+public enum GinRummyGameState
+{
+    // ... existing states
+    MatchEnd  // When someone reaches 100
+}
+
+// After each round:
+knocker.MatchScore += knocker.RoundScore;
+
+if (knocker.MatchScore >= 100)
+{
+    currentState = GinRummyGameState.MatchEnd;
+}
+```
+
+### Enhancement 4: Laying Off Cards
+
+After a knock, allow opponents to add cards to knocker's melds:
+
+```csharp
+public class LayOffPhase
+{
+    public static List<TraditionalCard> FindLayOffCards(
+        List<TraditionalCard> hand,
+        List<Meld> opponentMelds)
+    {
+        // Check each hand card to see if it extends opponent's melds
+        // ...
+    }
+}
+```
+
+### Enhancement 5: Better AI
+
+Advanced AI improvements:
+- Track discarded cards (card counting)
+- Infer opponent hands from discards
+- Calculate probability of completing melds
+- Defensive discarding (don't give opponent useful cards)
+
+---
+
+## Key Takeaways
+
+### What You Learned
+
+1. **Complex State Management:**
+   - Multi-phase turns (draw → discard)
+   - Turn rotation among players
+   - Handling knocking and gin conditions
+
+2. **Meld Detection Algorithm:**
+   - Finding all possible melds
+   - Combinatorial optimization to minimize deadwood
+   - Backtracking algorithm
+
+3. **AI Implementation:**
+   - Evaluating card usefulness
+   - Making draw/discard decisions
+   - Probabilistic knocking strategy
+
+4. **Scoring Logic:**
+   - Standard knock scoring
+   - Gin bonuses
+   - Undercut detection
+
+5. **Multi-Player Support:**
+   - Turn management
+   - Player indexing
+   - Mixed human/AI players
+
+### Design Patterns Used
+
+1. **State Machine:** GinRummyGameState controls game flow
+2. **Strategy Pattern:** AI decision-making in separate class
+3. **Rule Pattern:** Knock, Gin, TurnComplete rules
+4. **Component Pattern:** Animated hands, cards, deck display
+5. **Observer Pattern:** Event-driven rule matching
+
+---
+
+## Troubleshooting
+
+### Melds Not Detected
+- Debug `MeldDetector.FindOptimalMelds()`
+- Print all possible melds before optimization
+- Check card sorting logic
+
+### AI Makes Bad Moves
+- Add logging to AI decision methods
+- Print deadwood calculations
+- Verify card evaluation logic
+
+### Turn Doesn't Advance
+- Check `TurnCompleteRule` hand count tracking
+- Verify `NextPlayer()` is called
+- Debug state transitions
+
+### Scoring Incorrect
+- Print each player's deadwood
+- Verify meld point calculations
+- Check undercut logic
+
+---
+
+## Extending to Full Matches
+
+To implement full Gin Rummy matches to 100 points:
+
+**1. Add Match Tracking:**
+```csharp
+public int MatchScore { get; set; }
+public int RoundsWon { get; set; }
+```
+
+**2. Add Match State:**
+```csharp
+public enum MatchPhase
+{
+    InProgress,
+    Complete
+}
+```
+
+**3. Check After Each Round:**
+```csharp
+if (winner.MatchScore >= 100)
+{
+    currentState = GinRummyGameState.MatchEnd;
+    ShowMatchResults();
+}
+else
+{
+    PrepareNextRound();
+}
+```
+
+**4. Add Bonuses:**
+- Game bonus: +100 for winning match
+- Shutout bonus: Additional points if opponent scored 0
+
+---
+
+## Related Gin Rummy Variants
+
+Want to explore other variants? Here are some links:
+
+**Oklahoma Gin:**
+- First upcard determines knock limit (not always 10)
+- https://en.wikipedia.org/wiki/Oklahoma_gin
+
+**Hollywood Gin:**
+- Multiple simultaneous games
+- Wins count toward different game tracks
+- https://www.pagat.com/rummy/ginrummy.html#hollywood
+
+**Straight Gin:**
+- Can only knock with Gin (0 deadwood)
+- Higher scoring, faster games
+- https://www.pagat.com/rummy/ginrummy.html#straight
+
+---
+
+## Complete File Checklist
+
+- [ ] `Core/Game/GinRummy/Game/GinRummyGameState.cs`
+- [ ] `Core/Game/GinRummy/Game/Meld.cs`
+- [ ] `Core/Game/GinRummy/Game/MeldDetector.cs`
+- [ ] `Core/Game/GinRummy/Game/GinRummyCardGame.cs`
+- [ ] `Core/Game/GinRummy/Players/GinRummyPlayer.cs`
+- [ ] `Core/Game/GinRummy/Players/GinRummyAIPlayer.cs`
+- [ ] `Core/Game/GinRummy/AI/GinRummyAI.cs`
+- [ ] `Core/Game/GinRummy/Rules/KnockRule.cs`
+- [ ] `Core/Game/GinRummy/Rules/GinRule.cs`
+- [ ] `Core/Game/GinRummy/Rules/TurnCompleteRule.cs`
+- [ ] `Core/Game/GinRummy/UI/HandOrganizer.cs`
+- [ ] `Core/Game/Screens/GinRummyGameplayScreen.cs`
+- [ ] Modified `Core/Game/Screens/MainMenuScreen.cs`
+
+---
+
+## Conclusion
+
+Congratulations! You've built a complete Gin Rummy implementation with:
+
+- Full game rules (melds, knocking, gin)
+- Intelligent AI opponents
+- Multi-player support
+- Proper scoring
+- Turn-based gameplay
+
+This tutorial demonstrated advanced card game concepts including meld detection algorithms, AI decision-making, and complex game state management. You're now equipped to build any card game using the CardsStarterKit framework!
+
+**Next challenges to try:**
+- Implement other Rummy variants (Rummy 500, Canasta)
+- Build trick-taking games (Hearts, Spades, Bridge)
+- Create shedding games (Crazy Eights, Uno-style)
+
+Happy coding!

+ 245 - 0
MonoGame.Xna.Framework.Net/Net/ConnectionHealthMonitor.cs

@@ -0,0 +1,245 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.Xna.Framework.Net
+{
+    /// <summary>
+    /// Monitors connection health using heartbeats and detects disconnections.
+    /// </summary>
+    public class ConnectionHealthMonitor
+    {
+        private const int HEARTBEAT_INTERVAL_MS = 3000;  // Every 3 seconds (LAN)
+        private const int HEARTBEAT_TIMEOUT_MS = 9000;   // 3 missed = disconnect (fast for LAN)
+        
+        private readonly Dictionary<string, GamerConnectionState> connections = new Dictionary<string, GamerConnectionState>();
+        private uint sequenceNumber = 0;
+        private NetworkSession session;
+        private bool isRunning = false;
+
+        private class GamerConnectionState
+        {
+            public DateTime LastHeartbeat { get; set; } = DateTime.UtcNow;
+            public int MissedHeartbeats { get; set; } = 0;
+            public TimeSpan LastRtt { get; set; } = TimeSpan.Zero;
+            public uint LastSequenceNumber { get; set; } = 0;
+        }
+
+        /// <summary>
+        /// Starts monitoring connection health for the given session.
+        /// </summary>
+        public void StartMonitoring(NetworkSession session)
+        {
+            if (isRunning)
+                return;
+
+            this.session = session;
+            isRunning = true;
+
+            // Initialize connection tracking for all remote gamers
+            lock (connections)
+            {
+                foreach (var gamer in session.AllGamers.Where(g => !g.IsLocal))
+                {
+                    connections[gamer.Id] = new GamerConnectionState();
+                }
+            }
+
+            // Start heartbeat sender task
+            Task.Run(async () => await HeartbeatLoopAsync());
+        }
+
+        /// <summary>
+        /// Stops monitoring.
+        /// </summary>
+        public void StopMonitoring()
+        {
+            isRunning = false;
+        }
+
+        private async Task HeartbeatLoopAsync()
+        {
+            while (isRunning && session != null && !session.disposed)
+            {
+                try
+                {
+                    // Get the session's local gamer (not the static one, which can be stale)
+                    var localGamer = session.LocalGamers.FirstOrDefault();
+                    if (localGamer == null)
+                        continue; // No local gamer, skip this heartbeat
+                    
+                    // Send heartbeat to all remote gamers
+                    var heartbeat = new HeartbeatMessage
+                    {
+                        Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
+                        SequenceNumber = sequenceNumber++,
+                        GamerCount = session.AllGamers.Count,
+                        GamerId = localGamer.Id
+                    };
+
+                    var writer = new PacketWriter();
+                    heartbeat.Serialize(writer);
+                    var data = writer.GetData();
+
+                    // Send to each remote gamer individually (peer-to-peer)
+                    var remoteGamers = session.AllGamers.Where(g => !g.IsLocal).ToList();
+                    Console.WriteLine($"[HEARTBEAT-SEND] Sending to {remoteGamers.Count} remote gamer(s)");
+                    foreach (var gamer in remoteGamers)
+                    {
+                        Console.WriteLine($"[HEARTBEAT-SEND] -> {gamer.Gamertag} ({gamer.Id})");
+                        session.SendDataToGamer(gamer, data, SendDataOptions.None);
+                    }
+
+                    // Check for disconnections
+                    CheckForTimeouts();
+
+                    await Task.Delay(HEARTBEAT_INTERVAL_MS);
+                }
+                catch (Exception ex)
+                {
+                    Console.WriteLine($"[HEARTBEAT] Error in heartbeat loop: {ex.Message}");
+                }
+            }
+        }
+
+        private void CheckForTimeouts()
+        {
+            if (session == null)
+                return;
+
+            var now = DateTime.UtcNow;
+            var disconnected = new List<NetworkGamer>();
+
+            lock (connections)
+            {
+                foreach (var kvp in connections.ToList())
+                {
+                    var state = kvp.Value;
+                    var timeSinceLastHB = (now - state.LastHeartbeat).TotalMilliseconds;
+
+                    if (timeSinceLastHB > HEARTBEAT_TIMEOUT_MS)
+                    {
+                        // Mark for removal
+                        var gamer = session.AllGamers.FirstOrDefault(g => g.Id == kvp.Key);
+                        if (gamer != null)
+                        {
+                            disconnected.Add(gamer);
+                            Console.WriteLine($"[HEARTBEAT] {gamer.Gamertag} timed out (no response for {timeSinceLastHB:F0}ms)");
+                        }
+                    }
+                }
+            }
+
+            // Remove disconnected gamers (outside lock to avoid deadlock)
+            foreach (var gamer in disconnected)
+            {
+                // Use reflection to call internal RemoveGamer method
+                var removeMethod = session.GetType().GetMethod("RemoveGamer", 
+                    System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+                removeMethod?.Invoke(session, new object[] { gamer });
+                
+                lock (connections)
+                {
+                    connections.Remove(gamer.Id);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Called when HeartbeatMessage is received.
+        /// </summary>
+        public void OnHeartbeatReceived(string gamerId, HeartbeatMessage heartbeat)
+        {
+            Console.WriteLine($"[HEARTBEAT-RECV] Received from gamer {gamerId}, seq {heartbeat.SequenceNumber}");
+            
+            lock (connections)
+            {
+                if (!connections.ContainsKey(gamerId))
+                {
+                    // New remote gamer
+                    connections[gamerId] = new GamerConnectionState();
+                    Console.WriteLine($"[HEARTBEAT-RECV] Created new connection state for {gamerId}");
+                }
+
+                var state = connections[gamerId];
+                state.LastHeartbeat = DateTime.UtcNow;
+                state.MissedHeartbeats = 0;
+                state.LastSequenceNumber = heartbeat.SequenceNumber;
+            }
+
+            // Send reply for RTT measurement using session's local gamer (not static property)
+            var localGamer = session?.LocalGamers.FirstOrDefault();
+            var reply = new HeartbeatReplyMessage
+            {
+                RequestTimestamp = heartbeat.Timestamp,
+                ReplyTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
+                GamerId = localGamer?.Id ?? string.Empty
+            };
+
+            var writer = new PacketWriter();
+            reply.Serialize(writer);
+            
+            // Find the gamer and send reply
+            var gamer = session?.AllGamers.FirstOrDefault(g => g.Id == gamerId);
+            Console.WriteLine($"[HEARTBEAT-REPLY] Found gamer: {gamer?.Gamertag ?? "NULL"}, sending reply");
+            if (gamer != null)
+            {
+                session?.SendDataToGamer(gamer, writer.GetData(), SendDataOptions.None);
+            }
+            else
+            {
+                Console.WriteLine($"[HEARTBEAT-REPLY] ERROR: Could not find gamer {gamerId} to send reply!");
+            }
+        }
+
+        /// <summary>
+        /// Called when HeartbeatReplyMessage is received (for RTT calculation).
+        /// </summary>
+        public void OnHeartbeatReplyReceived(string gamerId, HeartbeatReplyMessage reply)
+        {
+            Console.WriteLine($"[HEARTBEAT-REPLY-RECV] Received reply from {gamerId}");
+            lock (connections)
+            {
+                if (connections.TryGetValue(gamerId, out var state))
+                {
+                    var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                    var rtt = TimeSpan.FromMilliseconds(now - reply.RequestTimestamp);
+                    state.LastRtt = rtt;
+
+                    // Update gamer's RTT for QoS display
+                    var gamer = session?.AllGamers.FirstOrDefault(g => g.Id == gamerId);
+                    gamer?.UpdateRoundtripTime(rtt);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Called when a new gamer joins to start tracking them.
+        /// </summary>
+        public void OnGamerJoined(NetworkGamer gamer)
+        {
+            if (gamer == null || gamer.IsLocal)
+                return;
+
+            lock (connections)
+            {
+                connections[gamer.Id] = new GamerConnectionState();
+            }
+        }
+
+        /// <summary>
+        /// Called when a gamer leaves to stop tracking them.
+        /// </summary>
+        public void OnGamerLeft(NetworkGamer gamer)
+        {
+            if (gamer == null)
+                return;
+
+            lock (connections)
+            {
+                connections.Remove(gamer.Id);
+            }
+        }
+    }
+}

+ 180 - 0
MonoGame.Xna.Framework.Net/Net/Discovery.cs

@@ -0,0 +1,180 @@
+// DiscoveryPacket.cs - Replaces your string header
+using System;
+using System.IO;
+using System.Net;
+
+namespace Microsoft.Xna.Framework.Net
+{
+    /// <summary>
+    /// Immutable discovery packet with length-prefixed binary format.
+    /// Format: [Magic(4)][Version(1)][Type(1)][PayloadLength(2)][Payload][CRC32(4)]
+    /// </summary>
+    public class DiscoveryPacket
+    {
+        public const uint MAGIC = 0x4D6F6E6F; // "Mono" in hex
+        public const byte PROTOCOL_VERSION = 1;
+        public const byte TYPE_DISCOVERY = 1;
+        public const byte TYPE_RESPONSE = 2;
+
+        public uint Magic { get; } = MAGIC;
+        public byte Version { get; } = PROTOCOL_VERSION;
+        public byte Type { get; }
+        public DiscoveryPayload Payload { get; }
+
+        public DiscoveryPacket(byte type, DiscoveryPayload payload)
+        {
+            Type = type;
+            Payload = payload ?? throw new ArgumentNullException(nameof(payload));
+        }
+
+        // Serialize to bytes using PacketWriter
+        public byte[] ToByteArray()
+        {
+            var writer = new PacketWriter();
+
+            // Header
+            writer.Write(Magic);
+            writer.Write(Version);
+            writer.Write(Type);
+
+            // Payload (write length first, then data)
+            var payloadBytes = Payload.ToByteArray();
+            writer.Write((ushort)payloadBytes.Length);
+            writer.Write(payloadBytes);
+
+            // CRC32 checksum (calculate on all data before CRC field)
+            var dataWithoutCrc = writer.GetData();
+            uint crc = CalculateCRC32(dataWithoutCrc, 0, dataWithoutCrc.Length);
+            writer.Write(crc);
+
+            return writer.GetData();
+        }
+
+        // Deserialize from bytes
+        public static bool TryParse(byte[] data, out DiscoveryPacket packet)
+        {
+            packet = null;
+            if (data == null || data.Length < 12) return false; // Minimum size
+
+            try
+            {
+                var reader = new PacketReader(data);
+
+                var magic = reader.ReadUInt32();
+                if (magic != MAGIC) return false;
+
+                var version = reader.ReadByte();
+                if (version != PROTOCOL_VERSION) return false;
+
+                var type = reader.ReadByte();
+                var payloadLength = reader.ReadUInt16();
+
+                // Validate payload length
+                if (payloadLength > reader.BytesRemaining - 4) return false;
+
+                var payloadBytes = reader.ReadBytes(payloadLength);
+                var checksum = reader.ReadUInt32();
+
+                // Verify CRC (basic check)
+                var calculatedCrc = CalculateCRC32(data, 0, data.Length - 4);
+                if (checksum != calculatedCrc) return false;
+
+                if (!DiscoveryPayload.TryParse(payloadBytes, out var payload)) return false;
+
+                packet = new DiscoveryPacket(type, payload);
+                return true;
+            }
+            catch
+            {
+                return false; // Any parse error = invalid packet
+            }
+        }
+
+        private static uint CalculateCRC32(byte[] data, int offset, int length)
+        {
+            uint crc = 0xFFFFFFFF;
+            for (int i = offset; i < offset + length; i++)
+            {
+                crc ^= data[i];
+                for (uint j = 0; j < 8; j++)
+                {
+                    bool isOdd = (crc & 1) != 0;
+                    crc = (crc >> 1) ^ (isOdd ? 0xEDB88320 : 0);
+                }
+            }
+            return ~crc;
+        }
+    }
+
+    /// <summary>
+    /// Payload containing session information.
+    /// </summary>
+    public class DiscoveryPayload
+    {
+        public string SessionId { get; }
+        public int MaxGamers { get; }
+        public int PrivateGamerSlots { get; }
+        public string HostGamertag { get; }
+        public int GamePort { get; }
+        public byte[] SessionProperties { get; }
+
+        public DiscoveryPayload(string sessionId, int maxGamers, int privateSlots,
+                               string hostGamertag, int gamePort, byte[] properties)
+        {
+            SessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId));
+            MaxGamers = maxGamers;
+            PrivateGamerSlots = privateSlots;
+            HostGamertag = hostGamertag ?? throw new ArgumentNullException(nameof(hostGamertag));
+            GamePort = gamePort;
+            SessionProperties = properties ?? Array.Empty<byte>();
+
+            Validate();
+        }
+
+        private void Validate()
+        {
+            if (SessionId.Length != 36) throw new ArgumentException("Invalid GUID length", nameof(SessionId));
+            if (MaxGamers < 1 || MaxGamers > 32) throw new ArgumentOutOfRangeException(nameof(MaxGamers));
+            if (PrivateGamerSlots < 0 || PrivateGamerSlots > MaxGamers)
+                throw new ArgumentOutOfRangeException(nameof(PrivateGamerSlots));
+            if (GamePort < 1024 || GamePort > 65535) throw new ArgumentOutOfRangeException(nameof(GamePort));
+            if (HostGamertag.Length > 32) throw new ArgumentException("Gamertag too long", nameof(HostGamertag));
+        }
+
+        public byte[] ToByteArray()
+        {
+            var writer = new PacketWriter();
+            writer.Write(SessionId);
+            writer.Write(MaxGamers);
+            writer.Write(PrivateGamerSlots);
+            writer.Write(HostGamertag);
+            writer.Write(GamePort);
+            writer.Write(SessionProperties);
+            return writer.GetData();
+        }
+
+        public static bool TryParse(byte[] data, out DiscoveryPayload payload)
+        {
+            payload = null;
+            if (data == null || data.Length < 50) return false; // Minimum GUID + metadata
+
+            try
+            {
+                var reader = new PacketReader(data);
+                var sessionId = reader.ReadString();
+                var maxGamers = reader.ReadInt32();
+                var privateSlots = reader.ReadInt32();
+                var gamertag = reader.ReadString();
+                var gamePort = reader.ReadInt32();
+                var properties = reader.ReadBytes();
+
+                payload = new DiscoveryPayload(sessionId, maxGamers, privateSlots, gamertag, gamePort, properties);
+                return true;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+    }
+}

+ 11 - 1
MonoGame.Xna.Framework.Net/Net/Enums/NetworkSessionJoinError.cs

@@ -27,6 +27,16 @@ namespace Microsoft.Xna.Framework.Net
         /// <summary>
         /// Session not joinable.
         /// </summary>
-        SessionNotJoinable
+        SessionNotJoinable,
+
+        /// <summary>
+        /// Protocol version mismatch between client and host.
+        /// </summary>
+        ProtocolVersionMismatch,
+
+        /// <summary>
+        /// Join request timed out (no response from host).
+        /// </summary>
+        Timeout
     }
 }

+ 5 - 0
MonoGame.Xna.Framework.Net/Net/Enums/NetworkSessionState.cs

@@ -10,6 +10,11 @@ namespace Microsoft.Xna.Framework.Net
         /// </summary>
         Creating,
 
+        /// <summary>
+        /// Session is joining (client waiting for host acceptance).
+        /// </summary>
+        Joining,
+
         /// <summary>
         /// Session is in the lobby, waiting for players.
         /// </summary>

+ 45 - 0
MonoGame.Xna.Framework.Net/Net/GamerLeavingMessage.cs

@@ -0,0 +1,45 @@
+using Microsoft.Xna.Framework.Net;
+
+namespace Microsoft.Xna.Framework.Net
+{
+    /// <summary>
+    /// Message sent when a gamer explicitly leaves the session (graceful disconnect).
+    /// This allows immediate notification instead of waiting for heartbeat timeout (9 seconds).
+    /// </summary>
+    public class GamerLeavingMessage : INetworkMessage
+    {
+        /// <summary>
+        /// Message type ID for GamerLeavingMessage.
+        /// </summary>
+        public byte MessageType => 9;
+
+        /// <summary>
+        /// The unique ID of the gamer who is leaving.
+        /// </summary>
+        public string GamerId { get; set; }
+
+        /// <summary>
+        /// Optional reason for leaving (e.g., "User quit", "Application closing").
+        /// </summary>
+        public string Reason { get; set; }
+
+        /// <summary>
+        /// Serializes the message to a packet writer.
+        /// </summary>
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(MessageType);
+            writer.Write(GamerId ?? string.Empty);
+            writer.Write(Reason ?? string.Empty);
+        }
+
+        /// <summary>
+        /// Deserializes the message from a packet reader.
+        /// </summary>
+        public void Deserialize(PacketReader reader)
+        {
+            GamerId = reader.ReadString();
+            Reason = reader.ReadString();
+        }
+    }
+}

+ 48 - 0
MonoGame.Xna.Framework.Net/Net/HeartbeatMessage.cs

@@ -0,0 +1,48 @@
+namespace Microsoft.Xna.Framework.Net
+{
+    /// <summary>
+    /// Heartbeat message sent periodically to monitor connection health.
+    /// </summary>
+    public class HeartbeatMessage : INetworkMessage
+    {
+        public byte MessageType => 7;
+        
+        /// <summary>
+        /// Timestamp in milliseconds since epoch for RTT calculation.
+        /// </summary>
+        public long Timestamp { get; set; }
+        
+        /// <summary>
+        /// Sequence number for detecting packet loss.
+        /// </summary>
+        public uint SequenceNumber { get; set; }
+        
+        /// <summary>
+        /// Current number of gamers in session.
+        /// </summary>
+        public int GamerCount { get; set; }
+        
+        /// <summary>
+        /// ID of the gamer sending the heartbeat.
+        /// </summary>
+        public string GamerId { get; set; }
+
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(MessageType);
+            writer.Write(Timestamp);
+            writer.Write(SequenceNumber);
+            writer.Write(GamerCount);
+            writer.Write(GamerId ?? string.Empty);
+        }
+
+        public void Deserialize(PacketReader reader)
+        {
+            // Reader is positioned after the type byte
+            Timestamp = reader.ReadInt64();
+            SequenceNumber = reader.ReadUInt32();
+            GamerCount = reader.ReadInt32();
+            GamerId = reader.ReadString();
+        }
+    }
+}

+ 43 - 0
MonoGame.Xna.Framework.Net/Net/HeartbeatReplyMessage.cs

@@ -0,0 +1,43 @@
+using System;
+
+namespace Microsoft.Xna.Framework.Net
+{
+    /// <summary>
+    /// Reply message sent in response to a heartbeat, used for RTT calculation.
+    /// </summary>
+    public class HeartbeatReplyMessage : INetworkMessage
+    {
+        public byte MessageType => 8;
+        
+        /// <summary>
+        /// Echo back the request timestamp for RTT calculation.
+        /// </summary>
+        public long RequestTimestamp { get; set; }
+        
+        /// <summary>
+        /// Reply's own timestamp.
+        /// </summary>
+        public long ReplyTimestamp { get; set; }
+        
+        /// <summary>
+        /// ID of the gamer sending the reply.
+        /// </summary>
+        public string GamerId { get; set; }
+
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(MessageType);
+            writer.Write(RequestTimestamp);
+            writer.Write(ReplyTimestamp);
+            writer.Write(GamerId ?? string.Empty);
+        }
+
+        public void Deserialize(PacketReader reader)
+        {
+            // Reader is positioned after the type byte
+            RequestTimestamp = reader.ReadInt64();
+            ReplyTimestamp = reader.ReadInt64();
+            GamerId = reader.ReadString();
+        }
+    }
+}

+ 58 - 0
MonoGame.Xna.Framework.Net/Net/INetworkLogger.cs

@@ -0,0 +1,58 @@
+using System;
+
+namespace Microsoft.Xna.Framework.Net
+{
+    /// <summary>
+    /// Interface for network logging.
+    /// </summary>
+    public interface INetworkLogger
+    {
+        /// <summary>
+        /// Logs an informational message.
+        /// </summary>
+        void LogInfo(string message);
+        
+        /// <summary>
+        /// Logs a warning message.
+        /// </summary>
+        void LogWarning(string message);
+        
+        /// <summary>
+        /// Logs an error message with optional exception.
+        /// </summary>
+        void LogError(string message, Exception ex = null);
+    }
+
+    /// <summary>
+    /// Console-based network logger implementation.
+    /// </summary>
+    public class ConsoleNetworkLogger : INetworkLogger
+    {
+        public void LogInfo(string message)
+        {
+            Console.WriteLine($"[NET] {message}");
+        }
+
+        public void LogWarning(string message)
+        {
+            Console.WriteLine($"[NET-WARN] {message}");
+        }
+
+        public void LogError(string message, Exception ex = null)
+        {
+            Console.WriteLine($"[NET-ERROR] {message}");
+            if (ex != null)
+                Console.WriteLine($"  {ex}");
+        }
+    }
+
+    /// <summary>
+    /// Null logger that does nothing (for production builds).
+    /// </summary>
+    public class NullNetworkLogger : INetworkLogger
+    {
+        public void LogInfo(string message) { }
+        public void LogWarning(string message) { }
+        public void LogError(string message, Exception ex = null) { }
+    }
+}

+ 36 - 0
MonoGame.Xna.Framework.Net/Net/JoinRejectedMessage.cs

@@ -0,0 +1,36 @@
+using System;
+
+namespace Microsoft.Xna.Framework.Net
+{
+    /// <summary>
+    /// Message sent by the host to reject a join request.
+    /// </summary>
+    public class JoinRejectedMessage : INetworkMessage
+    {
+        public byte MessageType => 6;
+        
+        /// <summary>
+        /// The reason for the rejection.
+        /// </summary>
+        public NetworkSessionJoinError ErrorCode { get; set; }
+        
+        /// <summary>
+        /// Human-readable reason for rejection.
+        /// </summary>
+        public string Reason { get; set; }
+
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(MessageType);
+            writer.Write((byte)ErrorCode);
+            writer.Write(Reason ?? string.Empty);
+        }
+
+        public void Deserialize(PacketReader reader)
+        {
+            // Reader is positioned after the type byte
+            ErrorCode = (NetworkSessionJoinError)reader.ReadByte();
+            Reason = reader.ReadString();
+        }
+    }
+}

+ 5 - 0
MonoGame.Xna.Framework.Net/Net/JoinRequestMessage.cs

@@ -5,13 +5,17 @@ namespace Microsoft.Xna.Framework.Net
 {
     public class JoinRequestMessage : INetworkMessage
     {
+        public const byte CURRENT_PROTOCOL_VERSION = 1;
+        
         public byte MessageType => 2;
+        public byte ProtocolVersion { get; set; } = CURRENT_PROTOCOL_VERSION;
         public string GamerId { get; set; }
         public string Gamertag { get; set; }
 
         public void Serialize(PacketWriter writer)
         {
             writer.Write(MessageType);
+            writer.Write(ProtocolVersion);
             writer.Write(GamerId);
             writer.Write(Gamertag);
         }
@@ -19,6 +23,7 @@ namespace Microsoft.Xna.Framework.Net
         public void Deserialize(PacketReader reader)
         {
             // Reader is positioned after the type byte
+            ProtocolVersion = reader.ReadByte();
             GamerId = reader.ReadString();
             Gamertag = reader.ReadString();
         }

+ 108 - 0
MonoGame.Xna.Framework.Net/Net/NetworkDiagnostics.cs

@@ -0,0 +1,108 @@
+using System;
+
+namespace Microsoft.Xna.Framework.Net
+{
+    /// <summary>
+    /// Tracks network statistics and diagnostics for a session.
+    /// </summary>
+    public class NetworkDiagnostics
+    {
+        /// <summary>
+        /// Total packets sent.
+        /// </summary>
+        public long PacketsSent { get; set; }
+        
+        /// <summary>
+        /// Total packets received.
+        /// </summary>
+        public long PacketsReceived { get; set; }
+        
+        /// <summary>
+        /// Total bytes sent.
+        /// </summary>
+        public long BytesSent { get; set; }
+        
+        /// <summary>
+        /// Total bytes received.
+        /// </summary>
+        public long BytesReceived { get; set; }
+        
+        /// <summary>
+        /// Estimated packet loss rate (0.0 to 1.0).
+        /// </summary>
+        public float PacketLossRate { get; set; }
+        
+        /// <summary>
+        /// Average round-trip time.
+        /// </summary>
+        public TimeSpan AverageRtt { get; set; }
+        
+        /// <summary>
+        /// Minimum round-trip time observed.
+        /// </summary>
+        public TimeSpan MinRtt { get; set; } = TimeSpan.MaxValue;
+        
+        /// <summary>
+        /// Maximum round-trip time observed.
+        /// </summary>
+        public TimeSpan MaxRtt { get; set; }
+        
+        /// <summary>
+        /// When the session started.
+        /// </summary>
+        public DateTime SessionStartTime { get; set; } = DateTime.UtcNow;
+        
+        /// <summary>
+        /// How long the session has been running.
+        /// </summary>
+        public TimeSpan Uptime => DateTime.UtcNow - SessionStartTime;
+
+        /// <summary>
+        /// Gets a human-readable summary of diagnostics.
+        /// </summary>
+        public string GetSummary()
+        {
+            return $@"Network Diagnostics:
+  Uptime: {Uptime}
+  Packets: {PacketsReceived} received, {PacketsSent} sent
+  Traffic: {BytesReceived / 1024.0:F1} KB in, {BytesSent / 1024.0:F1} KB out
+  Loss Rate: {PacketLossRate * 100:F2}%
+  RTT: avg={AverageRtt.TotalMilliseconds:F1}ms, min={MinRtt.TotalMilliseconds:F1}ms, max={MaxRtt.TotalMilliseconds:F1}ms";
+        }
+
+        /// <summary>
+        /// Records a packet sent.
+        /// </summary>
+        public void RecordPacketSent(int bytes)
+        {
+            PacketsSent++;
+            BytesSent += bytes;
+        }
+
+        /// <summary>
+        /// Records a packet received.
+        /// </summary>
+        public void RecordPacketReceived(int bytes)
+        {
+            PacketsReceived++;
+            BytesReceived += bytes;
+        }
+
+        /// <summary>
+        /// Records an RTT measurement.
+        /// </summary>
+        public void RecordRtt(TimeSpan rtt)
+        {
+            if (rtt < MinRtt)
+                MinRtt = rtt;
+            if (rtt > MaxRtt)
+                MaxRtt = rtt;
+            
+            // Simple moving average (can be improved with proper averaging)
+            if (AverageRtt == TimeSpan.Zero)
+                AverageRtt = rtt;
+            else
+                AverageRtt = TimeSpan.FromMilliseconds((AverageRtt.TotalMilliseconds * 0.9) + (rtt.TotalMilliseconds * 0.1));
+        }
+    }
+}

+ 5 - 0
MonoGame.Xna.Framework.Net/Net/NetworkMessageRegistry.cs

@@ -36,6 +36,11 @@ namespace Microsoft.Xna.Framework.Net
             Register<JoinAcceptedMessage>(3);
             Register<ReadinessUpdateMessage>(4);
             Register<GameStateChangeMessage>(5);
+            Register<JoinRejectedMessage>(6);
+            Register<HeartbeatMessage>(7);
+            Register<HeartbeatReplyMessage>(8);
+            Register<GamerLeavingMessage>(9); // Phase 2: Graceful leave protocol
+            Register<SessionStateMessage>(10); // Phase 2: Session state synchronization
         }
     }
 }

+ 328 - 19
MonoGame.Xna.Framework.Net/Net/NetworkSession.cs

@@ -28,12 +28,17 @@ namespace Microsoft.Xna.Framework.Net
 
         private INetworkTransport networkTransport;
         internal NetworkSessionState sessionState;
-        private bool disposed;
+        internal bool disposed;
         private bool isHost;
         internal string sessionId;
         private Task receiveTask;
         private CancellationTokenSource cancellationTokenSource;
 
+        // Phase 1 additions
+        private ConnectionHealthMonitor connectionMonitor;
+        private NetworkDiagnostics diagnostics;
+        private INetworkLogger logger;
+
         // Events
         public event EventHandler<GameStartedEventArgs> GameStarted;
         public event EventHandler<GameEndedEventArgs> GameEnded;
@@ -78,6 +83,20 @@ namespace Microsoft.Xna.Framework.Net
             set => networkTransport = value ?? throw new ArgumentNullException(nameof(value));
         }
 
+        /// <summary>
+        /// Gets network diagnostics for this session.
+        /// </summary>
+        public NetworkDiagnostics Diagnostics => diagnostics;
+
+        /// <summary>
+        /// Gets or sets the network logger.
+        /// </summary>
+        public INetworkLogger Logger
+        {
+            get => logger;
+            set => logger = value ?? new NullNetworkLogger();
+        }
+
         /// <summary>
         /// Initializes a new NetworkSession.
         /// </summary>
@@ -100,6 +119,11 @@ namespace Microsoft.Xna.Framework.Net
             networkTransport = new UdpTransport();
             cancellationTokenSource = new CancellationTokenSource();
 
+            // Phase 1: Initialize diagnostics and logging
+            diagnostics = new NetworkDiagnostics();
+            logger = new ConsoleNetworkLogger();
+            connectionMonitor = new ConnectionHealthMonitor();
+
             // Add local gamer
             var gamerGuid = Guid.NewGuid().ToString();
             var localGamer = new LocalNetworkGamer(this, gamerGuid, isHost, $"{SignedInGamer.Current?.Gamertag ?? "Player"}_{gamerGuid.Substring(0, 8)}");
@@ -131,7 +155,8 @@ namespace Microsoft.Xna.Framework.Net
             : this(sessionType, maxGamers, privateGamerSlots, isHost)
         {
             this.sessionId = sessionId;
-            this.sessionState = NetworkSessionState.Lobby;
+            // Don't set state here - let JoinSessionAsync set it to Joining, then acceptance sets it to Lobby
+            // The main constructor already starts the receive loop for SystemLink sessions
         }
 
         /// <summary>
@@ -292,6 +317,9 @@ namespace Microsoft.Xna.Framework.Net
                     // SystemLink: start UDP listener and broadcast session
                     session = new NetworkSession(sessionType, maxGamers, privateGamerSlots, true);
                     session.sessionState = NetworkSessionState.Lobby;
+                    // Phase 1: Start connection monitoring for SystemLink sessions
+                    session.connectionMonitor.StartMonitoring(session);
+                    session.logger?.LogInfo($"Started connection monitoring for session {session.sessionId}");
                     // Use the session's own cancellation token, not the CreateAsync parameter
                     _ = SystemLinkSessionManager.AdvertiseSessionAsync(session, session.cancellationTokenSource.Token); // Fire-and-forget
                     break;
@@ -473,6 +501,28 @@ namespace Microsoft.Xna.Framework.Net
             }
         }
 
+        /// <summary>
+        /// Phase 2: Broadcasts session state change to all gamers.
+        /// </summary>
+        private void BroadcastSessionState(NetworkSessionState newState, string reason)
+        {
+            if (!IsHost)
+                return; // Only host broadcasts state changes
+
+            logger?.LogInfo($"Broadcasting session state change: {newState} - {reason}");
+
+            var stateMessage = new SessionStateMessage
+            {
+                NewState = newState,
+                Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
+                Reason = reason
+            };
+
+            var writer = new PacketWriter();
+            stateMessage.Serialize(writer);
+            SendToAll(writer, SendDataOptions.Reliable);
+        }
+
         /// <summary>
         /// Starts the game.
         /// </summary>
@@ -483,9 +533,12 @@ namespace Microsoft.Xna.Framework.Net
                 sessionState = NetworkSessionState.Playing;
                 OnGameStarted();
 
-                // Host should notify others that the game started
+                // Phase 2: Broadcast state change
                 if (IsHost)
                 {
+                    BroadcastSessionState(NetworkSessionState.Playing, "Host started game");
+                    
+                    // Also send legacy GameStateChangeMessage for backward compatibility
                     var msg = new GameStateChangeMessage { Kind = GameStateChangeKind.Started };
                     var writer = new PacketWriter();
                     msg.Serialize(writer);
@@ -504,8 +557,12 @@ namespace Microsoft.Xna.Framework.Net
                 sessionState = NetworkSessionState.Lobby;
                 OnGameEnded();
 
+                // Phase 2: Broadcast state change
                 if (IsHost)
                 {
+                    BroadcastSessionState(NetworkSessionState.Lobby, "Game ended");
+                    
+                    // Also send legacy GameStateChangeMessage for backward compatibility
                     var msg = new GameStateChangeMessage { Kind = GameStateChangeKind.Ended };
                     var writer = new PacketWriter();
                     msg.Serialize(writer);
@@ -558,12 +615,83 @@ namespace Microsoft.Xna.Framework.Net
         {
             if (e.Message is JoinRequestMessage joinRequest)
             {
-                // Handle join request
-                var newGamer = new NetworkGamer(this, joinRequest.GamerId, isLocal: false, isHost: false, gamertag: joinRequest.Gamertag);
-                AddGamer(newGamer);
-                RegisterGamerEndpoint(newGamer, e.RemoteEndPoint);
+                // Phase 1: Check protocol version
+                if (joinRequest.ProtocolVersion != JoinRequestMessage.CURRENT_PROTOCOL_VERSION)
+                {
+                    logger?.LogWarning($"Join request from {joinRequest.Gamertag} has protocol version {joinRequest.ProtocolVersion}, expected {JoinRequestMessage.CURRENT_PROTOCOL_VERSION}");
+                    
+                    var rejection = new JoinRejectedMessage
+                    {
+                        ErrorCode = NetworkSessionJoinError.ProtocolVersionMismatch,
+                        Reason = $"Protocol version mismatch. Host: v{JoinRequestMessage.CURRENT_PROTOCOL_VERSION}, Client: v{joinRequest.ProtocolVersion}"
+                    };
+                    var rejectWriter = new PacketWriter();
+                    rejection.Serialize(rejectWriter);
+                    networkTransport.Send(rejectWriter.GetData(), e.RemoteEndPoint);
+                    return;
+                }
+
+                // Phase 1: Check if session is full
+                if (AllGamers.Count >= MaxGamers)
+                {
+                    logger?.LogWarning($"Join request from {joinRequest.Gamertag} rejected: session is full ({AllGamers.Count}/{MaxGamers})");
+                    
+                    var rejection = new JoinRejectedMessage
+                    {
+                        ErrorCode = NetworkSessionJoinError.SessionFull,
+                        Reason = "Session is full"
+                    };
+                    var rejectWriter = new PacketWriter();
+                    rejection.Serialize(rejectWriter);
+                    networkTransport.Send(rejectWriter.GetData(), e.RemoteEndPoint);
+                    return;
+                }
 
-                // Send JoinAcceptedMessage back to the sender
+                // Phase 1 FIX: Check if gamer already exists (from previous retry)
+                // CRITICAL: Keep the check and add in the same lock to prevent race conditions
+                NetworkGamer existingGamer = null;
+                bool isNewGamer = false;
+                
+                Console.WriteLine($"[JOIN-DEBUG] Processing join request from {joinRequest.Gamertag} (ID: {joinRequest.GamerId})");
+                Console.WriteLine($"[JOIN-DEBUG] Current gamer count before lock: {AllGamers.Count}");
+                
+                lock (lockObject)
+                {
+                    Console.WriteLine($"[JOIN-DEBUG] Inside lock, checking for existing gamer with ID: {joinRequest.GamerId}");
+                    existingGamer = gamers.FirstOrDefault(g => g.Id == joinRequest.GamerId);
+                    
+                    if (existingGamer == null)
+                    {
+                        // New gamer - create and add atomically while holding lock
+                        Console.WriteLine($"[JOIN-DEBUG] No existing gamer found, creating new gamer");
+                        logger?.LogInfo($"Accepting join request from {joinRequest.Gamertag} (ID: {joinRequest.GamerId})");
+                        var newGamer = new NetworkGamer(this, joinRequest.GamerId, isLocal: false, isHost: false, gamertag: joinRequest.Gamertag);
+                        gamers.Add(newGamer);
+                        Console.WriteLine($"[JOIN-DEBUG] Added new gamer, count now: {gamers.Count}");
+                        existingGamer = newGamer;
+                        isNewGamer = true;
+                    }
+                    else
+                    {
+                        // Gamer already joined (this is a retry) - just resend acceptance
+                        Console.WriteLine($"[JOIN-DEBUG] Found existing gamer: {existingGamer.Gamertag}, this is a retry");
+                        logger?.LogInfo($"Join request from {joinRequest.Gamertag} is a retry (gamer already exists), resending acceptance");
+                    }
+                }
+                
+                Console.WriteLine($"[JOIN-DEBUG] After lock, total gamers: {AllGamers.Count}");
+                
+                // Register endpoint outside lock (uses its own lock internally)
+                RegisterGamerEndpoint(existingGamer, e.RemoteEndPoint);
+                
+                // Notify connection monitor for new gamers only
+                if (isNewGamer)
+                {
+                    connectionMonitor?.OnGamerJoined(existingGamer);
+                    OnGamerJoined(existingGamer);
+                }
+
+                // Send JoinAcceptedMessage back to the sender (always send, even on retry)
                 var joinAccepted = new JoinAcceptedMessage
                 {
                     SessionId = sessionId,
@@ -573,12 +701,15 @@ namespace Microsoft.Xna.Framework.Net
                 var writer = new PacketWriter();
                 joinAccepted.Serialize(writer);
                 networkTransport.Send(writer.GetData(), e.RemoteEndPoint);
+                logger?.LogInfo($"Sent JoinAcceptedMessage to {joinRequest.Gamertag}");
             }
             else if (e.Message is JoinAcceptedMessage joinAccepted)
             {
                 // Client receives confirmation from host; ensure host is present and mapped
                 if (!IsHost)
                 {
+                    logger?.LogInfo($"Received JoinAcceptedMessage from host {joinAccepted.HostGamertag}");
+                    
                     var existingHost = gamers.FirstOrDefault(g => g.IsHost);
                     if (existingHost != null && existingHost.Id != joinAccepted.HostGamerId)
                     {
@@ -591,8 +722,18 @@ namespace Microsoft.Xna.Framework.Net
                     if (existingHost == null)
                         AddGamer(host);
                     RegisterGamerEndpoint(host, e.RemoteEndPoint);
+                    
+                    // Phase 1: Transition to Lobby state when join is accepted
+                    sessionState = NetworkSessionState.Lobby;
+                    logger?.LogInfo("Successfully joined session, now in Lobby state");
                 }
             }
+            else if (e.Message is JoinRejectedMessage joinRejected)
+            {
+                // Phase 1: Handle join rejection
+                logger?.LogError($"Join rejected: {joinRejected.Reason} (Error: {joinRejected.ErrorCode})");
+                // Session state remains in Joining, caller will check this
+            }
             else if (e.Message is PlayerMoveMessage moveMessage)
             {
                 // Identify sender by endpoint mapping
@@ -649,6 +790,69 @@ namespace Microsoft.Xna.Framework.Net
                     OnGameEnded();
                 }
             }
+            else if (e.Message is HeartbeatMessage heartbeat)
+            {
+                // Phase 1: Handle heartbeat from remote gamer
+                connectionMonitor?.OnHeartbeatReceived(heartbeat.GamerId, heartbeat);
+            }
+            else if (e.Message is HeartbeatReplyMessage heartbeatReply)
+            {
+                // Phase 1: Handle heartbeat reply for RTT calculation
+                connectionMonitor?.OnHeartbeatReplyReceived(heartbeatReply.GamerId, heartbeatReply);
+                
+                // Update diagnostics
+                var rtt = TimeSpan.FromMilliseconds(
+                    DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - heartbeatReply.RequestTimestamp);
+                diagnostics?.RecordRtt(rtt);
+            }
+            else if (e.Message is GamerLeavingMessage leaveMessage)
+            {
+                // Phase 2: Handle graceful leave - immediate gamer removal
+                logger?.LogInfo($"Received leave notification from gamer {leaveMessage.GamerId}: {leaveMessage.Reason}");
+                
+                NetworkGamer leavingGamer = null;
+                lock (lockObject)
+                {
+                    leavingGamer = gamers.FirstOrDefault(g => g.Id == leaveMessage.GamerId);
+                }
+                
+                if (leavingGamer != null)
+                {
+                    logger?.LogInfo($"Removing {leavingGamer.Gamertag} from session (graceful leave)");
+                    RemoveGamer(leavingGamer);
+                    
+                    // Notify connection monitor (stops tracking this gamer)
+                    connectionMonitor?.OnGamerLeft(leavingGamer);
+                }
+                else
+                {
+                    logger?.LogWarning($"Received leave notification for unknown gamer {leaveMessage.GamerId}");
+                }
+            }
+            else if (e.Message is SessionStateMessage stateMessage)
+            {
+                // Phase 2: Handle session state synchronization from host
+                if (!IsHost)
+                {
+                    logger?.LogInfo($"Received session state update: {stateMessage.NewState} - {stateMessage.Reason}");
+                    
+                    // Update local session state to match host
+                    var previousState = sessionState;
+                    sessionState = stateMessage.NewState;
+                    
+                    // Raise appropriate events based on state transition
+                    if (previousState == NetworkSessionState.Lobby && stateMessage.NewState == NetworkSessionState.Playing)
+                    {
+                        OnGameStarted();
+                    }
+                    else if (previousState == NetworkSessionState.Playing && stateMessage.NewState == NetworkSessionState.Lobby)
+                    {
+                        OnGameEnded();
+                    }
+                    
+                    logger?.LogInfo($"Session state changed from {previousState} to {sessionState}");
+                }
+            }
 
             // Raise the MessageReceived event
             var handler = MessageReceived;
@@ -682,6 +886,60 @@ namespace Microsoft.Xna.Framework.Net
             SessionEnded?.Invoke(this, new NetworkSessionEndedEventArgs(reason));
         }
 
+        /// <summary>
+        /// Phase 2: Gracefully leaves the session with immediate notification to all gamers.
+        /// Sends GamerLeavingMessage to all remote gamers before disposing.
+        /// </summary>
+        /// <param name="reason">Optional reason for leaving (e.g., "User quit").</param>
+        public void Leave(string reason = "User left session")
+        {
+            if (disposed)
+                return;
+
+            logger?.LogInfo($"Leaving session: {reason}");
+
+            // Send leave notification to all remote gamers
+            var localGamer = LocalGamers.FirstOrDefault();
+            if (localGamer != null && networkTransport != null && networkTransport.IsBound)
+            {
+                var leaveMessage = new GamerLeavingMessage
+                {
+                    GamerId = localGamer.Id,
+                    Reason = reason
+                };
+
+                var writer = new PacketWriter();
+                leaveMessage.Serialize(writer);
+                var data = writer.GetData();
+
+                // Send to all remote gamers
+                lock (lockObject)
+                {
+                    foreach (var gamer in gamers.Where(g => !g.IsLocal))
+                    {
+                        try
+                        {
+                            if (gamerEndpoints.TryGetValue(gamer.Id, out var endpoint))
+                            {
+                                networkTransport.Send(data, endpoint);
+                                logger?.LogInfo($"Sent leave notification to {gamer.Gamertag}");
+                            }
+                        }
+                        catch (Exception ex)
+                        {
+                            logger?.LogError($"Failed to send leave notification to {gamer.Gamertag}", ex);
+                        }
+                    }
+                }
+
+                // Give a brief moment for messages to be sent
+                System.Threading.Thread.Sleep(50);
+            }
+
+            // Now dispose normally
+            Dispose();
+        }
+
         /// <summary>
         /// Disposes the network session.
         /// </summary>
@@ -689,13 +947,22 @@ namespace Microsoft.Xna.Framework.Net
         {
             if (!disposed)
             {
-                cancellationTokenSource?.Cancel();
+                disposed = true; // Set BEFORE cleanup to prevent re-entry
+                
+                try { cancellationTokenSource?.Cancel(); } catch { /* Already disposed */ }
+                
+                // Phase 1: Stop connection monitoring
+                connectionMonitor?.StopMonitoring();
+                
                 // Dispose transport first to unblock ReceiveAsync
                 networkTransport?.Dispose();
+                
                 try { receiveTask?.Wait(1000); } catch { /* ignore */ }
-                cancellationTokenSource?.Dispose();
+                
+                try { cancellationTokenSource?.Dispose(); } catch { /* Already disposed */ }
+                
+                // Raise SessionEnded event (may cause re-entrant Dispose call, now safe)
                 OnSessionEnded(NetworkSessionEndReason.ClientSignedOut);
-                disposed = true;
             }
         }
 
@@ -703,19 +970,28 @@ namespace Microsoft.Xna.Framework.Net
         {
             if (!disposed)
             {
-                cancellationTokenSource?.Cancel();
+                disposed = true; // Set BEFORE cleanup to prevent re-entry
+                
+                try { cancellationTokenSource?.Cancel(); } catch { /* Already disposed */ }
+                
+                // Phase 1: Stop connection monitoring
+                connectionMonitor?.StopMonitoring();
+                
                 // Dispose transport first to unblock any pending ReceiveAsync
                 if (networkTransport is IAsyncDisposable asyncTransport)
                     await asyncTransport.DisposeAsync();
                 else
                     networkTransport?.Dispose();
+                    
                 if (receiveTask != null)
                 {
                     await Task.WhenAny(receiveTask, Task.Delay(1000));
                 }
-                cancellationTokenSource?.Dispose();
+                
+                try { cancellationTokenSource?.Dispose(); } catch { /* Already disposed */ }
+                
+                // Raise SessionEnded event (may cause re-entrant Dispose call, now safe)
                 OnSessionEnded(NetworkSessionEndReason.ClientSignedOut);
-                disposed = true;
             }
         }
 
@@ -838,6 +1114,8 @@ namespace Microsoft.Xna.Framework.Net
                 gamers.Remove(gamer);
                 gamerEndpoints.Remove(gamer.Id);
             }
+            // Phase 1: Notify connection monitor
+            connectionMonitor?.OnGamerLeft(gamer);
             OnGamerLeft(gamer);
         }
 
@@ -858,10 +1136,13 @@ namespace Microsoft.Xna.Framework.Net
                 try
                 {
                     networkTransport.Send(data, endpoint);
+                    // Phase 1: Record sent packet in diagnostics
+                    diagnostics?.RecordPacketSent(data.Length);
                 }
                 catch (Exception ex)
                 {
-                    Debug.WriteLine($"Failed to send data to {gamer.Gamertag}: {ex.Message}");
+                    // Phase 1: Use logger
+                    logger?.LogError($"Failed to send data to {gamer.Gamertag}: {ex.Message}", ex);
                 }
             }
         }
@@ -894,6 +1175,9 @@ namespace Microsoft.Xna.Framework.Net
                     var (data, senderEndpoint) = await networkTransport.ReceiveAsync();
                     if (data.Length > 0)
                     {
+                        // Phase 1: Record received packet in diagnostics
+                        diagnostics?.RecordPacketReceived(data.Length);
+                        
                         // Identify sender by endpoint mapping
                         NetworkGamer senderGamer = null;
                         lock (lockObject)
@@ -930,18 +1214,43 @@ namespace Microsoft.Xna.Framework.Net
 
                         if (!handledFramework)
                         {
-                            // Application payload. Enqueue for LocalGamer.ReceiveData
+                            // Application payload. Enqueue for all LocalGamers in this session
                             if (senderGamer != null)
                             {
-                                NetworkGamer.LocalGamer?.EnqueueIncomingPacket(data, senderGamer);
+                                // Enqueue to all local gamers so they can receive via ReceiveData()
+                                foreach (var localGamer in LocalGamers)
+                                {
+                                    localGamer.EnqueueIncomingPacket(data, senderGamer);
+                                }
                             }
                         }
                     }
                 }
-                catch (ObjectDisposedException) { break; }
+                catch (ObjectDisposedException) 
+                { 
+                    // Session disposed - exit gracefully
+                    break; 
+                }
+                catch (OperationCanceledException)
+                {
+                    // Cancellation token triggered - exit gracefully
+                    break;
+                }
+                catch (System.Net.Sockets.SocketException sex) when (sex.SocketErrorCode == System.Net.Sockets.SocketError.OperationAborted)
+                {
+                    // Socket operation canceled during shutdown - exit gracefully
+                    break;
+                }
                 catch (Exception ex)
                 {
-                    Debug.WriteLine($"ReceiveLoop error: {ex.Message}");
+                    // Only log unexpected errors
+                    if (!cancellationToken.IsCancellationRequested)
+                    {
+                        logger?.LogError($"ReceiveLoop error: {ex.Message}", ex);
+                    }
+                    // Exit if cancellation was requested
+                    if (cancellationToken.IsCancellationRequested)
+                        break;
                 }
             }
         }

+ 53 - 0
MonoGame.Xna.Framework.Net/Net/SessionStateMessage.cs

@@ -0,0 +1,53 @@
+using Microsoft.Xna.Framework.Net;
+using System;
+
+namespace Microsoft.Xna.Framework.Net
+{
+    /// <summary>
+    /// Message broadcast by host to synchronize session state transitions across all clients.
+    /// Ensures all players see the same state (Lobby → Playing → Ended) at the same time.
+    /// </summary>
+    public class SessionStateMessage : INetworkMessage
+    {
+        /// <summary>
+        /// Message type ID for SessionStateMessage.
+        /// </summary>
+        public byte MessageType => 10;
+
+        /// <summary>
+        /// The new session state.
+        /// </summary>
+        public NetworkSessionState NewState { get; set; }
+
+        /// <summary>
+        /// UTC timestamp when the state change occurred (milliseconds since Unix epoch).
+        /// </summary>
+        public long Timestamp { get; set; }
+
+        /// <summary>
+        /// Optional reason for the state change (e.g., "Host started game", "Game completed").
+        /// </summary>
+        public string Reason { get; set; }
+
+        /// <summary>
+        /// Serializes the message to a packet writer.
+        /// </summary>
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(MessageType);
+            writer.Write((byte)NewState);
+            writer.Write(Timestamp);
+            writer.Write(Reason ?? string.Empty);
+        }
+
+        /// <summary>
+        /// Deserializes the message from a packet reader.
+        /// </summary>
+        public void Deserialize(PacketReader reader)
+        {
+            NewState = (NetworkSessionState)reader.ReadByte();
+            Timestamp = reader.ReadInt64();
+            Reason = reader.ReadString();
+        }
+    }
+}

+ 66 - 17
MonoGame.Xna.Framework.Net/Net/SystemLinkSessionManager.cs

@@ -55,7 +55,7 @@ namespace Microsoft.Xna.Framework.Net
                         broadcastCount++;
                         Console.WriteLine($"[BROADCAST] Sent broadcast #{broadcastCount} - SessionID: {session.sessionId}, Gamers: {session.AllGamers.Count}/{session.MaxGamers}");
 
-                        await Task.Delay(1000, cancellationToken); // Broadcast every second
+                        await Task.Delay(750, cancellationToken); // Broadcast every 750ms for faster discovery
                     }
 
                     Console.WriteLine($"[BROADCAST] Stopped broadcasting. Reason: Cancelled={cancellationToken.IsCancellationRequested}, Full={session.AllGamers.Count >= session.MaxGamers}, Ended={session.sessionState == NetworkSessionState.Ended}");
@@ -83,9 +83,10 @@ namespace Microsoft.Xna.Framework.Net
                     udpClient.EnableBroadcast = true;
                     // DON'T set ReceiveTimeout - it interferes with ReceiveAsync()
 
-                    // Listen for 2 seconds to catch at least 2 broadcast cycles (hosts broadcast every 1 second)
+                    // Phase 1: Listen for 1.5 seconds to catch at least 1 broadcast cycle (hosts broadcast every 1 second)
+                    // Reduced from 2.5s for faster discovery while still being reliable
                     var startTime = DateTime.UtcNow;
-                    var endTime = startTime.AddSeconds(2);
+                    var endTime = startTime.AddSeconds(1.5);
                     int receiveAttempts = 0;
                     int packetsReceived = 0;
 
@@ -94,9 +95,9 @@ namespace Microsoft.Xna.Framework.Net
                         try
                         {
                             receiveAttempts++;
-                            // Try to receive with timeout
+                            // Try to receive with reduced timeout for faster response
                             var receiveTask = udpClient.ReceiveAsync();
-                            var timeoutTask = Task.Delay(150, cancellationToken);
+                            var timeoutTask = Task.Delay(100, cancellationToken);
                             var completedTask = await Task.WhenAny(receiveTask, timeoutTask);
 
                             if (completedTask == receiveTask)
@@ -197,14 +198,19 @@ namespace Microsoft.Xna.Framework.Net
 
         public static async Task<NetworkSession> JoinSessionAsync(AvailableNetworkSession availableSession, CancellationToken cancellationToken)
         {
-            // Minimal viable join: create a new client session and remember the host endpoint
-            await Task.Delay(10, cancellationToken);
+            // Phase 1: Reliable join with timeout and retry
+            const int MAX_RETRIES = 3;
+            const int TIMEOUT_MS = 300; // Faster timeout for LAN (reduced from 500ms)
+
+            Console.WriteLine($"[JOIN] Starting join process for session {availableSession.SessionId}");
+
+            // Create client session in Joining state
             var session = new NetworkSession(NetworkSessionType.SystemLink,
                 availableSession.OpenPublicGamerSlots + availableSession.CurrentGamerCount,
                 availableSession.OpenPrivateGamerSlots,
                 false,
                 availableSession.SessionId);
-            session.sessionState = NetworkSessionState.Lobby;
+            session.sessionState = NetworkSessionState.Joining; // Phase 1: Use new Joining state
 
             // Copy session properties from AvailableNetworkSession to NetworkSession
             foreach (var kvp in availableSession.SessionProperties)
@@ -217,25 +223,68 @@ namespace Microsoft.Xna.Framework.Net
             }
 
             // Create a synthetic remote host gamer and record endpoint so SendToAll can reach host
-            if (availableSession.HostEndpoint != null)
+            if (availableSession.HostEndpoint == null)
+            {
+                throw new NetworkSessionJoinException("Host endpoint is null", NetworkSessionJoinError.SessionNotFound);
+            }
+
+            var hostGamer = new NetworkGamer(session, Guid.NewGuid().ToString(), isLocal: false, isHost: true, gamertag: availableSession.HostGamertag);
+            session.GetType().GetMethod("AddGamer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+                ?.Invoke(session, new object[] { hostGamer });
+            session.RegisterGamerEndpoint(hostGamer, availableSession.HostEndpoint);
+
+            // Phase 1: Send join request with retry logic
+            for (int attempt = 0; attempt < MAX_RETRIES; attempt++)
             {
-                var hostGamer = new NetworkGamer(session, Guid.NewGuid().ToString(), isLocal: false, isHost: true, gamertag: availableSession.HostGamertag);
-                session.GetType().GetMethod("AddGamer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
-                    ?.Invoke(session, new object[] { hostGamer });
-                session.RegisterGamerEndpoint(hostGamer, availableSession.HostEndpoint);
+                Console.WriteLine($"[JOIN] Sending join request (attempt {attempt + 1}/{MAX_RETRIES})");
+
+                // CRITICAL: Use the session's own local gamer, not the static NetworkGamer.LocalGamer
+                // The static property can be stale when multiple sessions exist (e.g., testing on same machine)
+                var localGamer = session.LocalGamers.FirstOrDefault();
+                if (localGamer == null)
+                    throw new InvalidOperationException("No local gamer found in session");
+
+                Console.WriteLine($"[JOIN] Sending as gamer: {localGamer.Gamertag} (ID: {localGamer.Id})");
 
-                // Proactively send a JoinRequest to the host so it can add this client
                 var joinRequest = new JoinRequestMessage
                 {
-                    GamerId = NetworkGamer.LocalGamer.Id,
-                    Gamertag = NetworkGamer.LocalGamer.Gamertag
+                    GamerId = localGamer.Id,
+                    Gamertag = localGamer.Gamertag,
+                    ProtocolVersion = JoinRequestMessage.CURRENT_PROTOCOL_VERSION
                 };
                 var writer = new PacketWriter();
                 joinRequest.Serialize(writer);
                 session.NetworkTransport.Send(writer.GetData(), availableSession.HostEndpoint);
+
+                // Wait for response (NetworkSession.OnMessageReceived will update state to Lobby if accepted)
+                var waitStart = DateTime.UtcNow;
+                while ((DateTime.UtcNow - waitStart).TotalMilliseconds < TIMEOUT_MS)
+                {
+                    if (session.sessionState == NetworkSessionState.Lobby)
+                    {
+                        Console.WriteLine($"[JOIN] Successfully joined session after {attempt + 1} attempt(s)");
+                        // Phase 1: Start connection monitoring for client
+                        session.GetType().GetField("connectionMonitor", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+                            ?.GetValue(session)
+                            ?.GetType().GetMethod("StartMonitoring")
+                            ?.Invoke(session.GetType().GetField("connectionMonitor", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(session), new object[] { session });
+                        return session;
+                    }
+
+                    // Check if we received rejection (would still be in Joining state but we can check for it)
+                    // For now, just wait
+                    await Task.Delay(50, cancellationToken);
+                }
+
+                Console.WriteLine($"[JOIN] Attempt {attempt + 1} timed out");
             }
 
-            return session;
+            // After MAX_RETRIES attempts, give up
+            Console.WriteLine($"[JOIN] Failed to join session after {MAX_RETRIES} attempts");
+            session.Dispose();
+            throw new NetworkSessionJoinException(
+                $"Failed to join session after {MAX_RETRIES} attempts. Host may be unreachable or session is full.",
+                NetworkSessionJoinError.Timeout);
         }
     }
 }

+ 18 - 3
Peer2Peer/Core/PeerToPeerGame.cs

@@ -32,7 +32,9 @@ namespace PeerToPeer
 		SpriteBatch spriteBatch;
 		SpriteFont font;
 		KeyboardState currentKeyboardState;
+		KeyboardState previousKeyboardState;
 		GamePadState currentGamePadState;
+		GamePadState previousGamePadState;
 		TouchCollection currentTouchState;
 		NetworkSession networkSession;
 		PacketWriter packetWriter = new PacketWriter();
@@ -431,6 +433,9 @@ namespace PeerToPeer
 		/// </summary>
 		private void HandleInput()
 		{
+			previousKeyboardState = currentKeyboardState;
+			previousGamePadState = currentGamePadState;
+			
 			currentKeyboardState = Keyboard.GetState();
 			currentGamePadState = GamePad.GetState(PlayerIndex.One);
 			currentTouchState = TouchPanel.GetState();
@@ -438,6 +443,13 @@ namespace PeerToPeer
 			// Check for exit.
 			if (IsActive && IsPressed(Keys.Escape, Buttons.Back))
 			{
+				// Phase 2: Gracefully leave session before exiting
+				if (networkSession != null)
+				{
+					Console.WriteLine("[GAME] Gracefully leaving session...");
+					networkSession.Leave("User quit");
+					networkSession = null;
+				}
 				Exit();
 			}
 
@@ -477,12 +489,15 @@ namespace PeerToPeer
 		}
 
 		/// <summary>
-		/// Checks if the specified button is pressed on either keyboard or gamepad.
+		/// Checks if the specified button was just pressed (not held) on either keyboard or gamepad.
 		/// </summary>
 		bool IsPressed(Keys key, Buttons button)
 		{
-			return (currentKeyboardState.IsKeyDown(key) ||
-			currentGamePadState.IsButtonDown(button));
+			// Check for key/button transition: was up last frame, is down this frame
+			bool keyJustPressed = currentKeyboardState.IsKeyDown(key) && !previousKeyboardState.IsKeyDown(key);
+			bool buttonJustPressed = currentGamePadState.IsButtonDown(button) && !previousGamePadState.IsButtonDown(button);
+			
+			return keyJustPressed || buttonJustPressed;
 		}
 
 

Some files were not shown because too many files changed in this diff