Lotto.cs 14.8 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 25
        // Lista lottokoneen pelaajista
        public ConcurrentBag<Pelaaja> Pelaajat { get; set; }

M1888's avatar
M1888 committed
26 27
        // Neljä eri listaa eri voittotasoille:
        // 5, 6, 6+1 ja 7 oikein.
M1888's avatar
M1888 committed
28 29 30 31 32 33 34 35 36 37 38 39
        // 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
40
        private ConcurrentBag<Pelaaja> oikein6_1;
M1888's avatar
M1888 committed
41 42
        private ConcurrentBag<Pelaaja> oikein7;

M1888's avatar
M1888 committed
43 44 45
        // Viikolla on laskin, joka nostaa myös vuosia.
        // Lottokoneen loopissa voidaan siis vaan kutsua Viikko++,
        // ja kun vuosi tulee täyteen, alkaa viikot alusta automaattisesti.
M1888's avatar
M1888 committed
46 47 48 49 50 51 52
        private short viikko;
        public short Viikko
        {
            get
            {
                return viikko;
            }
M1888's avatar
M1888 committed
53 54 55
            set
            {
                viikko = value;
M1888's avatar
M1888 committed
56
                if (viikko > 52)
M1888's avatar
M1888 committed
57 58 59 60 61 62
                {
                    viikko = 1;
                    Vuosi++;
                }
                RaisePropertyChanged();
            }
M1888's avatar
M1888 committed
63 64
        }

M1888's avatar
M1888 committed
65
        // monesko vuosi on menossa
M1888's avatar
M1888 committed
66 67 68 69 70 71 72
        private short vuosi;
        public short Vuosi
        {
            get
            {
                return vuosi;
            }
M1888's avatar
M1888 committed
73 74 75 76 77
            set
            {
                vuosi = value;
                RaisePropertyChanged();
            }
M1888's avatar
M1888 committed
78 79
        }

M1888's avatar
M1888 committed
80
        // Kuinka mones kierros on menossa?
M1888's avatar
M1888 committed
81 82 83 84 85 86 87 88
        public int Kierros
        {
            get
            {
                return ((Vuosi - 1) * 52) + Viikko;
            }
        }

M1888's avatar
M1888 committed
89

M1888's avatar
M1888 committed
90 91 92 93
        // 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
94 95 96 97
        // Tässä pidetään joka kierroksella jaettujen eri voittoluokkien
        // palkkioiden määrät.
        private int[] voitot;
        public int[] Voitot
M1888's avatar
M1888 committed
98 99 100
        {
            get
            {
M1888's avatar
M1888 committed
101
                return voitot;
M1888's avatar
M1888 committed
102 103 104
            }
        }

M1888's avatar
M1888 committed
105
        // Joka kierroksella jaossa oleva potti
M1888's avatar
M1888 committed
106 107
        private int potti;
        public int Potti
M1888's avatar
M1888 committed
108 109 110 111 112
        {
            get
            {
                return potti;
            }
M1888's avatar
M1888 committed
113 114 115 116 117
            set
            {
                potti = value;
                RaisePropertyChanged();
            }
M1888's avatar
M1888 committed
118 119
        }

M1888's avatar
M1888 committed
120
        public event PropertyChangedEventHandler PropertyChanged;
M1888's avatar
M1888 committed
121

M1888's avatar
M1888 committed
122
        // Kierroksen voittorivi
M1888's avatar
M1888 committed
123 124 125 126 127 128 129
        private Voittorivi voittorivi;
        public Voittorivi Voittorivi
        {
            get
            {
                return voittorivi;
            }
M1888's avatar
M1888 committed
130 131 132 133 134 135 136
            set
            {
                voittorivi = value;
                RaisePropertyChanged();
            }
        }

M1888's avatar
M1888 committed
137 138 139 140 141 142 143 144 145 146
        // 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
147 148 149 150
        public string Status
        {
            get
            {
M1888's avatar
M1888 committed
151
                return StatusHistory.FirstOrDefault();
M1888's avatar
M1888 committed
152 153 154
            }
            set
            {
M1888's avatar
M1888 committed
155 156 157 158 159 160 161
                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
162 163 164
            }
        }

