Lotto.cs 13.1 KB
Newer Older
M1888's avatar
M1888 committed
1
using System;
M1888's avatar
M1888 committed
2
using System.Collections.Concurrent;
M1888's avatar
M1888 committed
3
using System.Collections.Generic;
M1888's avatar
M1888 committed
4 5
using System.Collections.ObjectModel;
using System.ComponentModel;
M1888's avatar
M1888 committed
6
using System.Diagnostics;
M1888's avatar
M1888 committed
7
using System.Linq;
M1888's avatar
M1888 committed
8
using System.Runtime.CompilerServices;
M1888's avatar
M1888 committed
9
using System.Text;
M1888's avatar
M1888 committed
10
using System.Threading;
M1888's avatar
M1888 committed
11
using System.Threading.Tasks;
M1888's avatar
M1888 committed
12
using Medallion;
M1888's avatar
M1888 committed
13

M1888's avatar
M1888 committed
14

M1888's avatar
M1888 committed
15 16
namespace Lottokone.Model
{
M1888's avatar
M1888 committed
17
    public class Lotto : INotifyPropertyChanged
M1888's avatar
M1888 committed
18
    {
M1888's avatar
M1888 committed
19 20 21 22
        // Mitataan kunkin kierroksen kesto, jotta
        // osataan näyttää nättiä statistiikkaa.
        private Stopwatch stopwatch;

M1888's avatar
M1888 committed
23 24
        // Neljä eri listaa eri voittotasoille:
        // 5, 6, 6+1 ja 7 oikein.
M1888's avatar
M1888 committed
25 26 27 28 29 30 31 32 33 34 35 36
        // Näihin laitetaan joka kierroksella voittaneet pelaajat,
        // ja sitten jaetaan niistä tarkistuksen jälkeen voitot.
        // 
        // Pienempiä voittotasoja ei tarvitse listata, koska niiden voittosumma
        // ei riipu voittajien määristä vaan on aina sama. Ne voidaan hoitaa helpommin.
        //
        // Olisin mielummin toteuttanut tämän indeksoituna listana:
        // eli oikein[5] takaa löytyisi lista viisi oikein pelaajista, jne..
        // Parallelismin kannalta tämä oli kuitenkin hankalaa, ja valmiiden
        // thread-safe ConcurrentBagien käyttö on järkevämpää, vaikkakin rumaa.
        private ConcurrentBag<Pelaaja> oikein5;
        private ConcurrentBag<Pelaaja> oikein6;
M1888's avatar
M1888 committed
37
        private ConcurrentBag<Pelaaja> oikein6_1;
M1888's avatar
M1888 committed
38 39
        private ConcurrentBag<Pelaaja> oikein7;

M1888's avatar
M1888 committed
40 41 42 43 44 45 46
        private short viikko;
        public short Viikko
        {
            get
            {
                return viikko;
            }
M1888's avatar
M1888 committed
47 48 49
            set
            {
                viikko = value;
M1888's avatar
M1888 committed
50
                if (viikko > 52)
M1888's avatar
M1888 committed
51 52 53 54 55 56
                {
                    viikko = 1;
                    Vuosi++;
                }
                RaisePropertyChanged();
            }
M1888's avatar
M1888 committed
57 58 59 60 61 62 63 64 65
        }

        private short vuosi;
        public short Vuosi
        {
            get
            {
                return vuosi;
            }
M1888's avatar
M1888 committed
66 67 68 69 70
            set
            {
                vuosi = value;
                RaisePropertyChanged();
            }
M1888's avatar
M1888 committed
71 72
        }

M1888's avatar
M1888 committed
73 74 75 76 77 78 79 80
        public int Kierros
        {
            get
            {
                return ((Vuosi - 1) * 52) + Viikko;
            }
        }

M1888's avatar
M1888 committed
81 82
        public ConcurrentBag<Pelaaja> Pelaajat { get; set; }

M1888's avatar
M1888 committed
83 84 85 86
        // Nämä voi olla pelkkiä listoja, koska threadeissa vain lueataan niistä.
        public List<string> Etunimet { get; set; }
        public List<string> Sukunimet { get; set; }

M1888's avatar
M1888 committed
87 88 89 90
        // Tässä pidetään joka kierroksella jaettujen eri voittoluokkien
        // palkkioiden määrät.
        private int[] voitot;
        public int[] Voitot
M1888's avatar
M1888 committed
91 92 93
        {
            get
            {
M1888's avatar
M1888 committed
94
                return voitot;
M1888's avatar
M1888 committed
95 96 97
            }
        }

M1888's avatar
M1888 committed
98 99
        private int potti;
        public int Potti
M1888's avatar
M1888 committed
100 101 102 103 104
        {
            get
            {
                return potti;
            }
M1888's avatar
M1888 committed
105 106 107 108 109
            set
            {
                potti = value;
                RaisePropertyChanged();
            }
M1888's avatar
M1888 committed
110 111
        }

M1888's avatar
M1888 committed
112
        public event PropertyChangedEventHandler PropertyChanged;
M1888's avatar
M1888 committed
113

M1888's avatar
M1888 committed
114 115 116 117 118 119 120
        private Voittorivi voittorivi;
        public Voittorivi Voittorivi
        {
            get
            {
                return voittorivi;
            }
M1888's avatar
M1888 committed
121 122 123 124 125 126 127
            set
            {
                voittorivi = value;
                RaisePropertyChanged();
            }
        }

M1888's avatar
M1888 committed
128 129 130 131 132 133 134 135 136 137
        // StatusHistory pitää sisällään kaikki tilaviestit, jotka lottokone suustaan päästää.
        // Status-string taas on wrapperi historian ympärille, niin saadaan helposti viimeisin
        // viesti sekä voidaan lisätä viestejä historiaan helposti vain asettamalla Statukseen jotain.
        //   Esim.  Status = "Jee";
        // StatusLock on objekti joka lukitaan kun StatusHistorya päivitetään.
        // UI threadissa kutsutaan BindingOperations.EnableCollectionSynchronization(),
        // jolle annetaan tämä sama objekti, niin UI-thread ei sörki kokoelmaa samalla
        // kun me päivitetään sitä muualla.
        public object StatusLock { get; set; }
        public ObservableCollection<string> StatusHistory;
M1888's avatar
M1888 committed
138 139 140 141
        public string Status
        {
            get
            {
M1888's avatar
M1888 committed
142
                return StatusHistory.FirstOrDefault();
M1888's avatar
M1888 committed
143 144 145
            }
            set
            {
M1888's avatar
M1888 committed
146 147 148 149 150 151 152
                lock (StatusLock)
                {
                    // En saanut ListBoxin autoskrollausta toimimaan uusimpaan itemiin (joka siis tulee
                    // alimmaiseksi), joten lisätään uudet viestit ylimmäiseksi ja ongelma poistuu. :)
                    StatusHistory.Insert(0, value);
                    RaisePropertyChanged();
                }
M1888's avatar
M1888 committed
153 154 155
            }
        }

M1888's avatar
M1888 committed
156 157
        // Lasketaan kierroksella mukana olleiden rivien määrä.
        public int Rivimaara
M1888's avatar
M1888 committed
158 159 160
        {
            get
            {
M1888's avatar
M1888 committed
161
                int n = 0;
M1888's avatar
M1888 committed
162
                foreach (Pelaaja p in Pelaajat)
M1888's avatar
M1888 committed
163 164 165 166
                {
                    n += p.Rivit.Count();
                }
                return n;
M1888's avatar
M1888 committed
167 168 169
            }
        }

M1888's avatar
M1888 committed
170 171 172
        // Konstruktori lottokoneen pääluokalle
        public Lotto()
        {
M1888's avatar
M1888 committed
173
            Potti = 0;
M1888's avatar
M1888 committed
174 175 176 177 178 179 180 181 182 183 184 185 186 187

            voitot = new int[6];
            // voitot[0]: 7 oikein
            // voitot[1]: 6+1 oikein
            // voitot[2]: 6 oikein
            // voitot[3]: 5 oikein
            // voitot[4]: 4 oikein
            // voitot[5]: 3+1 oikein
            // näistä potista riippumattomia voittosummia on 7, 4 ja 3+1 oikein,
            // joten ne voidaan asettaa tässä.
            voitot[0] = 1_250_000;
            voitot[4] = 10;
            voitot[5] = 2;

M1888's avatar
M1888 committed
188 189
            Viikko = 0;
            Vuosi = 1;
M1888's avatar
M1888 committed
190
            Pelaajat = new ConcurrentBag<Pelaaja>();
M1888's avatar
M1888 committed
191 192 193 194 195 196 197
            oikein5 = new ConcurrentBag<Pelaaja>();
            oikein6 = new ConcurrentBag<Pelaaja>();
            oikein6_1 = new ConcurrentBag<Pelaaja>();
            oikein7 = new ConcurrentBag<Pelaaja>();
            StatusLock = new object();
            StatusHistory = new ObservableCollection<string>();
            Status = "Veikkaus: Pelaa maltilla :):):):)";
M1888's avatar
M1888 committed
198

M1888's avatar
M1888 committed
199
            stopwatch = Stopwatch.StartNew();
M1888's avatar
M1888 committed
200 201 202 203 204 205 206 207 208
            try
            {
                Etunimet = ViewModel.Nimigeneraattori.Etunimet();
                Sukunimet = ViewModel.Nimigeneraattori.Sukunimet();
            }
            catch (Exception ex)
            {
                Status = $"Nimien lataus epäonnistui: {ex.Message}";
            }
M1888's avatar
M1888 committed
209
            LisaaPelaajia(Properties.Settings.Default.Pelaajia, Properties.Settings.Default.Riveja);
M1888's avatar
M1888 committed
210
            stopwatch.Stop();
M1888's avatar
M1888 committed
211
            Status = $"Lisätty {Pelaajat.Count():N0} pelaajaa ({stopwatch.ElapsedMilliseconds}ms)";
M1888's avatar
M1888 committed
212 213
        }

M1888's avatar
M1888 committed
214
        public void LisaaPelaajia(int pelaajia, int riveja)
M1888's avatar
M1888 committed
215
        {
M1888's avatar
M1888 committed
216
            for (int i = 0; i < pelaajia; i++)
M1888's avatar
M1888 committed
217
            {
M1888's avatar
M1888 committed
218 219 220
                Pelaajat.Add(new Pelaaja(
                    $"{Etunimet[Rand.Next(0, Etunimet.Count())]} {Sukunimet[Rand.Next(0, Sukunimet.Count())]}",
                    riveja, Properties.Settings.Default.Voimassa));
M1888's avatar
M1888 committed
221
            }
M1888's avatar
M1888 committed
222 223
        }

M1888's avatar
M1888 committed
224 225 226 227 228
        public void PoistaPelaajia(int maara)
        {
            Pelaajat = new ConcurrentBag<Pelaaja>(Pelaajat.Skip(maara).ToList());
        }

M1888's avatar
M1888 committed
229 230
        public void Tick()
        {
M1888's avatar
M1888 committed
231 232 233
            stopwatch.Reset();
            stopwatch.Start();

M1888's avatar
M1888 committed
234
            Viikko++;
M1888's avatar
M1888 committed
235

M1888's avatar
M1888 committed
236 237 238 239 240 241 242 243 244 245 246
            // Tyhjennetään voittajien listat vain jos viime kierroksella oli voittajia.
            // ConcurrentBagia ei saa helposti tyhjennettyä, joten luodaan uusi päälle.
            if (!oikein5.IsEmpty)
                oikein5 = new ConcurrentBag<Pelaaja>();
            if (!oikein6.IsEmpty)
                oikein6 = new ConcurrentBag<Pelaaja>();
            if (!oikein6_1.IsEmpty)
                oikein6_1 = new ConcurrentBag<Pelaaja>();
            if (!oikein7.IsEmpty)
                oikein7 = new ConcurrentBag<Pelaaja>();

M1888's avatar
M1888 committed
247
            Voittorivi = new Voittorivi();
M1888's avatar
M1888 committed
248

M1888's avatar
M1888 committed
249
            EtsiVoittajat();
M1888's avatar
M1888 committed
250
            RaisePropertyChanged("Rivimaara");
M1888's avatar
M1888 committed
251
            Potti = (int)(Rivimaara * Properties.Settings.Default.Rivihinta);
M1888's avatar
M1888 committed
252 253 254

            JaaVoitot();
            stopwatch.Stop();
M1888's avatar
M1888 committed
255 256 257

            Status = $"-Kierros {Kierros} ({stopwatch.ElapsedMilliseconds}ms)-";

M1888's avatar
M1888 committed
258 259
        }

M1888's avatar
M1888 committed
260 261 262
        // Voittajat täytyy erikseen etsiä ja sitten vasta jakaa voitot.
        // Samassa loopissa niitä ei voi tehdä, koska voittosumma riippuu voittajien
        // määrästä. Se on selvillä vasta kun kaikki on käyty läpi.
M1888's avatar
M1888 committed
263
        private void EtsiVoittajat()
M1888's avatar
M1888 committed
264
        {
M1888's avatar
M1888 committed
265
            Parallel.ForEach(Pelaajat, (p) =>
M1888's avatar
M1888 committed
266
            {
M1888's avatar
M1888 committed
267 268 269
                // päivitetään lottorivit
                p.Tick();
                foreach (Lottorivi r in p.Rivit)
M1888's avatar
M1888 committed
270
                {
M1888's avatar
M1888 committed
271
                    switch (r.Oikein(Voittorivi))
M1888's avatar
M1888 committed
272
                    {
M1888's avatar
M1888 committed
273 274 275 276 277
                        // 3+1 ja 4 oikein on tasamääräisiä voittoja, joten ne voidaan jakaa
                        // suoraan täällä. Isommat voitot koostetaan ConcurrentBageihin,
                        // kunnes voittajien määrä on selvillä.
                        case 3:
                            // kolme oikein, niin tarvitaan vielä lisänumero
M1888's avatar
M1888 committed
278
                            if (r.Numerot.Contains(Voittorivi.Lisanumero))
M1888's avatar
M1888 committed
279 280 281 282 283 284 285 286 287
                            {
                                // Pottia voi käsitellä useampi säie samaan aikaan, siksi
                                // System.Threading.Intelocked.Add() joka on atominen operaatio
                                Interlocked.Add(ref potti, -2);

                                // tätä pelaajaa ei taas käsitellä kuin me tässä ja nyt.
                                p.Saldo += 2;
                            }
                            break;
M1888's avatar
M1888 committed
288
                        case 4:
M1888's avatar
M1888 committed
289 290
                            Interlocked.Add(ref potti, -10);
                            p.Saldo += 10;
M1888's avatar
M1888 committed
291 292
                            break;
                        case 5:
M1888's avatar
M1888 committed
293
                            oikein5.Add(p);
M1888's avatar
M1888 committed
294 295
                            break;
                        case 6:
M1888's avatar
M1888 committed
296 297 298 299 300
                            // Täällä pitää erotella vielä 6 ja 6+1 oikein rivit
                            if (r.Numerot.Contains(Voittorivi.Lisanumero))
                                oikein6_1.Add(p);
                            else
                                oikein6.Add(p);
M1888's avatar
M1888 committed
301 302
                            break;
                        case 7:
M1888's avatar
M1888 committed
303
                            oikein7.Add(p);
M1888's avatar
M1888 committed
304 305 306
                            break;
                        default:
                            break;
M1888's avatar
M1888 committed
307 308
                    }
                }
M1888's avatar
M1888 committed
309 310 311 312
            });
        }

        // https://fi.wikipedia.org/wiki/Lotto_(Veikkaus)#Voittoluokat_ja_voiton_todenn%C3%A4k%C3%B6isyys
M1888's avatar
M1888 committed
313
        // 3+1: 2 e 
M1888's avatar
M1888 committed
314 315 316
        // 4 oikein: 10 e
        // 5 oikein: 3% potista voittajien kesken
        // 6 oikein: 2,5% potista voittajien kesken
M1888's avatar
M1888 committed
317
        // 6+1: 3,8% potista voittajien kesken
M1888's avatar
M1888 committed
318
        // 7: päävoitto
M1888's avatar
M1888 committed
319 320 321

        // Luotetaan, että aiemmat tarkistukset on oikein, eikä
        // listoilla ole pelaajia joiden rivit eivät ole voittaneet...
M1888's avatar
M1888 committed
322 323
        private void JaaVoitot()
        {
M1888's avatar
M1888 committed
324 325

            if (!oikein5.IsEmpty)
M1888's avatar
M1888 committed
326
            {
M1888's avatar
M1888 committed
327
                int voittajia = oikein5.Count();
M1888's avatar
M1888 committed
328
                voitot[3] = ((potti / 100) * 3) / voittajia;
M1888's avatar
M1888 committed
329

M1888's avatar
M1888 committed
330
                Status = $"5 oikein voittoja {voittajia:N0} kpl ({voitot[3]:C0})";
M1888's avatar
M1888 committed
331

M1888's avatar
M1888 committed
332
                while (oikein5.TryTake(out Pelaaja p) != false)
M1888's avatar
M1888 committed
333
                {
M1888's avatar
M1888 committed
334 335
                    p.Saldo += voitot[3];
                    Potti -= voitot[3];
M1888's avatar
M1888 committed
336 337
                }
            }
M1888's avatar
M1888 committed
338

M1888's avatar
M1888 committed
339 340 341
            if (!oikein6.IsEmpty)
            {
                int voittajia = oikein6.Count();
M1888's avatar
M1888 committed
342
                voitot[2] = (int)((potti / 100) * 2.5) / voittajia;
M1888's avatar
M1888 committed
343

M1888's avatar
M1888 committed
344
                Status = $"6 oikein voittoja {voittajia:N0} kpl ({voitot[2]:C0})";
M1888's avatar
M1888 committed
345 346

                while (oikein6.TryTake(out Pelaaja p) != false)
M1888's avatar
M1888 committed
347
                {
M1888's avatar
M1888 committed
348 349
                    p.Saldo += voitot[2];
                    Potti -= voitot[2];
M1888's avatar
M1888 committed
350
                }
M1888's avatar
M1888 committed
351 352
            }

M1888's avatar
M1888 committed
353
            if (!oikein6_1.IsEmpty)
M1888's avatar
M1888 committed
354 355
            {
                int voittajia = oikein6_1.Count();
M1888's avatar
M1888 committed
356
                voitot[1] = (int)((potti / 100) * 3.8) / voittajia;
M1888's avatar
M1888 committed
357

M1888's avatar
M1888 committed
358
                Status = $"6+1 oikein voittoja {voittajia:N0} kpl ({voitot[1]:C0})";
M1888's avatar
M1888 committed
359 360 361

                while (oikein6.TryTake(out Pelaaja p) != false)
                {
M1888's avatar
M1888 committed
362 363
                    p.Saldo += voitot[1];
                    Potti -= voitot[1];
M1888's avatar
M1888 committed
364 365 366 367
                }
            }

            if (!oikein7.IsEmpty)
M1888's avatar
M1888 committed
368 369
            {
                int voittajia = oikein7.Count();
M1888's avatar
M1888 committed
370
                voitot[0] = (int)(voitot[0] / voittajia);
M1888's avatar
M1888 committed
371

M1888's avatar
M1888 committed
372
                Status = $"!!! 7 oikein !!!! {voitot[0]:C0} ({voittajia:N0} kpl) !!!";
M1888's avatar
M1888 committed
373 374 375

                while (oikein7.TryTake(out Pelaaja p) != false)
                {
M1888's avatar
M1888 committed
376
                    p.Saldo += voitot[0];
M1888's avatar
M1888 committed
377
                }
M1888's avatar
M1888 committed
378
                voitot[0] = 1_250_000;
M1888's avatar
M1888 committed
379
            }
M1888's avatar
M1888 committed
380 381
            else
            {
M1888's avatar
M1888 committed
382
                // n. 20 miljoonaa lienee hyvä maksimi päävoitto
M1888's avatar
M1888 committed
383
                if (voitot[0] < 20_000_000)
M1888's avatar
M1888 committed
384
                    voitot[0] = (int)(voitot[0] * 1.07);
M1888's avatar
M1888 committed
385
            }
M1888's avatar
M1888 committed
386
            RaisePropertyChanged("Voitot");
M1888's avatar
M1888 committed
387 388 389 390
        }

        protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
        {
M1888's avatar
M1888 committed
391
            if (PropertyChanged != null)
M1888's avatar
M1888 committed
392
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
M1888's avatar
M1888 committed
393 394 395
        }
    }
}