放浪軍師のXamarin.Formsアプリ開発局

Xamarin.Forms+Prism+ReactivePropertyで素人がAndroidやUWPのアプリを右往左往しながら開発している様を発信していきます。性質上間違いも多いのでご注意ください。

乱ちゃんProjectその7(乱屍ツールの挙動完成)

放浪軍師のXamarin.Formsによるアプリ開発
今回は乱ちゃんProjectを進めます。
まず、初めて訪問された方は以下をお読みください。
www.gunshi.info

乱屍の挙動が完成したよ!

www.gunshi.info
上記の記事で勉強した技術をフルに生かして乱屍の挙動部分がひとまず完成したのでお披露目しようと思います。
いやぁここまで長かったねぇ~!教えてくださったみなさんに感謝しかないですね。

ToggleButton.cs

Buttonを継承してIsSelectedプロパティとIsHitedプロパティを追加したToggleButtonクラスです。Controlsというフォルダ内に置いてみました。
IsSelectedは未使用選択を示し、Trueでランダム選択から除外されている事を示します。
IsHitedはルーレットによってランダム選択された状態を示し、Trueの場合はランダム選択された事を示します。

using Xamarin.Forms;

namespace RanCyan.Controls
{
    public class ToggleButton : Button
    {
        public static readonly BindableProperty IsSelectedProperty =
            BindableProperty.Create(
                "IsSelected",
                typeof(bool),
                typeof(ToggleButton),
                false);

        public object IsSelected
        {
            get => GetValue(IsSelectedProperty);
            set => SetValue(IsSelectedProperty, value);
        }

        public static readonly BindableProperty IsHitedProperty =
            BindableProperty.Create(
            "IsHited",
            typeof(bool),
            typeof(ToggleButton),
            false);

        public object IsHited
        {
            get => GetValue(IsHitedProperty);
            set => SetValue(IsHitedProperty, value);
        }

    }
}

RanShikaKoushinPage.xaml

Viewに該当します。
Style.Triggersを用いてToggleButtonIsSelectedプロパティでTextColorが、IsHitedプロパティでBackgroundColorが変化するようにしてみました。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             xmlns:controls="clr-namespace:RanCyan.Controls"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Name="Base"
             x:Class="RanCyan.Views.RanShikaKoushinPage"
             Title="交神ランダムの呪い">

    <ContentPage.Resources>
        <ResourceDictionary>
            <Style TargetType="controls:ToggleButton">
                <Setter Property="TextColor" Value="Black"/>
                <Setter Property="BackgroundColor" Value="AliceBlue"/>
                <Style.Triggers>
                    <Trigger TargetType="controls:ToggleButton" Property="IsSelected" Value="True">
                        <Setter Property="TextColor" Value="Gainsboro"/>
                    </Trigger>
                    <Trigger TargetType="controls:ToggleButton" Property="IsHited" Value="True">
                        <Setter Property="BackgroundColor" Value="Red"/>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="4*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <ListView ItemsSource="{Binding Items}" Grid.Row="0">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <controls:ToggleButton Command="{Binding BindingContext.ItemTapped, Source={x:Reference Base}}"
                                   CommandParameter="{Binding}"
                                   Text="{Binding Name}"
                                   IsSelected="{Binding IsSelected}"
                                   IsHited="{Binding IsHited}"/>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Button Text="交神ランダム" Command="{Binding RanCommand}" Grid.Row="1"/>
    </Grid>
    
</ContentPage>

RandomList.cs

Modelに該当します。
ちょっと悩んだんですけどList用のItemクラスはここに配置。RandomListクラスではこのアプリの主となるランダム選択を実行。IsHitedTrueになったりFalseになったりして最終的にどこかで止まります。これが変わる度にバインディングによってViewToggleButtonBackgroundColorが変化します。

using Prism.Mvvm;
using Reactive.Bindings;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;

namespace RanCyan.Models
{
    public class Item : BindableBase
    {
        /// <summary>
        /// ラベル名称
        /// </summary>
        public string Name
        {
            get => name;
            set => SetProperty(ref name, value);
        }
        private string name;

        /// <summary>
        /// 除外状態
        /// </summary>
        public bool IsSelected
        {
            get => isSelected;
            set => SetProperty(ref isSelected, value);
        }
        private bool isSelected;

        /// <summary>
        /// 割合 
        /// </summary>
        public int Ratio
        {
            get => ratio;
            set => SetProperty(ref ratio, value);
        }
        private int ratio;

        /// <summary>
        /// ランダム選択された
        /// </summary>
        public bool IsHited
        {
            get => isHited;
            set => SetProperty(ref isHited, value);
        }
        private bool isHited;

    }

    public class RandomList : BindableBase
    {
        /// <summary>
        /// リスト
        /// </summary>
        public ObservableCollection<Item> Items { get; }

        /// <summary>
        /// ループする回数(回)
        /// </summary>
        public int LoopNo
        {
            get => loopNo;
            set => SetProperty(ref loopNo, value);
        }
        private int loopNo;

        /// <summary>
        /// 全ループ合計時間(msec)
        /// </summary>
        public int LoopTime
        {
            get => loopTime;
            set => SetProperty(ref loopTime, value);
        }
        private int loopTime;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="items">ループリスト</param>
        /// <param name="loopNo">ループする回数(回)</param>
        /// <param name="loopTime">全ループ合計時間(msec)</param>
        public RandomList(List<Item> items, int loopNo = 10, int loopTime = 1000)
        {
            Items = new ObservableCollection<Item>(items);
            LoopNo = loopNo;
            LoopTime = loopTime;
        }