M1888's avatar
M1888 committed
165 166
        // Lasketaan kierroksella mukana olleiden rivien määrä.
        public int Rivimaara
M1888's avatar
M1888 committed
167 168 169
        {
            get
            {
M1888's avatar
M1888 committed
170
                int n = 0;
M1888's avatar
M1888 committed
171
                foreach (Pelaaja p in Pelaajat)
M1888's avatar
M1888 committed
172 173 174 175
                {
                    n += p.Rivit.Count();
                }
                return n;
M1888's avatar
M1888 committed
176 177 178
            }
        }

M1888's avatar
M1888 committed
179 180 181
        // Konstruktori lottokoneen pääluokalle
        public Lotto()
        {
M1888's avatar
M1888 committed
182 183
            // kello käyntiin
            stopwatch = Stopwatch.StartNew();
M1888's avatar
M1888 committed
184

M1888's avatar
M1888 committed
185
            Potti = 0;
M1888's avatar
M1888 committed
186 187 188 189 190 191 192 193 194 195 196 197 198
            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
199 200
            Viikko = 0;
            Vuosi = 1;
M1888's avatar
M1888 committed
201
            Pelaajat = new ConcurrentBag<Pelaaja>();
M1888's avatar
M1888 committed
202 203 204 205 206 207 208
            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
209

M1888's avatar
M1888 committed
210 211 212 213 214 215 216 217 218
            try
            {
                Etunimet = ViewModel.Nimigeneraattori.Etunimet();
                Sukunimet = ViewModel.Nimigeneraattori.Sukunimet();
            }
            catch (Exception ex)
            {
                Status = $"Nimien lataus epäonnistui: {ex.Message}";
            }
M1888's avatar
M1888 committed
219 220
            stopwatch.Stop();
            Status = $"Alustettu lottokone ({stopwatch.ElapsedMilliseconds}ms)";
M1888's avatar
M1888 committed
221

M1888's avatar
M1888 committed
222
            LisaaPelaajia(Properties.Settings.Default.Pelaajia, Properties.Settings.Default.Riveja);
M1888's avatar
M1888 committed
223 224
        }

M1888's avatar
M1888 committed
225
        public void LisaaPelaajia(int pelaajia, int riveja)
M1888's avatar
M1888 committed
226
        {
M1888's avatar
M1888 committed
227
            stopwatch.Restart();
M1888's avatar
M1888 committed
228 229 230 231 232 233 234 235 236 237 238

            //for (int i = 0; i < pelaajia; i++)
            Parallel.For(0, pelaajia,
                i => {
                    Pelaajat.Add(new Pelaaja(
                        $"{Etunimet[Rand.Next(0, Etunimet.Count())]} {Sukunimet[Rand.Next(0, Sukunimet.Count())]}",
                        riveja, Properties.Settings.Default.Voimassa));
                });

            stopwatch.Stop();
            Status = $"Lisätty {pelaajia} pelaajaa ({stopwatch.ElapsedMilliseconds}ms)";
M1888's avatar
M1888 committed
239 240
        }

M1888's avatar
M1888 committed
241 242
        public void PoistaPelaajia(int maara)
        {
M1888's avatar
M1888 committed
243
            stopwatch.Restart();
M1888's avatar
M1888 committed
244
            Pelaajat = new ConcurrentBag<Pelaaja>(Pelaajat.Skip(maara).ToList());
M1888's avatar
M1888 committed
245 246
            stopwatch.Stop();
            Status = $"Poistettu {maara} pelaajaa ({stopwatch.ElapsedMilliseconds}ms)";
M1888's avatar
M1888 committed
247 248
        }

M1888's avatar
M1888 committed
249
        // Tämä metodi edistää lottokoneen simulaatiota viikolla.
