-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy path2019-ncov-simulate.linq
417 lines (365 loc) · 11.8 KB
/
2019-ncov-simulate.linq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
<Query Kind="Program">
<NuGetReference>FlysEngine.Desktop</NuGetReference>
<Namespace>FlysEngine.Desktop</Namespace>
<Namespace>SharpDX.Direct2D1</Namespace>
<Namespace>FlysEngine</Namespace>
<Namespace>SharpDX</Namespace>
<Namespace>System.Windows.Forms</Namespace>
<Namespace>SharpDX.DXGI</Namespace>
</Query>
// 第一版由 周杰 制作,源码:https://github.com/sdcb/2019-ncp-simulation
// 修改和转发时,请保留这两行文字,否则视为侵权。
static Random random = new Random();
static double MoveWilling = 0.90f; // 移动意愿,0-1
static bool WearMask = false; // 是否戴口罩
static int HospitalBeds = 40; // 床位数
const float InffectRate = 0.8f; // 靠得够近时,被携带者感染的机率
const float SecondsPerDay = 0.3f; // 模拟器的秒数,对应真实一天
const float MovingDistancePerDay = 10.0f; // 每天移动距离
const int InitialInfectorCount = 5; // 最初感染者数
const double DeathRate = 0.021; // 死亡率5
// 要靠多近,才会触发感染验证
static float SafeDistance() => WearMask ? 1.5f : 3.5f;
// 住院治愈时间,最短5天,最长12.75天,平均约7天
static float GenerateCureDays() => random.NextFloat(5, 12.75f);
// 潜伏期,1-14天
static float GenerateShadowDays() => random.Next(1, 14);
// 发病后,就医时间,0-3天
static float GenerateToHospitalDays() => random.Next(0, 3);
// 显示参数
const float PersonSize = 4.0f;
const float HospitalBedSize = 20.0f;
const float HospitalHeight = 800.0f;
const float HospitalY = -400; const float HospitalX = 410;
void Main()
{
Util.NewProcess = true;
using (var w = new VirusWindow
{
Text = "病毒传播模拟",
ClientSize = new System.Drawing.Size(600, 600),
StartPosition = FormStartPosition.CenterScreen
})
{
RenderLoop.Run(w, () => w.Render(1, PresentFlags.None));
}
}
class VirusWindow : RenderWindow
{
City City = City.Create();
protected override void OnUpdateLogic(float dt)
{
City.Update(dt);
}
protected override void OnKeyPress(KeyPressEventArgs e)
{
switch (e.KeyChar)
{
case '1': MoveWilling = 0.10f; break;
case '2': MoveWilling = 0.50f; break;
case '3': MoveWilling = 0.90f; break;
case 'M': WearMask = !WearMask; break;
case 'A': HospitalBeds += 40; break;
case 'D': HospitalBeds -= 40; break;
case 'R':
{
if (MessageBox.Show("要重来吗?", "确认", MessageBoxButtons.YesNo) == DialogResult.Yes)
{
City = City.Create();
}
break;
}
}
}
protected override void OnDraw(DeviceContext ctx)
{
ctx.Clear(Color.DarkGray);
float minEdge = Math.Min(ClientSize.Width / 2, ClientSize.Height / 2);
float scale = minEdge / 540; // relative coordinate
ctx.Transform =
Matrix3x2.Scaling(scale) *
Matrix3x2.Translation(ClientSize.Width / 2, ClientSize.Height / 2);
City.Draw(ctx, XResource);
}
}
class City
{
public const int Population = 5000;
public float CitySize = 400;
private float day = 1;
public Person[] Persons;
private SortedSet<int> infectorIds = new SortedSet<int>();
private SortedSet<int> healthyIds = new SortedSet<int>();
public Hospital Hospital = new Hospital();
public static City Create()
{
var c = new City();
c.Persons = Enumerable.Range(1, Population)
.Select(x => Person.Create(c.CitySize))
.ToArray();
c.healthyIds = new SortedSet<int>(Enumerable
.Range(0, Population));
for (var i = 0; i < InitialInfectorCount; ++i) c.Infect(i);
return c;
}
public void Infect(int personId)
{
Persons[personId].Status = PersonStatus.InfectedInShadow;
Persons[personId].EstimateDays = GenerateShadowDays();
healthyIds.Remove(personId);
infectorIds.Add(personId);
}
public void Draw(DeviceContext ctx, XResource x)
{
ctx.DrawEllipse(
new Ellipse(new Vector2(0, 0), CitySize, CitySize),
x.GetColor(Color.Blue),
2.0f);
for (var i = 0; i < Persons.Length; ++i)
{
Persons[i].Draw(ctx, x);
}
Hospital.Draw(ctx, x);
DrawStatus(ctx, x);
}
float tagDay = 0;
string notificationText = null, notificationTitle = null;
void DrawStatus(DeviceContext ctx, XResource x)
{
ctx.Transform = Matrix3x2.Identity;
int healthy = 0, infected = 0, illness = 0, inHospital = 0, cured = 0, dead = 0;
for (var i = 0; i < Persons.Length; ++i)
{
_ = Persons[i].Status switch
{
PersonStatus.Healthy => ++healthy,
PersonStatus.InfectedInShadow => ++infected,
PersonStatus.Illness => ++illness,
PersonStatus.InHospital => ++inHospital,
PersonStatus.Cured => ++cured,
PersonStatus.Dead => ++dead,
_ => throw new InvalidOperationException()
};
}
if (notificationText != null)
{
MessageBox.Show(notificationText, notificationTitle);
notificationText = null;
}
if (infected == 0 && illness == 0 && inHospital == 0 && tagDay == 0)
{
tagDay = day;
notificationText = $"你在第{day:F1}天击败了病毒!死亡人数:{dead}";
notificationTitle = "恭喜!";
}
else if (healthy <= (Population / 2) && tagDay == 0)
{
tagDay = day;
notificationText = $"第{day:F1}天,疫情控制失败!\n(超过一半的人被感染即视为失败)";
notificationTitle = "失败!你没能阻止病毒的肆虐。";
}
string wearMaskText = WearMask ? "✔" : "❌";
var texts = new[]
{
(text: $"第{day:F1}天 移动意愿:{MoveWilling:P0} 居民戴口罩:{wearMaskText}", color: Color.Black),
(text: $"健康人数:{healthy}", color: ColorFromStatus(PersonStatus.Healthy)),
(text: $"感染人数:{infected}", color: ColorFromStatus(PersonStatus.InfectedInShadow)),
(text: $"发病人数:{illness+inHospital}", color: ColorFromStatus(PersonStatus.Illness)),
(text: $"住院人数:{inHospital}/{HospitalBeds}", color: ColorFromStatus(PersonStatus.InHospital)),
(text: $"治愈人数:{cured}", color: ColorFromStatus(PersonStatus.Cured)),
(text: $"死亡人数:{dead}", color: ColorFromStatus(PersonStatus.Dead)),
};
for (var i = 0; i < texts.Length; ++i)
{
ctx.DrawText(texts[i].text, x.TextFormats[18], new RectangleF(5, i * 20, ctx.Size.Width, ctx.Size.Height),
x.GetColor(texts[i].color),
DrawTextOptions.EnableColorFont);
}
}
float dayAccumulate = 0;
public void Update(float dt)
{
// step move
for (var i = 0; i < Persons.Length; ++i)
{
Persons[i].MoveAroundInCity(dt, CitySize);
}
// step status
dayAccumulate += dt;
day += (dt / SecondsPerDay);
while (dayAccumulate >= SecondsPerDay)
{
StepDay();
dayAccumulate -= SecondsPerDay;
}
}
void StepDay()
{
Hospital.Heal(Persons, infectorIds);
for (var i = 0; i < Persons.Length; ++i)
{
Persons[i].Direction = random.NextDouble() < MoveWilling ?
random.NextFloat(0, MathF.PI * 2) : float.NaN;
// illness/inHospital -> dead
if ((Persons[i].Status == PersonStatus.Illness || Persons[i].Status == PersonStatus.InHospital)
&& random.NextDouble() < (DeathRate / 3))
{
infectorIds.Remove(i); Hospital.PersonIds.Remove(i);
if (Persons[i].Status == PersonStatus.InHospital)
{
Persons[i].Position = new Vector2(int.MaxValue, int.MaxValue);
}
Persons[i].Status = PersonStatus.Dead;
continue;
}
// illness -> inHospital
if (Persons[i].Status == PersonStatus.Illness)
{
--Persons[i].EstimateDays;
if (Persons[i].EstimateDays <= 0 && Hospital.HasBed)
{
Hospital.Accept(Persons, i);
}
continue;
}
// infected -> illness
if (Persons[i].Status == PersonStatus.InfectedInShadow)
{
--Persons[i].EstimateDays;
if (Persons[i].EstimateDays <= 0)
{
Persons[i].Status = PersonStatus.Illness;
Persons[i].EstimateDays = GenerateToHospitalDays();
}
continue;
}
}
// healthy -> infected
List<int> newlyInffectedIds = new List<int>();
newlyInffectedIds = healthyIds
.AsParallel()
.Where(x =>
{
foreach (var infectorId in infectorIds)
{
if (Vector2.DistanceSquared(Persons[x].Position, Persons[infectorId].Position) <= SafeDistance() * SafeDistance())
return true;
}
return false;
})
.ToList();
foreach (int personId in newlyInffectedIds)
{
Infect(personId);
}
}
}
class Hospital
{
public int Beds => HospitalBeds;
public SortedSet<int> PersonIds = new SortedSet<int>();
public bool HasBed => Beds > PersonIds.Count;
public void Heal(Person[] persons, SortedSet<int> infectorIds)
{
var curedIds = new List<int>();
int index = 0;
foreach (var i in PersonIds)
{
persons[i].Position = GetPosition(index++) + new Vector2(HospitalBedSize / 2 - PersonSize / 2, HospitalBedSize / 2 - PersonSize / 2);
persons[i].EstimateDays--;
if (persons[i].EstimateDays <= 0)
{
curedIds.Add(i);
infectorIds.Remove(i);
}
}
foreach (var id in curedIds)
{
persons[id].Status = PersonStatus.Cured;
persons[id].Position = new Vector2(0, 0);
PersonIds.Remove(id);
}
}
Vector2 GetPosition(int index)
{
int columnBeds = (int)(HospitalHeight / HospitalBedSize);
int column = index % columnBeds;
int row = index / columnBeds;
return new Vector2(HospitalX + row * HospitalBedSize, HospitalY + column * HospitalBedSize);
}
public void Accept(Person[] persons, int personId)
{
persons[personId].Status = PersonStatus.InHospital;
persons[personId].EstimateDays = GenerateCureDays();
PersonIds.Add(personId);
}
public void Draw(DeviceContext ctx, XResource x)
{
int rows = (int)MathF.Ceiling(Beds * HospitalBedSize / HospitalHeight);
float width = rows * HospitalBedSize;
for (var i = 0; i < Beds; ++i)
{
Vector2 topLeft = GetPosition(i);
ctx.DrawRectangle(new RectangleF(topLeft.X, topLeft.Y, HospitalBedSize, HospitalBedSize), x.GetColor(Color.Green));
}
ctx.DrawRectangle(new RectangleF(HospitalX, HospitalY, width, HospitalHeight), x.GetColor(HasBed ? Color.Black : Color.Red), 3.0f);
}
}
struct Person
{
public PersonStatus Status;
public Vector2 Position;
public float EstimateDays;
public float Direction;
public static Person Create(float citySize)
{
float phi = random.NextFloat(0, MathUtil.TwoPi);
float r = random.NextFloat(0, citySize);
var p = new Person { Status = PersonStatus.Healthy };
p.Position.X = MathF.Sin(phi) * r;
p.Position.Y = -MathF.Cos(phi) * r;
p.Direction = random.NextFloat(0, MathF.PI * 2);
return p;
}
public void Draw(DeviceContext ctx, XResource x)
{
ctx.FillRectangle(new RectangleF(Position.X, Position.Y, PersonSize, PersonSize), x.GetColor(ColorFromStatus(Status)));
}
public void MoveAroundInCity(float dt, float citySize)
{
if (Status == PersonStatus.InHospital ||
Status == PersonStatus.Dead ||
float.IsNaN(Direction)) return;
float duration = dt / SecondsPerDay;
float dx = MovingDistancePerDay * duration * MathF.Sin(Direction);
float dy = MovingDistancePerDay * duration * -MathF.Cos(Direction);
var newPosition = Position + new Vector2(dx, dy);
if (newPosition.LengthSquared() < (citySize * citySize))
{
Position = newPosition;
}
else
{
Direction = random.NextFloat(0, MathF.PI * 2);
}
}
}
enum PersonStatus
{
Healthy, // 健康
InfectedInShadow, // 被感染,处于潜伏期
Illness, // 发病
InHospital, // 发病并进入医院
Cured, // 治愈
Dead, //死亡
}
static Color ColorFromStatus(PersonStatus status) => status switch
{
PersonStatus.Healthy => Color.Green,
PersonStatus.InfectedInShadow => Color.Yellow,
PersonStatus.Illness => Color.OrangeRed,
PersonStatus.InHospital => Color.Red,
PersonStatus.Dead => Color.Black,
PersonStatus.Cured => Color.White,
_ => throw new InvalidOperationException(),
};