        public async void RandomAction()
        {

            //シード値を取得(乱数固定化の阻止)
            int seed = Environment.TickCount;
            // Randomクラスのインスタンス生成
            Random rnd = new System.Random(seed);

            //基準となるウェイト時間(msec)
            float oneWaitTime = loopTime / (((loopNo - 1) * loopNo) / 2);

            var RaitoSum = Items.Where(x => !x.IsSelected)  //選択している奴は除外した
                                .Sum(x => x.Ratio);         //Ratioの合計値

            Items.Where(x => x.IsSelected).Select(x => x.IsHited = false).ToList();//IsHitedは全部Falseにする

            if (RaitoSum == 0) return; //0の場合抽選を回避する

            for (int i = 1; i <= loopNo; i++)
            {
                // listの数からダンダムで値を取得
                var hitCount = rnd.Next(1, RaitoSum + 1);
                // 得た数値に該当するIsHitをTrueに。それ以外はFalseにする。
                int lastCount = 0;
                int count = 0;
                foreach (var x in Items)
                {
                    if (!x.IsSelected)
                    {
                        count = count + x.Ratio;
                        x.IsHited = (lastCount < hitCount && hitCount <= count);
                        lastCount = count;
                    }
                }

                if (i < loopNo)
                {
                    //少しずつウェイト時間を長くする
                    await Task.Delay((int)(oneWaitTime * i));
                }
            }

            //最後に点滅させる
            foreach (var x in Items)
            {
                if (x.IsHited)
                {
                    for (int i = 0; i < 10; i++)
                    {
                        x.IsHited = !x.IsHited;
                        await Task.Delay(50);
                    }
                }
            }

        }
    }

}

RanShikaKoushinPageViewModel.cs

ViewModelに該当。
ルーレットの対象となるListをここに記述してnew RandomListで引き渡す…としてるんですけどなーんか違和感があります。
ToReadOnlyReactiveCollectionReactiveCollectionViewModel←Model片方向バインディングですが、これって双方向の方が良い気もする。ちょっとやり方が見つからなかったですね。まぁちゃんと動きはするんですけど…あってんのかな?

using Prism.Mvvm;
using RanCyan.Models;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive.Disposables;


namespace RanCyan.ViewModels
{
    public class RanShikaKoushinPageViewModel : BindableBase, IDisposable
    {
        private CompositeDisposable Disposable { get; } = new CompositeDisposable();

        public ReadOnlyReactiveCollection<Item> Items { get; }
        public ReactiveCommand<Item> ItemTapped { get; } = new ReactiveCommand<Item>();

        public ReactiveCommand RanCommand { get; } = new ReactiveCommand();

        public RandomList randomList { get; }

        public RanShikaKoushinPageViewModel()
        {
            var items = new List<Item>()
            {
                new Item { Name = "火" , Ratio=1 },
                new Item { Name = "水" , Ratio=1 },
                new Item { Name = "風" , Ratio=1 },
                new Item { Name = "土" , Ratio=1 },
            };
            RandomList randomList = new RandomList(items, 20, 5000);

            //ViewModel←Model
            this.Items = randomList.Items.ToReadOnlyReactiveCollection().AddTo(this.Disposable);

            //Button
            ItemTapped.Subscribe(x => x.IsSelected = !x.IsSelected);
            RanCommand.Subscribe(_ => randomList.RandomAction());

        }

        public void Dispose()
        {
            this.Disposable.Dispose();
        }
    }
}

RandomListクラスは流用できる!

この作りの良いところは、ViewModel

var items = new List<Item>()
{
    new Item { Name = "火" , Ratio=1 },
    new Item { Name = "水" , Ratio=1 },
    new Item { Name = "風" , Ratio=1 },
    new Item { Name = "土" , Ratio=1 },
};

の部分を変更するだけでModelを流用できるとこです。例えば職業ランダムなら

var items = new List<Item>()
{
    new Item { Name = "剣士" , Ratio=1 },
    new Item { Name = "薙刀士" , Ratio=1 },
    new Item { Name = "弓使い" , Ratio=1 },
    new Item { Name = "槍使い" , Ratio=1 },
    new Item { Name = "拳法家" , Ratio=1 },
    new Item { Name = "壊し屋" , Ratio=1 },
    new Item { Name = "大筒士" , Ratio=1 },
    new Item { Name = "踊り屋" , Ratio=1 },
};

とすればいいだけです。ちなみにRatioは選択率を示していて、2を指定した場合は他の2倍、5を指定すれば他の5倍選ばれやすくなります。今回はすべて1を指定していますが、今後の為の布石です。そう、勘が良い方なら次に何を作りたいかわかりますよね?

挙動確認

こんな感じになりました。左がAndroid、右がUWPです。
f:id:roamschemer:20181018004237g:plainf:id:roamschemer:20181018004328g:plain

進言ランダムは以前の数字表示はやめて、他と同じような挙動にしました。直観的にもこっちのほうが判りやすいしね~。
さて、これでスペース空きすぎとか連打したらおかしくなるとかの問題があるものの、これで乱屍ツールとしては充分に使用可能です!やったぜ!
ここまでくればあとは微調整をいくつかしてついに公開ですよ!皆さんがスマホ片手に乱屍で遊べる日は近い!?