M1888's avatar
M1888 committed
250 251
        public void Tick()
        {
M1888's avatar
M1888 committed
252
            stopwatch.Restart();
M1888's avatar
M1888 committed
253
            Viikko++;
M1888's avatar
M1888 committed
254

M1888's avatar
M1888 committed
255 256 257 258 259 260 261 262 263 264 265
            // 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
266
            // Arvotaan uusi voittorivi
M1888's avatar
M1888 committed
267
            Voittorivi = new Voittorivi();
M1888's avatar
M1888 committed
268

M1888's avatar
M1888 committed
269 270 271
            // Etsitään voitot (tämä kutsuu myös pelaajien Pelaaja.Tick() metodia,
            // joka päivittää lottorivit ja arpoo ne tarvittaessa uudelleen, 
            // sekä päivittää pelaajien saldot.
M1888's avatar
M1888 committed
272
            EtsiVoittajat();
M1888's avatar
M1888 committed
273 274 275

            // Rivimäärä on laskettu ominaisuus, johon ei aseteta suoraan mitään arvoa.
            // Kun se päviittyy, pitää siitä kertoa UI:lle, jonne luku on sidottu.
M1888's avatar
M1888 committed
276
            RaisePropertyChanged("Rivimaara");
M1888's avatar
M1888 committed
277 278

            // Lasketaan kierroksen uusi potti
M1888's avatar
M1888 committed
279
            Potti = (int)(Rivimaara * Properties.Settings.Default.Rivihinta);
M1888's avatar
M1888 committed
280

M1888's avatar
M1888 committed
281
            // Lopuksi jaetaan kierroksen voitot
M1888's avatar
M1888 committed
282
            JaaVoitot();
M1888's avatar
M1888 committed
283

M1888's avatar
M1888 committed
284
            stopwatch.Stop();
M1888's avatar
M1888 committed
285
            Status = $"-Kierros {Kierros} ({stopwatch.ElapsedMilliseconds}ms)-";
M1888's avatar
M1888 committed
286 287
        }

M1888's avatar
M1888 committed
288 289 290
        // 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
291
        private void EtsiVoittajat()
