這是一個我給自己做著玩的遊戲,沒有什麼複雜的介面,就一些簡單的邏輯
遊戲的規則十分簡單,那就是有多個列表。程式會給出一個數字,玩家決定數字放在哪個列表裡面。如果放入列表裡面的數字和列表裡面最後一個數字相同,那兩個數字將會疊加進行合併,合併兩個 1024 將會自動清理掉整個列表
如下圖,有 5 個列表。最右邊有一個數字。此時點選列表下方的 "點選" 按鈕,即表示將最右邊的數字放在這一列表中
如下圖,就是點選了首個列表的“點選”按鈕,將上圖的 1024 數字放在首個列表裡
如下圖,首個列表裡面的最後一個是 2 的數字,最右邊的數字也是 2 的數字,可以將其進行合併
如下圖,合併之後,首個列表的 2 將會和最右邊的數字 2 合併為 4 作為最後一個數字
規則介紹完了,接下來咱來開始開發咯。如果只是想玩這個簡單的遊戲的夥伴,可以快速到本文末尾,找到本文的所有程式碼的下載方法
如上面的介面圖,可以看到有多個列表,那不如每個列表就一個 UserControl 使用者控制元件好了。這裡沒有什麼最佳實踐,這麼簡單的應用,想怎麼寫就怎麼寫就好了
我這裡都不想好好命名,直接就用 Whitman 工具隨機一個名為 CecaqemdarYefarqukeafai 的控制元件名好了
在 CecaqemdarYefarqukeafai.xaml.cs 裡面存放一個 ObservableCollection<int>
集合,用來表示介面上每個列表裡面的資料,程式碼如下
public ObservableCollection<int> Collection { get; } = new ObservableCollection<int>();
在 CecaqemdarYefarqukeafai.xaml 的介面寫一個 ListView 進行繫結這個 Collection 屬性,程式碼如下
<ListView ItemsSource="{Binding ElementName=Root,Path=Collection}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding .}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
這裡我寫的繫結是 ElementName=Root
的方式,這是我的習慣使用方法。對於簡單沒有 MVVM 的模式下,可以將控制元件自身當成自己的繫結源,這樣在控制元件後臺程式碼編寫的屬性就可以很方便進行繫結
具體的實現方法就是將使用者控制元件自身加上 x:Name="Root"
屬性,加上之後的使用者控制元件的程式碼大概如下
<UserControl x:Class="BawjadurbaWurahuwa.CecaqemdarYefarqukeafai"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:BawjadurbaWurahuwa"
mc:Ignorable="d"
x:Name="Root"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
... 忽略其他程式碼
<ListView ItemsSource="{Binding ElementName=Root,Path=Collection}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding .}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
... 忽略其他程式碼
</Grid>
</UserControl>
如上圖介面,可以看到每個列表下方都有一個點選按鈕。那就繼續修改 CecaqemdarYefarqukeafai.xaml 介面,加上一個按鈕,程式碼如下
<Button Margin="10,10,10,10" HorizontalAlignment="Center" Click="Button_OnClick">點選</Button>
加上按鈕需要稍微修改一下佈局,修改一下 Grid 加上兩行,程式碼如下
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
以上就配置了列表的地方有多少空間使用多少空間,配置下面一行給按鈕使用,按鈕需要多少空間再給多少空間
修改之後的 CecaqemdarYefarqukeafai.xaml 的全部程式碼如下
<UserControl x:Class="BawjadurbaWurahuwa.CecaqemdarYefarqukeafai"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:BawjadurbaWurahuwa"
mc:Ignorable="d"
x:Name="Root"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding ElementName=Root,Path=Collection}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding .}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Grid.Row="1" Margin="10,10,10,10" HorizontalAlignment="Center" Click="Button_OnClick">點選</Button>
</Grid>
</UserControl>
可以看到實現非常簡單,即使不使用使用者控制元件也是可以的
這裡的點選按鈕需要將事件給到外面訂閱,編輯後臺 CecaqemdarYefarqukeafai.xaml.cs 程式碼,實現按鈕點選邏輯,程式碼如下
public event EventHandler<CecaqemdarYefarqukeafai>? Click;
private void Button_OnClick(object sender, RoutedEventArgs e)
{
Click?.Invoke(this, this);
}
如此即可在點選按鈕的時候,觸發 Click 事件給到外面訂閱
修改之後的 CecaqemdarYefarqukeafai.xaml.cs 的全部程式碼如下
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace BawjadurbaWurahuwa;
/// <summary>
/// CecaqemdarYefarqukeafai.xaml 的互動邏輯
/// </summary>
public partial class CecaqemdarYefarqukeafai : UserControl
{
public CecaqemdarYefarqukeafai()
{
InitializeComponent();
}
public ObservableCollection<int> Collection { get; } = new ObservableCollection<int>();
public event EventHandler<CecaqemdarYefarqukeafai>? Click;
private void Button_OnClick(object sender, RoutedEventArgs e)
{
Click?.Invoke(this, this);
}
}
回到主介面
主介面需要顯示 5 列,那就直接寫 5 個 CecaqemdarYefarqukeafai 控制元件好了。如果數量更多的話,那可以試試寫一個 ListView 之類的控制元件
如上圖,整個主介面可以分為 6 列,其中前面 5 列是 CecaqemdarYefarqukeafai 控制元件,最後一列是一個文字,用來說明下一個數字
實現的 MainWindow.xaml 程式碼如下
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<local:CecaqemdarYefarqukeafai Click="CecaqemdarYefarqukeafai_OnClick"/>
<local:CecaqemdarYefarqukeafai Grid.Column="1" Click="CecaqemdarYefarqukeafai_OnClick"/>
<local:CecaqemdarYefarqukeafai Grid.Column="2" Click="CecaqemdarYefarqukeafai_OnClick"/>
<local:CecaqemdarYefarqukeafai Grid.Column="3" Click="CecaqemdarYefarqukeafai_OnClick"/>
<local:CecaqemdarYefarqukeafai Grid.Column="4" Click="CecaqemdarYefarqukeafai_OnClick"/>
<TextBlock x:Name="CurrentTextBlock" Grid.Column="5" HorizontalAlignment="Center"></TextBlock>
</Grid>
也許有夥伴開始好奇了,為什麼上面程式碼裡面的 5 個 CecaqemdarYefarqukeafai 的 Click 事件都是相同的方法,那方法內是如何區分點選的是哪個列表的?答案是不需要區分,在 CecaqemdarYefarqukeafai 的定義事件的程式碼裡面,就將列表控制元件自身給傳遞進入了,如下面程式碼
public partial class CecaqemdarYefarqukeafai : UserControl
{
... // 忽略其他程式碼
public event EventHandler<CecaqemdarYefarqukeafai>? Click;
... // 忽略其他程式碼
}
於是在 MainWindow.xaml.cs 後臺程式碼實現方法裡面,就可以透過引數瞭解到當前點選按鈕屬於哪個使用者控制元件了
private void CecaqemdarYefarqukeafai_OnClick(object? sender, CecaqemdarYefarqukeafai e)
{
... // 忽略其他程式碼
}
為了方便拿到和表示當前最右側顯示的當前的數字,咱使用的是建立一個陣列和一個索引的方式表示。如此即可實現後續進行隨機給一個數字的方法,也可以讓給出的數字一定在陣列內。定義在 MainWindow.xaml.cs 的欄位程式碼如下
private int _index;
private readonly int[] _list = new int[] { 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2 };
那是否可以省略這個陣列,沒錯,因為這些都是 2 的倍數,想要省略也是可以的。省略了這個陣列,那就每次自己計算就好了。可不要覺得每次都重新計算速度很慢,對於現代 CPU 來說,你接近測試不出來這兩者的效能差異。但總之這個陣列也很小,佔用記憶體基本可以忽略,就隨大家想用什麼就用什麼咯
為什麼有時候陣列很小我也會關注,有時候陣列即使不小我也不會關注。這其實和業務有關係,在本文例子裡面的這個陣列只有一次定義,且全域性只有一個,那這個陣列就這點空間,自然就可以忽略其佔用記憶體了。但如果這個陣列是需要每次都建立的,那這時候我可能會稍微考慮一下。如果這個陣列是每次都需要建立的,且建立之後很難釋放,那才會考慮一下
回到點選事件裡面,透過索引和陣列即可拿到當前最右側的數字,程式碼如下
private void CecaqemdarYefarqukeafai_OnClick(object? sender, CecaqemdarYefarqukeafai e)
{
var number = _list[_index];
... // 忽略其他程式碼
}
將此數字加入到 CecaqemdarYefarqukeafai 的集合裡面,程式碼如下
private void CecaqemdarYefarqukeafai_OnClick(object? sender, CecaqemdarYefarqukeafai e)
{
var number = _list[_index];
e.Collection.Add(number);
... // 忽略其他程式碼
}
如此就完成了點選按鈕就將數字加到所點選的一列的基礎邏輯了
根據遊戲規則,如果列表裡面最後相鄰的兩個數字是相同的,則進行合併。接下來再寫一個方法,這個方法用於合併集合的數字,程式碼如下
private static void Clean(ObservableCollection<int> collection)
{
while (collection.Count > 1)
{
var n1 = collection[^1];
var n2 = collection[^2];
if (n1 == n2)
{
collection.RemoveAt(collection.Count - 1);
collection.RemoveAt(collection.Count - 1);
collection.Add(n1 + n2);
}
else
{
break;
}
}
if (collection[^1] == 1024 * 2)
{
collection.Clear();
}
}
為了這個方法需要一個迴圈?這是因為如果最後的數字剛好是 4、2、2 的話,那就可以先對 2 和 2 進行合併,合併完成拿到的 4 再和 4 進行合併
合併的方法就是移除這兩個數字,再新增一個新的更大的數字
為什麼移除的時候都是使用 collection.RemoveAt(collection.Count - 1);
程式碼移除,為什麼兩次移除都是相同的程式碼?這是因為首先集合列表陣列都是從 0 開始的,想象一下,一個只有元素的集合,想要移除最後一個元素,那下標是多少,沒錯就是 0 作為下標。因此 collection.Count - 1
表示的是最後一個元素。那為什麼呼叫兩次?這是因為第一次呼叫的時候,最後一個元素就被移除了。那原本倒數的第二個元素現在就成為倒數第一個元素了,自然再次移除最後一個元素就是移除掉原先的倒數第二個元素。舉個例子,假如你每次都是全班倒數第二,某天全班倒數第一退學了,那你是不是就成為全班倒數第一了
如何全部合併之後,最後一個數字是兩倍的 1024 則將列表清空。嗯,這裡的話,只去掉當前這個數也可以,這個看大家的規則
完成了 Clean 方法之後,嘗試呼叫一下,程式碼如下
private void CecaqemdarYefarqukeafai_OnClick(object? sender, CecaqemdarYefarqukeafai e)
{
var number = _list[_index];
e.Collection.Add(number);
Clean(e.Collection);
... // 忽略其他程式碼
}
如此就完成了將數字加入到所點選的列表裡面,且如果數字和列表最後一個數字相同則進行合併
根據遊戲的規則,此時咱就需要再生成最右側的新的數字了。如上文可以知道,最右側的數字是使用陣列和索引表示的,那就是隨機生成一個在陣列範圍內的索引就可以了。既可以降低難度,按照順序生成索引,如下面程式碼
private void CecaqemdarYefarqukeafai_OnClick(object? sender, CecaqemdarYefarqukeafai e)
{
... // 忽略其他程式碼
_index++;
if (_index == _list.Length)
{
_index = 0;
}
... // 忽略其他程式碼
}
也可以使用隨機數生成,程式碼如下
private void CecaqemdarYefarqukeafai_OnClick(object? sender, CecaqemdarYefarqukeafai e)
{
... // 忽略其他程式碼
_index = Random.Shared.Next(_list.Length);
... // 忽略其他程式碼
}
生成完成之後,將結果設定到介面的 CurrentTextBlock 控制元件裡面,如此即可在介面顯示
private void CecaqemdarYefarqukeafai_OnClick(object? sender, CecaqemdarYefarqukeafai e)
{
... // 忽略其他程式碼
CurrentTextBlock.Text = $"第 {_count} 次\r\n下一個 {_list[_index]}";
}
上述的 _count
欄位這時一個類似遊戲分數的作用,表示的是當前是第多少次,實現程式碼如下
private void CecaqemdarYefarqukeafai_OnClick(object? sender, CecaqemdarYefarqukeafai e)
{
_count++;
... // 忽略其他程式碼
CurrentTextBlock.Text = $"第 {_count} 次\r\n下一個 {_list[_index]}";
}
private int _count;
最後別忘了修改一下 MainWindow 建構函式,在其初始化時給最右側一個數字,程式碼如下
public MainWindow()
{
InitializeComponent();
CurrentTextBlock.Text = _list[_index].ToString();
}
如此即可完成簡單的實現邏輯,程式碼大概如下
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
CurrentTextBlock.Text = _list[_index].ToString();
}
private void CecaqemdarYefarqukeafai_OnClick(object? sender, CecaqemdarYefarqukeafai e)
{
_count++;
var number = _list[_index];
e.Collection.Add(number);
Clean(e.Collection);
_index++;
if (_index == _list.Length)
{
_index = 0;
}
CurrentTextBlock.Text = $"第 {_count} 次\r\n下一個 {_list[_index]}";
}
private static void Clean(ObservableCollection<int> collection)
{
while (collection.Count > 1)
{
var n1 = collection[^1];
var n2 = collection[^2];
if (n1 == n2)
{
collection.RemoveAt(collection.Count - 1);
collection.RemoveAt(collection.Count - 1);
collection.Add(n1 + n2);
}
else
{
break;
}
}
if (collection[^1] == 1024 * 2)
{
collection.Clear();
}
}
private int _index;
private int _count;
private readonly int[] _list = new int[] { 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2 };
}
作為開發者,我玩著玩著就想著看程式自己玩,做成掛機遊戲。於是再寫點演算法讓程式自己玩好了,實現程式碼如下
public MainWindow()
{
InitializeComponent();
CurrentTextBlock.Text = _list[_index].ToString();
Loaded += MainWindow_Loaded;
}
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
while (Content is Grid grid)
{
var number = _list[_index];
var maxValue = 1024;
var cecaqemdarYefarqukeafai =
grid.Children.OfType<CecaqemdarYefarqukeafai>()
.FirstOrDefault(t => t.Collection.Count > 0 && t.Collection[^1] == number);
cecaqemdarYefarqukeafai ??= grid.Children.OfType<CecaqemdarYefarqukeafai>().Where(t => t.Collection.Count > 0 && t.Collection[^1] > number).MinBy(t => t.Collection[^1]);
cecaqemdarYefarqukeafai ??= grid.Children.OfType<CecaqemdarYefarqukeafai>()
.FirstOrDefault(t => t.Collection.Count == 0);
cecaqemdarYefarqukeafai ??= grid.Children.OfType<CecaqemdarYefarqukeafai>().MinBy(t =>
{
if (t.Collection.Count > 0)
{
var lastValue = t.Collection[^1];
if (lastValue > number)
{
return lastValue;
}
else
{
return maxValue;
}
}
return 0;
});
if (cecaqemdarYefarqukeafai is null)
{
continue;
}
cecaqemdarYefarqukeafai.Collection.Add(number);
Clean(cecaqemdarYefarqukeafai.Collection);
_index = Random.Shared.Next(_list.Length);
_count++;
CurrentTextBlock.Text = $"第 {_count} 次\r\n下一個 {_list[_index]}";
await Task.Delay(300);
}
}
相信以上的邏輯大家看看也能明白,這是我隨意寫的簡單演算法,核心只是決定將數字放在哪個列表而已
完成之後的程式碼如下
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace BawjadurbaWurahuwa;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
CurrentTextBlock.Text = _list[_index].ToString();
Loaded += MainWindow_Loaded;
}
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
while (Content is Grid grid)
{
var number = _list[_index];
var maxValue = 1024;
var cecaqemdarYefarqukeafai =
grid.Children.OfType<CecaqemdarYefarqukeafai>()
.FirstOrDefault(t => t.Collection.Count > 0 && t.Collection[^1] == number);
cecaqemdarYefarqukeafai ??= grid.Children.OfType<CecaqemdarYefarqukeafai>().Where(t => t.Collection.Count > 0 && t.Collection[^1] > number).MinBy(t => t.Collection[^1]);
cecaqemdarYefarqukeafai ??= grid.Children.OfType<CecaqemdarYefarqukeafai>()
.FirstOrDefault(t => t.Collection.Count == 0);
cecaqemdarYefarqukeafai ??= grid.Children.OfType<CecaqemdarYefarqukeafai>().MinBy(t =>
{
if (t.Collection.Count > 0)
{
var lastValue = t.Collection[^1];
if (lastValue > number)
{
return lastValue;
}
else
{
return maxValue;
}
}
return 0;
});
if (cecaqemdarYefarqukeafai is null)
{
continue;
}
cecaqemdarYefarqukeafai.Collection.Add(number);
Clean(cecaqemdarYefarqukeafai.Collection);
_index = Random.Shared.Next(_list.Length);
_count++;
CurrentTextBlock.Text = $"第 {_count} 次\r\n下一個 {_list[_index]}";
await Task.Delay(300);
}
}
private void CecaqemdarYefarqukeafai_OnClick(object? sender, CecaqemdarYefarqukeafai e)
{
_count++;
var number = _list[_index];
e.Collection.Add(number);
Clean(e.Collection);
_index++;
if (_index == _list.Length)
{
_index = 0;
}
CurrentTextBlock.Text = $"第 {_count} 次\r\n下一個 {_list[_index]}";
}
private static void Clean(ObservableCollection<int> collection)
{
while (collection.Count > 1)
{
var n1 = collection[^1];
var n2 = collection[^2];
if (n1 == n2)
{
collection.RemoveAt(collection.Count - 1);
collection.RemoveAt(collection.Count - 1);
collection.Add(n1 + n2);
}
else
{
break;
}
}
if (collection[^1] == 1024 * 2)
{
collection.Clear();
}
}
private int _index;
private int _count;
private readonly int[] _list = new int[] { 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2 };
}
本文以上程式碼放在 github 和 gitee 上,可以使用如下命令列拉取程式碼
先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 243d50c0b94013e5beb782e384c98a7b6e3f629d
以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 243d50c0b94013e5beb782e384c98a7b6e3f629d
獲取程式碼之後,進入 WPFDemo/BawjadurbaWurahuwa 資料夾,即可獲取到原始碼
進入資料夾之後使用 VisualStudio 2022 或更高版本的 VisualStudio 開啟 BawjadurbaWurahuwa.sln 檔案,然後試試按下 F5 進行構建且執行即可開始玩遊戲