diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ba258c2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,110 @@ + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_prefer_braces = true:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule.severity = warning +dotnet_naming_rule.unity_serialized_field_rule.style = upper_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance +dotnet_separate_import_directive_groups = true +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# ReSharper properties +resharper_align_multiline_argument = true +resharper_align_multiline_for_stmt = true +resharper_align_multiple_declaration = true +resharper_align_multline_type_parameter_constrains = true +resharper_align_multline_type_parameter_list = true +resharper_align_tuple_components = false +resharper_blank_lines_around_auto_property = 0 +resharper_blank_lines_around_local_method = 0 +resharper_blank_lines_around_single_line_type = 0 +resharper_blank_lines_before_control_transfer_statements = 1 +resharper_blank_lines_inside_namespace = 1 +resharper_braces_redundant = true +resharper_csharp_align_multiline_argument = false +resharper_csharp_blank_lines_around_field = 0 +resharper_csharp_empty_block_style = together_same_line +resharper_csharp_int_align_comments = true +resharper_csharp_keep_blank_lines_in_code = 1 +resharper_csharp_stick_comment = false +resharper_csharp_wrap_extends_list_style = chop_if_long +resharper_force_attribute_style = join +resharper_indent_nested_foreach_stmt = true +resharper_indent_nested_for_stmt = true +resharper_indent_nested_while_stmt = true +resharper_indent_preprocessor_directives = normal +resharper_int_align_nested_ternary = true +resharper_int_align_property_patterns = true +resharper_int_align_switch_expressions = true +resharper_int_align_switch_sections = true +resharper_local_function_body = expression_body +resharper_max_formal_parameters_on_line = 5 +resharper_place_method_attribute_on_same_line = if_owner_is_single_line +resharper_place_type_attribute_on_same_line = if_owner_is_single_line +resharper_show_autodetect_configure_formatting_tip = false +resharper_space_around_arrow_op = true +resharper_space_within_single_line_array_initializer_braces = true +resharper_use_indent_from_vs = false + +# ReSharper inspection severities +resharper_arrange_attributes_highlighting = hint +resharper_arrange_constructor_or_destructor_body_highlighting = hint +resharper_arrange_local_function_body_highlighting = hint +resharper_arrange_method_or_operator_body_highlighting = suggestion +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_check_namespace_highlighting = hint +resharper_enforce_do_while_statement_braces_highlighting = hint +resharper_enforce_fixed_statement_braces_highlighting = hint +resharper_enforce_foreach_statement_braces_highlighting = hint +resharper_enforce_for_statement_braces_highlighting = hint +resharper_enforce_if_statement_braces_highlighting = hint +resharper_enforce_lock_statement_braces_highlighting = hint +resharper_enforce_using_statement_braces_highlighting = hint +resharper_enforce_while_statement_braces_highlighting = hint +resharper_redundant_base_qualifier_highlighting = warning +resharper_remove_redundant_braces_highlighting = hint +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning + +[*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,paml,razor,resw,resx,shader,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 diff --git a/.gitignore b/.gitignore index 1bc915c..da23fc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,156 +1,6 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.sln.docstates - -# Build results - -[Dd]ebug/ -[Rr]elease/ -x64/ -build/ -[Bb]in/ -[Oo]bj/ - -# Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets -!packages/*/build/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -*_i.c -*_p.c -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.log -*.scc - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -*.ncrunch* -.*crunch*.local.xml - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.Publish.xml - -# NuGet Packages Directory -## TODO: If you have NuGet Package Restore enabled, uncomment the next line -#packages/ - -# Windows Azure Build Output -csx -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -sql/ -*.Cache -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.[Pp]ublish.xml -*.pfx -*.publishsettings - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file to a newer -# Visual Studio version. Backup files are not needed, because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -App_Data/*.mdf -App_Data/*.ldf - - -#LightSwitch generated files -GeneratedArtifacts/ -_Pvt_Extensions/ -ModelManifest.xml - -# ========================= -# Windows detritus -# ========================= - -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Mac desktop service store files -.DS_Store +bin/ +obj/ +.idea/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ diff --git a/2048.cs b/2048.cs deleted file mode 100644 index f5b8fc1..0000000 --- a/2048.cs +++ /dev/null @@ -1,302 +0,0 @@ -namespace Kfl.Game2048 -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using System.Threading.Tasks; - - class Program - { - static void Main(string[] args) - { - Game game = new Game(); - game.Run(); - } - } - - class Game - { - public ulong Score { get; private set; } - public ulong[,] Board { get; private set; } - - private readonly int nRows; - private readonly int nCols; - private readonly Random random = new Random(); - - public Game() - { - this.Board = new ulong[4, 4]; - this.nRows = this.Board.GetLength(0); - this.nCols = this.Board.GetLength(1); - this.Score = 0; - } - - public void Run() - { - bool hasUpdated = true; - do - { - if (hasUpdated) - { - PutNewValue(); - } - - Display(); - - if (IsDead()) - { - using (new ColorOutput(ConsoleColor.Red)) - { - Console.WriteLine("YOU ARE DEAD!!!"); - break; - } - } - - Console.WriteLine("Use arrow keys to move the tiles. Press Ctrl-C to exit."); - ConsoleKeyInfo input = Console.ReadKey(true); // BLOCKING TO WAIT FOR INPUT - Console.WriteLine(input.Key.ToString()); - - switch (input.Key) - { - case ConsoleKey.UpArrow: - hasUpdated = Update(Direction.Up); - break; - - case ConsoleKey.DownArrow: - hasUpdated = Update(Direction.Down); - break; - - case ConsoleKey.LeftArrow: - hasUpdated = Update(Direction.Left); - break; - - case ConsoleKey.RightArrow: - hasUpdated = Update(Direction.Right); - break; - - default: - hasUpdated = false; - break; - } - } - while (true); // use CTRL-C to break out of loop - - Console.WriteLine("Press any key to quit..."); - Console.Read(); - } - - private static ConsoleColor GetNumberColor(ulong num) - { - switch (num) - { - case 0: - return ConsoleColor.DarkGray; - case 2: - return ConsoleColor.Cyan; - case 4: - return ConsoleColor.Magenta; - case 8: - return ConsoleColor.Red; - case 16: - return ConsoleColor.Green; - case 32: - return ConsoleColor.Yellow; - case 64: - return ConsoleColor.Yellow; - case 128: - return ConsoleColor.DarkCyan; - case 256: - return ConsoleColor.Cyan; - case 512: - return ConsoleColor.DarkMagenta; - case 1024: - return ConsoleColor.Magenta; - default: - return ConsoleColor.Red; - } - } - - private static bool Update(ulong[,] board, Direction direction, out ulong score) - { - int nRows = board.GetLength(0); - int nCols = board.GetLength(1); - - score = 0; - bool hasUpdated = false; - - // You shouldn't be dead at this point. We always check if you're dead at the end of the Update() - - // Drop along row or column? true: process inner along row; false: process inner along column - bool isAlongRow = direction == Direction.Left || direction == Direction.Right; - - // Should we process inner dimension in increasing index order? - bool isIncreasing = direction == Direction.Left || direction == Direction.Up; - - int outterCount = isAlongRow ? nRows : nCols; - int innerCount = isAlongRow ? nCols : nRows; - int innerStart = isIncreasing ? 0 : innerCount - 1; - int innerEnd = isIncreasing ? innerCount - 1 : 0; - - Func drop = isIncreasing - ? new Func(innerIndex => innerIndex - 1) - : new Func(innerIndex => innerIndex + 1); - - Func reverseDrop = isIncreasing - ? new Func(innerIndex => innerIndex + 1) - : new Func(innerIndex => innerIndex - 1); - - Func getValue = isAlongRow - ? new Func((x, i, j) => x[i, j]) - : new Func((x, i, j) => x[j, i]); - - Action setValue = isAlongRow - ? new Action((x, i, j, v) => x[i, j] = v) - : new Action((x, i, j, v) => x[j, i] = v); - - Func innerCondition = index => Math.Min(innerStart, innerEnd) <= index && index <= Math.Max(innerStart, innerEnd); - - for (int i = 0; i < outterCount; i++) - { - for (int j = innerStart; innerCondition(j); j = reverseDrop(j)) - { - if (getValue(board, i, j) == 0) - { - continue; - } - - int newJ = j; - do - { - newJ = drop(newJ); - } - // Continue probing along as long as we haven't hit the boundary and the new position isn't occupied - while (innerCondition(newJ) && getValue(board, i, newJ) == 0); - - if (innerCondition(newJ) && getValue(board, i, newJ) == getValue(board, i, j)) - { - // We did not hit the canvas boundary (we hit a node) AND no previous merge occurred AND the nodes' values are the same - // Let's merge - ulong newValue = getValue(board, i, newJ) * 2; - setValue(board, i, newJ, newValue); - setValue(board, i, j, 0); - - hasUpdated = true; - score += newValue; - } - else - { - // Reached the boundary OR... - // we hit a node with different value OR... - // we hit a node with same value BUT a prevous merge had occurred - // - // Simply stack along - newJ = reverseDrop(newJ); // reverse back to its valid position - if (newJ != j) - { - // there's an update - hasUpdated = true; - } - - ulong value = getValue(board, i, j); - setValue(board, i, j, 0); - setValue(board, i, newJ, value); - } - } - } - - return hasUpdated; - } - - private bool Update(Direction dir) - { - ulong score; - bool isUpdated = Game.Update(this.Board, dir, out score); - this.Score += score; - return isUpdated; - } - - private bool IsDead() - { - ulong score; - foreach (Direction dir in new Direction[] { Direction.Down, Direction.Up, Direction.Left, Direction.Right }) - { - ulong[,] clone = (ulong[,])Board.Clone(); - if (Game.Update(clone, dir, out score)) - { - return false; - } - } - - // tried all directions. none worked. - return true; - } - - private void Display() - { - Console.Clear(); - Console.WriteLine(); - for (int i = 0; i < nRows; i++) - { - for (int j = 0; j < nCols; j++) - { - using (new ColorOutput(Game.GetNumberColor(Board[i, j]))) - { - Console.Write(string.Format("{0,6}", Board[i, j])); - } - } - - Console.WriteLine(); - Console.WriteLine(); - } - - Console.WriteLine("Score: {0}", this.Score); - Console.WriteLine(); - } - - private void PutNewValue() - { - // Find all empty slots - List> emptySlots = new List>(); - for (int iRow = 0; iRow < nRows; iRow++) - { - for (int iCol = 0; iCol < nCols; iCol++) - { - if (Board[iRow, iCol] == 0) - { - emptySlots.Add(new Tuple(iRow, iCol)); - } - } - } - - // We should have at least 1 empty slot. Since we know the user is not dead - int iSlot = random.Next(0, emptySlots.Count); // randomly pick an empty slot - ulong value = random.Next(0, 100) < 95 ? (ulong)2 : (ulong)4; // randomly pick 2 (with 95% chance) or 4 (rest of the chance) - Board[emptySlots[iSlot].Item1, emptySlots[iSlot].Item2] = value; - } - - #region Utility Classes - enum Direction - { - Up, - Down, - Right, - Left, - } - - class ColorOutput : IDisposable - { - public ColorOutput(ConsoleColor fg, ConsoleColor bg = ConsoleColor.Black) - { - Console.ForegroundColor = fg; - Console.BackgroundColor = bg; - } - - public void Dispose() - { - Console.ResetColor(); - } - } - #endregion Utility Classes - } -} \ No newline at end of file diff --git a/2048.csproj b/2048.csproj deleted file mode 100644 index c8fda81..0000000 --- a/2048.csproj +++ /dev/null @@ -1,89 +0,0 @@ - - - - Debug - x86 - {FC59096B-CDA3-44C0-AF85-073E8F284A10} - Exe - false - ConsoleApplication - v4.0 - Client - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - x86 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - x86 - - - _2048 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/C#/Board.cs b/C#/Board.cs new file mode 100644 index 0000000..70b615a --- /dev/null +++ b/C#/Board.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Core_2048 +{ + + public class Board : IEnumerable> + { + public delegate void Mapper(T value, int row, int column); + + private readonly T[,] _values; + + public Board(int height, int width, Func valueInitiator) + { + Height = height; + Width = width; + + _values = new T[Height, Width]; + ForEach((value, row, column) => Set(row, column, valueInitiator())); + } + + public int Height { get; } + public int Width { get; } + + public IEnumerator> GetEnumerator() + { + for (var row = 0; row < _values.GetLength(0); row++) + { + for (var column = 0; column < _values.GetLength(1); column++) + { + yield return new Cell() + { + Row = row, + Column = column, + Value = _values[row, column] + }; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public T Get(int row, int column) + { + return _values[row, column]; + } + + public Board Set(int row, int column, T value) + { + _values[row, column] = value; + + return this; + } + + public Board Set(Cell cell) + { + Set(cell.Row, cell.Column, cell.Value); + + return this; + } + + public void ForEach(Mapper mapper) + { + foreach (var element in this) + { + mapper.Invoke(element.Value, element.Row, element.Column); + } + } + + public override bool Equals(object obj) + { + if (!(obj is Board anotherBoard)) + { + return false; + } + + if (Height != anotherBoard.Height) + { + return false; + } + + if (Width != anotherBoard.Width) + { + return false; + } + + var result = true; + + ForEach((value, row, column) => + { + var anotherValue = anotherBoard.Get(row, column); + result = result + && value.GetType() == anotherValue.GetType() + && Equals(value, anotherValue) + && result; + }); + + return result; + } + + protected bool Equals(Board other) + { + return Equals(_values, other._values) && Height == other.Height && Width == other.Width; + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = _values != null ? _values.GetHashCode() : 0; + hashCode = (hashCode * 397) ^ Height; + hashCode = (hashCode * 397) ^ Width; + + return hashCode; + } + } + } + +} diff --git a/C#/BoardBehavior.cs b/C#/BoardBehavior.cs new file mode 100644 index 0000000..3ea8e80 --- /dev/null +++ b/C#/BoardBehavior.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; + +namespace Core_2048 +{ + + public partial class BoardBehavior + { + private readonly Board _board; + private readonly ICellBehavior _cellBehavior; + private readonly ICellGenerator _cellGenerator; + + private Action, Cell>> _updated; + + public BoardBehavior(Board board, ICellGenerator cellGenerator, ICellBehavior cellBehavior, + Action, Cell>> updated = null) + { + _updated = updated; + _board = board; + _cellGenerator = cellGenerator; + _cellBehavior = cellBehavior; + } + + public void AddUpdatedListener(Action, Cell>> action) + { + _updated += action; + } + + public void RemoveUpdatedListener(Action, Cell>> action) + { + _updated -= action; + } + + public void AddNew() + { + var element = _cellGenerator.GetNewElement(_board); + if (element == null) + { + return; + } + + _board.Set(element); + } + + public void Update(bool isAlongRow, bool isIncreasing) + { + var changes = CalculateChanges(isAlongRow, isIncreasing); + var updateMap = new Dictionary, Cell>(); + foreach (var changeElementAction in changes) + { + var prev = changeElementAction.Previous; + var next = changeElementAction.Next; + _board.Set(prev.Row, prev.Column, _cellBehavior.GetCellBaseValue()) + .Set(next); + updateMap.Add(prev, next); + } + + _updated?.Invoke(updateMap); + } + + public IEnumerable CalculateChanges(bool isAlongRow, bool isIncreasing) + { + return new UpdateLoop(_cellBehavior, _board, isAlongRow, isIncreasing); + } + + public class ChangeElementAction + { + public Cell Next; + public Cell Previous; + } + } + +} diff --git a/C#/BoardHelper.cs b/C#/BoardHelper.cs new file mode 100644 index 0000000..2d691fd --- /dev/null +++ b/C#/BoardHelper.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Core_2048 +{ + + public static class BoardHelper + { + public static IEnumerable> GetEmpties(Board board, Predicate emptyChecker) + { + return board.Where(value => emptyChecker.Invoke(value.Value)); + } + } + +} diff --git a/C#/Cell.cs b/C#/Cell.cs new file mode 100644 index 0000000..090ce54 --- /dev/null +++ b/C#/Cell.cs @@ -0,0 +1,11 @@ +namespace Core_2048 +{ + + public class Cell + { + public int Column; + public int Row; + public T Value; + } + +} diff --git a/C#/ChangeElementAction.cs b/C#/ChangeElementAction.cs new file mode 100644 index 0000000..0865397 --- /dev/null +++ b/C#/ChangeElementAction.cs @@ -0,0 +1,13 @@ +namespace Core_2048 +{ + + public partial class Core + { + public class ChangeElementAction + { + public Element Next; + public Element Previous; + } + } + +} diff --git a/C#/ICellBehavior.cs b/C#/ICellBehavior.cs new file mode 100644 index 0000000..87fbc15 --- /dev/null +++ b/C#/ICellBehavior.cs @@ -0,0 +1,12 @@ +namespace Core_2048 +{ + + public interface ICellBehavior + { + bool IsBaseCell(Cell cell); + bool IsMergeCells(Cell previous, Cell next); + T MergeCells(Cell previous, Cell next); + T GetCellBaseValue(); + } + +} diff --git a/C#/ICellGenerator.cs b/C#/ICellGenerator.cs new file mode 100644 index 0000000..962437d --- /dev/null +++ b/C#/ICellGenerator.cs @@ -0,0 +1,10 @@ + +namespace Core_2048 +{ + + public interface ICellGenerator + { + Cell GetNewElement(Board board); + } + +} diff --git a/C#/IRandomizer.cs b/C#/IRandomizer.cs new file mode 100644 index 0000000..caee1ad --- /dev/null +++ b/C#/IRandomizer.cs @@ -0,0 +1,9 @@ +namespace Core_2048 +{ + + public interface IRandomizer + { + int Random(int min, int max); + } + +} diff --git a/C#/IValueBehavior.cs b/C#/IValueBehavior.cs new file mode 100644 index 0000000..d21e206 --- /dev/null +++ b/C#/IValueBehavior.cs @@ -0,0 +1,53 @@ +namespace Core_2048 +{ + + public partial class Core + { + /// + /// Interface for defining interaction with element values + /// + public interface IValueBehavior + { + /// + /// The value for setting empty elements on a board. + /// + T BaseValue { get; } + + /// + /// Check value is a base value. Example, is 0. + /// + /// Value from element on a board + /// If true, value will not be to change + /// + bool IsBase(T value); + + /// + /// Create a new value based on the value from the previous element and the next. + /// + /// Value from previous row, column element + /// Value from next row, column element + /// Value for next element + T Merge(T previous, T next); + + /// + /// Determine whether to merge a value from previous element to another element. + /// + /// Value from element on a board + /// Value from element on a board + /// If true, value will be merge + bool IsMerge(T previous, T next); + + /// + /// Instantiate value from another values for setting element with new class of value. + /// For creating elements with value of custom class. + /// + /// Value not from element + /// Index in outer array dimensions + /// Index in inner array dimensions + /// + /// Value for setting in element with row and column + T Instantiate(T prefab, int row, int column); + } + } + +} diff --git a/C#/RandomCellGenerator.cs b/C#/RandomCellGenerator.cs new file mode 100644 index 0000000..4319143 --- /dev/null +++ b/C#/RandomCellGenerator.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Core_2048 +{ + + public class RandomCellGenerator : ICellGenerator + { + private readonly Dictionary _pool = new Dictionary(); + private readonly Random _random = new Random(); + private readonly Predicate _emptyChecker; + + private int _allPercentage; + + public RandomCellGenerator(Predicate emptyChecker) + { + _emptyChecker = emptyChecker; + } + + public Cell GetNewElement(Board board) + { + var empties = BoardHelper.GetEmpties(board, _emptyChecker).ToList(); + if (empties.Count == 0) + { + return null; + } + + var index = _random.Next(0, empties.Count()); + var randomPosition = empties[index]; + + var predicatePool = new Dictionary, T>(); + _pool.Aggregate(0, (percentage, pair) => + { + var min = percentage; + var max = percentage + pair.Value; + predicatePool.Add(checkPercentage => min < checkPercentage && checkPercentage <= max, pair.Key); + + return max; + }); + var resultPercentage = _random.Next(1, _allPercentage); + var resultElements = from pair in predicatePool + let predicate = pair.Key + let element = pair.Value + where predicate.Invoke(resultPercentage) + select element; + + return new Cell + { + Row = randomPosition.Row, + Column = randomPosition.Column, + Value = resultElements.First() + }; + } + + public void AddToPool(T value, int percentage) + { + _pool.Add(value, percentage); + _allPercentage += percentage; + } + + public void AddToPool(T value) + { + var percentage = _pool.Count != 0 ? _allPercentage / _pool.Count : 1; + AddToPool(value, percentage); + } + } + +} diff --git a/C#/UpdateBehavior.cs b/C#/UpdateBehavior.cs new file mode 100644 index 0000000..abf7af3 --- /dev/null +++ b/C#/UpdateBehavior.cs @@ -0,0 +1,54 @@ +using System; + +namespace Core_2048 +{ + + public partial class Core + { + internal class UpdateBehavior + { + private readonly Board _board; + private readonly bool _isAlongRow; + private readonly bool _isIncreasing; + + public UpdateBehavior(Board board, bool isIncreasing, bool isAlongRow) + { + _board = board ?? throw new ArgumentNullException(nameof(board)); + _isIncreasing = isIncreasing; + _isAlongRow = isAlongRow; + } + + public int OuterCount => _isAlongRow ? _board.Height : _board.Width; + public int InnerCount => _isIncreasing ? _board.Width : _board.Height; + + public int InnerStart => _isIncreasing ? 0 : InnerCount - 1; + public int InnerEnd => _isIncreasing ? InnerCount - 1 : 0; + + public int Drop(int index) + { + return _isIncreasing ? index - 1 : index + 1; + } + + public int ReverseDrop(int index) + { + return !_isIncreasing ? index - 1 : index + 1; + } + + public Element Get(int outerItem, int innerItem) + { + return _isAlongRow + ? new Element { Row = outerItem, Column = innerItem, Value = _board.Get(outerItem, innerItem) } + : new Element { Row = innerItem, Column = outerItem, Value = _board.Get(innerItem, outerItem) }; + } + + public bool IsInnerCondition(int index) + { + var minIndex = Math.Min(InnerStart, InnerEnd); + var maxIndex = Math.Max(InnerStart, InnerEnd); + + return minIndex <= index && index <= maxIndex; + } + } + } + +} diff --git a/C#/UpdateLoop.cs b/C#/UpdateLoop.cs new file mode 100644 index 0000000..34e7d67 --- /dev/null +++ b/C#/UpdateLoop.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Core_2048 +{ + + public partial class BoardBehavior + { + private class UpdateLoop : IEnumerable + { + private readonly ICellBehavior _cellBehavior; + private readonly Board _board; + private readonly bool _isIncreasing; + private readonly bool _isAlongRow; + + private int OuterCount => _isAlongRow ? _board.Height : _board.Width; + private int InnerCount => _isIncreasing ? _board.Width : _board.Height; + + private int InnerStart => _isIncreasing ? 0 : InnerCount - 1; + private int InnerEnd => _isIncreasing ? InnerCount - 1 : 0; + + public UpdateLoop(ICellBehavior cellBehavior, Board board, bool isAlongRow, bool isIncreasing) + { + _cellBehavior = cellBehavior ?? throw new ArgumentNullException(nameof(cellBehavior)); + _board = board ?? throw new ArgumentNullException(nameof(board)); + _isAlongRow = isAlongRow; + _isIncreasing = isIncreasing; + } + + public IEnumerator GetEnumerator() + { + for (var outerItem = 0; outerItem < OuterCount; outerItem++) + { + for (var innerItem = InnerStart; IsInnerCondition(innerItem); innerItem = ReverseDrop(innerItem)) + { + if (_cellBehavior.IsBaseCell(Get(outerItem, innerItem))) + { + continue; + } + + var newInnerItem = CalculateNewItem(innerItem, outerItem); + var isMerge = IsInnerCondition(newInnerItem) && _cellBehavior.IsMergeCells( + Get(outerItem, newInnerItem), + Get(outerItem, innerItem) + ); + + yield return isMerge + ? ExecuteWithMerge(outerItem, innerItem, newInnerItem) + : ExecuteWithoutMerge(outerItem, innerItem, newInnerItem); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private ChangeElementAction ExecuteWithMerge(int outerItem, int innerItem, int newInnerItem) + { + var previous = Get(outerItem, innerItem); + var newElement = Get(outerItem, newInnerItem); + + var next = new Cell + { + Row = newElement.Row, + Column = newElement.Column, + Value = _cellBehavior.MergeCells(previous, newElement) + }; + + return new ChangeElementAction + { + Previous = previous, + Next = next + }; + } + + private ChangeElementAction ExecuteWithoutMerge(int outerItem, int innerItem, int newInnerItem) + { + newInnerItem = ReverseDrop(newInnerItem); + var previous = Get(outerItem, innerItem); + var newElement = Get(outerItem, newInnerItem); + + var next = new Cell + { + Row = newElement.Row, + Column = newElement.Column, + Value = previous.Value + }; + + return new ChangeElementAction + { + Previous = previous, + Next = next + }; + } + + private int CalculateNewItem(int innerItem, int outerItem) + { + var newInnerItem = innerItem; + do + { + newInnerItem = Drop(newInnerItem); + } while (IsInnerCondition(newInnerItem) && _cellBehavior.IsBaseCell(Get(outerItem, newInnerItem))); + + return newInnerItem; + } + + private Cell Get(int outerItem, int innerItem) + { + return _isAlongRow + ? new Cell { Row = outerItem, Column = innerItem, Value = _board.Get(outerItem, innerItem) } + : new Cell { Row = innerItem, Column = outerItem, Value = _board.Get(innerItem, outerItem) }; + } + + private int Drop(int innerIndex) + { + return _isIncreasing ? innerIndex - 1 : innerIndex + 1; + } + + private int ReverseDrop(int innerIndex) + { + return !_isIncreasing ? innerIndex - 1 : innerIndex + 1; + } + + private bool IsInnerCondition(int index) + { + var minIndex = Math.Min(InnerStart, InnerEnd); + var maxIndex = Math.Max(InnerStart, InnerEnd); + + return minIndex <= index && index <= maxIndex; + } + } + } + +} diff --git a/ConsoleOut/ConsoleOut.csproj b/ConsoleOut/ConsoleOut.csproj new file mode 100644 index 0000000..0a363a1 --- /dev/null +++ b/ConsoleOut/ConsoleOut.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + 7.3 + + + + + + + diff --git a/ConsoleOut/Program.cs b/ConsoleOut/Program.cs new file mode 100644 index 0000000..246f3e4 --- /dev/null +++ b/ConsoleOut/Program.cs @@ -0,0 +1,117 @@ +using System; + +using Core_2048; + +namespace ConsoleOut +{ + + public static class Program + { + public static void Main(string[] args) + { + var board = new Board(4, 4, () => 0); + var elementGenerator = new RandomCellGenerator(element => element == 0); + elementGenerator.AddToPool(2, 95); + elementGenerator.AddToPool(4, 5); + var app = new BoardBehavior(board, elementGenerator, new BaseCellBehavior()); + app.AddNew(); + app.AddUpdatedListener(elements => + { + app.AddNew(); + Render(board); + }); + Render(board); + while (true) + { + var direction = Input(); + if (direction == null) + { + continue; + } + + var isAlongRow = direction == Direction.Left || direction == Direction.Right; + var isIncreasing = direction == Direction.Left || direction == Direction.Up; + app.Update(isAlongRow, isIncreasing); + } + } + + private static void Render(Board board) + { + Console.Clear(); + var prevRow = -1; + var pattern = new Func(value => $" {value} |"); + board.ForEach((value, row, column) => + { + var cell = pattern(value); + if (prevRow == row) + { + Console.Write(cell); + } + else + { + Console.WriteLine(""); + Console.Write($"|{cell}"); + prevRow = row; + } + }); + } + + private static Direction? Input() + { + var key = Console.ReadKey(); + + switch (key.Key) + { + case ConsoleKey.W: + return Direction.Up; + case ConsoleKey.S: + return Direction.Down; + case ConsoleKey.A: + return Direction.Left; + case ConsoleKey.D: + return Direction.Right; + default: + return null; + } + } + } + + internal class BaseCellBehavior : ICellBehavior + { + private readonly ulong _baseValue; + + public BaseCellBehavior(ulong baseValue = 0) + { + _baseValue = baseValue; + } + + public bool IsBaseCell(Cell cell) + { + return cell.Value == _baseValue; + } + + public bool IsMergeCells(Cell previous, Cell next) + { + return previous.Value == next.Value; + } + + public ulong MergeCells(Cell previous, Cell next) + { + return previous.Value + next.Value; + } + + public ulong GetCellBaseValue() + { + return _baseValue; + } + } + + internal enum Direction + { + Up, + Down, + Left, + Right + } + +} diff --git a/Core_2048.csproj b/Core_2048.csproj new file mode 100644 index 0000000..8f952c2 --- /dev/null +++ b/Core_2048.csproj @@ -0,0 +1,59 @@ + + + + + Debug + AnyCPU + {FF86D050-B76D-4D93-80A0-22BA15BBDD90} + Library + Properties + Core_2048 + Core_2048 + v4.8 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + diff --git a/Core_2048.sln b/Core_2048.sln new file mode 100644 index 0000000..30a0ed9 --- /dev/null +++ b/Core_2048.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core_2048", "Core_2048.csproj", "{FF86D050-B76D-4D93-80A0-22BA15BBDD90}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleOut", "ConsoleOut\ConsoleOut.csproj", "{F1D86226-1378-4F0D-A56C-DA96FE15AB29}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FF86D050-B76D-4D93-80A0-22BA15BBDD90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF86D050-B76D-4D93-80A0-22BA15BBDD90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF86D050-B76D-4D93-80A0-22BA15BBDD90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF86D050-B76D-4D93-80A0-22BA15BBDD90}.Release|Any CPU.Build.0 = Release|Any CPU + {F1D86226-1378-4F0D-A56C-DA96FE15AB29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1D86226-1378-4F0D-A56C-DA96FE15AB29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1D86226-1378-4F0D-A56C-DA96FE15AB29}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1D86226-1378-4F0D-A56C-DA96FE15AB29}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Core_2048.sln.DotSettings b/Core_2048.sln.DotSettings new file mode 100644 index 0000000..8422d3c --- /dev/null +++ b/Core_2048.sln.DotSettings @@ -0,0 +1,51 @@ + + False + Required + Required + Required + Required + Join + ExpressionBody + True + True + True + True + True + True + 0 + 0 + 0 + 0 + 1 + 1 + 1 + TOGETHER_SAME_LINE + True + True + True + True + True + True + True + True + 1 + 5 + IF_OWNER_IS_SINGLE_LINE + IF_OWNER_IS_SINGLE_LINE + True + True + False + CHOP_IF_LONG + False + False + True + Skip + False + False + True + <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Any" Description="Unity serialized field"><ElementKinds><Kind Name="UNITY_SERIALISED_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + True + True + True + True \ No newline at end of file diff --git a/Core_2048.sln.DotSettings.user b/Core_2048.sln.DotSettings.user new file mode 100644 index 0000000..f7799a2 --- /dev/null +++ b/Core_2048.sln.DotSettings.user @@ -0,0 +1,6 @@ + + False + <AssemblyExplorer> + <PhysicalFolder Path="C:\Users\vlad1\.nuget\packages\jetbrains.annotations\2021.2.0-eap2" Loaded="True" /> +</AssemblyExplorer> + \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1ef167a --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,44 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SDK for create 2048")] +[assembly: AssemblyDescription(@"Suitable for any engine from console app to Unity game. Only 2048 game logic. A very simple + customizable core for initialization game with params: + canvas size + base value, which means blank cells + configuring merging elements value + configuring predicate elements value + customizable amount of value for new cells with customizable chance of creation for each individual case + generic for the element value (Convenient for use in game engines) + ")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Shyrokyi Vladislav")] +[assembly: AssemblyProduct("Core_2048")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyKeyFile("keypair.snk")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("FF86D050-B76D-4D93-80A0-22BA15BBDD90")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyFileVersion("1.0.*")] diff --git a/README.md b/README.md index 86c7900..12d3a6d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,64 @@ -2048 +2048 [![Nuget](https://img.shields.io/nuget/v/Core_2048)](https://www.nuget.org/packages/Core_2048) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/VladShyrokyi/2048-1)](https://github.com/VladShyrokyi/2048-1) ==== -My take the 2048 game in C#. Console version. The implementation is pretty customizable. You can tweak the size of the canvas and the probabilities of 2s and 4s, etc. Just compile and run from console. Should work with both .NET and Mono. +My take mini sdk libs for creating the 2048 game in C#. Suitable for any engine from console app to Unity game. Only +2048 game logic. A very simple customizable core for initialization game with params: -Here's how it looks like: ![screenshot](doc/screenshot.png "Screenshot") +* canvas size +* base value, which means blank cells +* configuring merging cells value +* configuring predicate cells value +* customizable amount of value for new cells with customizable chance of creation for each individual case +* generic for the cells value (Convenient for use in game engines) + +## Easy to implements in your game: + +1. Create a game `Board` by your generic type with **height**, **width** and **initialized function**. +2. Create a generator of random cells `RandomCellGenerator`, set the check to an empty cell and add a list of cells to the pool to generate with a specified percentage probability (Or you can create your own generator by implementing `IElementGenerator`). +3. Create an implementation of the `ICellBehavior` interface to define base cells and behavior for merging cells. +4. Create `BoardBehavior` with `Board`, class implementing `ICellBehavior`, and `CellGenerator`. +5. Call `AddNew` for generating and add new element on board. +6. In the game loop, call the `Update` with arguments: + * `isAlongRow` - true when movement **left** and **right** if render from up to down; + * `isIncreasing` - true when movement **left** and **up** (if board render from **up** to **down**) or **down** (if board render from **down** to **up**); +7. Add listeners on `Updated` action to see if cells moved after ant action. +8. Call `AddNew` method when need add new value at board. +9. Create `Render` method in your engine. In Core class has methods for base iteration and there is updating map with movement info for easier implementation of interaction with external object. + +Sample console project in ConsoleOut directory. + +### Example code: +```csharp +var board = new Board(4, 4, () => 0); +var elementGenerator = new RandomCellGenerator(element => element == 0); +elementGenerator.AddToPool(2, 95); +elementGenerator.AddToPool(4, 5); +var app = new BoardBehavior(board, elementGenerator, new BaseCellBehavior()); +app.AddNew(); +app.AddUpdatedListener(elements => +{ + app.AddNew(); + Render(board); +}); +Render(board); +while (true) +{ + var direction = Input(); + if (direction == null) + { + continue; + } + + var isAlongRow = direction == Direction.Left || direction == Direction.Right; + var isIncreasing = direction == Direction.Left || direction == Direction.Up; + app.Update(isAlongRow, isIncreasing); +} +``` + +### Here's how it looks like: + +![screenshot](https://raw.githubusercontent.com/VladShyrokyi/2048-1/master/doc/ConsoleOut.png "Console app for 2048") + +### Also implementation in Unity: + +![screenshot](https://raw.githubusercontent.com/VladShyrokyi/2048-1/master/doc/GameInUnity.gif "Unity app for 2048") diff --git a/doc/2048_console_app_code.png b/doc/2048_console_app_code.png new file mode 100644 index 0000000..44a452f Binary files /dev/null and b/doc/2048_console_app_code.png differ diff --git a/doc/ConsoleOut.png b/doc/ConsoleOut.png new file mode 100644 index 0000000..99e873d Binary files /dev/null and b/doc/ConsoleOut.png differ diff --git a/doc/GameInUnity.gif b/doc/GameInUnity.gif new file mode 100644 index 0000000..4e4e3c9 Binary files /dev/null and b/doc/GameInUnity.gif differ