M1888's avatar
M1888 committed
292
        {
M1888's avatar
M1888 committed
293
            Parallel.ForEach(Pelaajat, (p) =>
M1888's avatar
M1888 committed
294
            {
M1888's avatar
M1888 committed
295 296 297
                // päivitetään lottorivit
                p.Tick();
                foreach (Lottorivi r in p.Rivit)
M1888's avatar
M1888 committed
298
                {
M1888's avatar
M1888 committed
299
                    switch (r.Oikein(Voittorivi))
M1888's avatar
M1888 committed
300
                    {
M1888's avatar
M1888 committed
301 302 303 304 305
                        // 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
306
                            if (r.Numerot.Contains(Voittorivi.Lisanumero))
M1888's avatar
M1888 committed
307 308 309 310 311 312 313 314 315
                            {
                                // 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
316
                        case 4:
M1888's avatar
M1888 committed
317 318
                            Interlocked.Add(ref potti, -10);
                            p.Saldo += 10;
M1888's avatar
M1888 committed
319 320
                            break;
                        case 5:
M1888's avatar
M1888 committed
321
                            oikein5.Add(p);
M1888's avatar
M1888 committed
322 323
                            break;
                        case 6:
M1888's avatar
M1888 committed
324 325 326 327 328
                            // 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
329 330
                            break;
                        case 7:
M1888's avatar
M1888 committed
331
                            oikein7.Add(p);
M1888's avatar
M1888 committed
332 333 334
                            break;
                        default:
                            break;
M1888's avatar
M1888 committed
335 336
                    }
                }
M1888's avatar
M1888 committed
337 338 339 340
            });
        }

        // https://fi.wikipedia.org/wiki/Lotto_(Veikkaus)#Voittoluokat_ja_voiton_todenn%C3%A4k%C3%B6isyys
M1888's avatar
M1888 committed
341
        // 3+1: 2 e 
M1888's avatar
M1888 committed
342 343 344
        // 4 oikein: 10 e
        // 5 oikein: 3% potista voittajien kesken
        // 6 oikein: 2,5% potista voittajien kesken
M1888's avatar
M1888 committed
345
        // 6+1: 3,8% potista voittajien kesken
M1888's avatar
M1888 committed
346
        // 7: päävoitto
M1888's avatar
M1888 committed
347 348 349

        // Luotetaan, että aiemmat tarkistukset on oikein, eikä
        // listoilla ole pelaajia joiden rivit eivät ole voittaneet...
M1888's avatar
M1888 committed
350 351
        private void JaaVoitot()
        {
M1888's avatar
M1888 committed
352 353 354
            // lasketaan prosentit voitoille alkuperäisestä potista, vaikka
            // voittoja jakaessa jo vähennetään kierroksen oikeasta potista.
            int alkupotti = potti;
M1888's avatar
M1888 committed
355 356

            if (!oikein5.IsEmpty)
M1888's avatar
M1888 committed
357
            {
M1888's avatar
M1888 committed
358
                int voittajia = oikein5.Count();
M1888's avatar
M1888 committed
359 360
                // 3% voittajien kesken
                voitot[3] = ((alkupotti / 100) * 3) / voittajia;
M1888's avatar
M1888 committed
361

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

M1888's avatar
M1888 committed
364
                while (oikein5.TryTake(out Pelaaja p) != false)
M1888's avatar
M1888 committed
365
                {
M1888's avatar
M1888 committed
366 367
                    p.Saldo += voitot[3];
                    Potti -= voitot[3];
M1888's avatar
M1888 committed
368 369
                }
            }
M1888's avatar
M1888 committed
370

M1888's avatar
M1888 committed
371 372 373
            if (!oikein6.IsEmpty)
            {
                int voittajia = oikein6.Count();
M1888's avatar
M1888 committed
374 375
                // 2.5% voittajien kesken
                voitot[2] = (int)((alkupotti / 100) * 2.5) / voittajia;
M1888's avatar
M1888 committed
376

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

                while (oikein6.TryTake(out Pelaaja p) != false)
M1888's avatar
M1888 committed
380
                {
M1888's avatar
M1888 committed
381 382
                    p.Saldo += voitot[2];
                    Potti -= voitot[2];
M1888's avatar
M1888 committed
383
                }
M1888's avatar
M1888 committed
384 385
            }

M1888's avatar
M1888 committed
386
            if (!oikein6_1.IsEmpty)
M1888's avatar
M1888 committed
387 388
            {
                int voittajia = oikein6_1.Count();
M1888's avatar
M1888 committed
389 390
                // 3.8% voittajien kesken
                voitot[1] = (int)((alkupotti / 100) * 3.8) / voittajia;
M1888's avatar
M1888 committed
391

M1888's avatar
M1888 committed
392
                Status = $"6+1 oikein voittoja {voittajia:N0} kpl ({voitot[1]:C0})";
M1888's avatar
M1888 committed
393 394 395

                while (oikein6.TryTake(out Pelaaja p) != false)
                {
M1888's avatar
M1888 committed
396 397
                    p.Saldo += voitot[1];
                    Potti -= voitot[1];
M1888's avatar
M1888 committed
398 399 400 401
                }
            }

            if (!oikein7.IsEmpty)
M1888's avatar
M1888 committed
402 403
            {
                int voittajia = oikein7.Count();
M1888's avatar
M1888 committed
404
                voitot[0] = (int)(voitot[0] / voittajia);
M1888's avatar
M1888 committed
405

M1888's avatar
M1888 committed
406
                Status = $"!!! 7 oikein !!!! {voitot[0]:C0} ({voittajia:N0} kpl) !!!";
M1888's avatar
M1888 committed
407 408 409

                while (oikein7.TryTake(out Pelaaja p) != false)
                {
M1888's avatar
M1888 committed
410
                    p.Saldo += voitot[0];
M1888's avatar
M1888 committed
411
                }
M1888's avatar
M1888 committed
412
                voitot[0] = 1_250_000;
M1888's avatar
M1888 committed
413
            }
M1888's avatar
M1888 committed
414 415
            else
            {
M1888's avatar
M1888 committed
416
                // n. 20 miljoonaa lienee hyvä maksimi päävoitto
M1888's avatar
M1888 committed
417
                if (voitot[0] < 20_000_000)
M1888's avatar
M1888 committed
418
                    voitot[0] = (int)(voitot[0] * 1.07);
M1888's avatar
M1888 committed
419
            }
M1888's avatar
M1888 committed
420
            RaisePropertyChanged("Voitot");
M1888's avatar
M1888 committed
421 422 423 424
        }

        protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
        {
M1888's avatar
M1888 committed
425
            if (PropertyChanged != null)
M1888's avatar
M1888 committed
426
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
M1888's avatar
M1888 committed
427 428 429
        }
    }
}