File: sun_test.cpp

package info (click to toggle)
cataclysm-dda 0.H-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 710,808 kB
  • sloc: cpp: 524,019; python: 11,580; sh: 1,228; makefile: 1,169; xml: 507; javascript: 150; sql: 56; exp: 41; perl: 37
file content (720 lines) | stat: -rw-r--r-- 33,370 bytes parent folder | download
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
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
#include "cata_catch.h"
#include "calendar.h" // IWYU pragma: associated

#include <iomanip>
#include <optional>
#include <string>
#include <unordered_set>

#include "cata_scope_helpers.h"
#include "hash_utils.h"
#include "line.h"
#include "options_helpers.h"
#include "output.h"
#include "stringmaker.h"
#include "units_utility.h"

// SUN TESTS

// The 24-hour solar cycle has four overlapping parts, as defined by four calendar.cpp functions:
//
// is_night : While the Sun is below -6° altitude
// is_day   : While the Sun is above -1° altitude
// is_dawn, is_dusk : While the Sun is in between -6° to -1° at the appropriate end
//                    of the day
//
//
// The times of sunrise and sunset will naturally depend on the current time of year; this aspect is
// covered by the "sunrise and sunset" and solstice/equinox tests later in this file. Here we simply
// use the first day of spring as a baseline.
TEST_CASE( "daily_solar_cycle", "[sun][night][dawn][day][dusk]" )
{
    // Use sunrise/sunset on the first day (spring equinox)
    const time_point midnight = calendar::turn_zero;
    const time_point noon = calendar::turn_zero + 12_hours;
    const time_point today_sunrise = sunrise( midnight );
    const time_point today_sunset = sunset( midnight );

    CAPTURE( to_string( today_sunrise ) );
    CAPTURE( to_string( today_sunset ) );

    SECTION( "Night" ) {
        // First, at the risk of stating the obvious
        CHECK( is_night( midnight + 1_seconds ) );
        CHECK( is_night( midnight + 2_hours ) );
        CHECK( is_night( midnight + 3_hours ) );
        CHECK( is_night( midnight + 4_hours ) );

        // Yep, still dark
        CHECK( is_night( midnight + 1_seconds ) );
        CHECK( is_night( midnight + 2_hours ) );
        CHECK( is_night( midnight + 3_hours ) );
        CHECK( is_night( midnight + 4_hours ) );
    }

    SECTION( "Dawn" ) {
        CHECK_FALSE( is_night( today_sunrise ) );
        CHECK( is_dawn( today_sunrise - 1_seconds ) );
        CHECK( is_dawn( today_sunrise - 20_minutes ) );

        // Dawn stops at 1 degrees
        CHECK_FALSE( is_dawn( today_sunrise + 7_minutes ) );
    }
    SECTION( "Day" ) {
        // Due to roundings the day may start few seconds later than expected
        CHECK( is_day( today_sunrise + 2_seconds ) );
        CHECK( is_day( today_sunrise + 2_hours ) );
        // Second breakfast
        CHECK( is_day( today_sunrise + 3_hours ) );
        CHECK( is_day( today_sunrise + 4_hours ) );
        // Luncheon
        CHECK( is_day( noon - 3_hours ) );
        CHECK( is_day( noon - 2_hours ) );
        // Elevenses
        CHECK( is_day( noon - 1_hours ) );
        // Noon
        CHECK( is_day( noon ) );
        CHECK_FALSE( is_dawn( noon ) );
        CHECK_FALSE( is_dusk( noon ) );
        CHECK_FALSE( is_night( noon ) );
        // Afternoon tea
        CHECK( is_day( noon + 1_hours ) );
        CHECK( is_day( noon + 2_hours ) );
        // Dinner
        CHECK( is_day( noon + 3_hours ) );
        CHECK( is_day( today_sunset - 2_hours ) );
        // Supper
        CHECK( is_day( today_sunset - 1_hours ) );
        CHECK( is_day( today_sunset - 1_seconds ) );
    }

    SECTION( "Dusk" ) {
        CHECK_FALSE( is_day( today_sunset + 1_seconds ) );
        CHECK( is_dusk( today_sunset + 1_seconds ) );
        CHECK( is_dusk( today_sunset + 20_minutes ) );
    }

    SECTION( "Night again" ) {
        CHECK( is_night( today_sunset + 2_hours ) );
        CHECK( is_night( today_sunset + 3_hours ) );
        CHECK( is_night( today_sunset + 4_hours ) );
    }

    SECTION( "Eternal night" ) {
        //Eternal night
        calendar::set_eternal_night( true );
        CHECK( is_night( midnight + 1_seconds ) );
        CHECK( is_night( today_sunrise + 1_seconds ) );
        CHECK( is_night( noon + 1_seconds ) );
        CHECK( is_night( today_sunset + 1_seconds ) );
        CHECK_FALSE( is_dawn( today_sunrise + 1_seconds ) );
        CHECK_FALSE( is_day( noon + 1_seconds ) );
        CHECK_FALSE( is_dusk( today_sunset + 1_seconds ) );
        calendar::set_eternal_night( false );
    }

    SECTION( "Eternal night" ) {
        //Eternal day
        calendar::set_eternal_day( true );
        CHECK( is_day( midnight + 1_seconds ) );
        CHECK( is_day( today_sunrise + 1_seconds ) );
        CHECK( is_day( noon + 1_seconds ) );
        CHECK( is_day( today_sunset + 1_seconds ) );
        CHECK_FALSE( is_dawn( today_sunrise + 1_seconds ) );
        CHECK_FALSE( is_night( midnight + 1_seconds ) );
        CHECK_FALSE( is_dusk( today_sunset + 1_seconds ) );
        calendar::set_eternal_day( false );
    }
}

// The calendar `sunlight` function returns light level for both sun and moon.
TEST_CASE( "sunlight_and_moonlight", "[sun][sunlight][moonlight]" )
{
    // Use sunrise/sunset on the first day (spring equinox)
    const time_point midnight = calendar::turn_zero;
    const time_point noon = calendar::turn_zero + 12_hours;
    const time_point today_sunrise = sunrise( midnight );
    const time_point today_sunset = sunset( midnight );
    CHECK( today_sunset > today_sunrise );
    CHECK( today_sunrise > midnight );

    // Expected numbers below assume 110.0f maximum daylight level
    // (maximum daylight is different at other times of year - see [daylight] tests)
    REQUIRE( 100.0f == default_daylight_level() );

    SECTION( "sunlight" ) {
        // Before dawn
        CHECK( 1.0f == sun_moon_light_at( midnight ) );
        // Dawn
        CHECK( sun_moon_light_at( today_sunrise - 2_hours ) == 1.0f );
        CHECK( sun_moon_light_at( today_sunrise - 1_hours ) == Approx( 5 ).margin( 2 ) );
        CHECK( sun_moon_light_at( today_sunrise ) == Approx( 60 ).margin( 1 ) );
        // Light gets brighter towards noon
        CHECK( sun_moon_light_at( today_sunrise + 2_hours ) >
               sun_moon_light_at( today_sunrise + 1_hours ) );
        CHECK( sun_moon_light_at( today_sunrise + 3_hours ) >
               sun_moon_light_at( today_sunrise + 2_hours ) );
        // Noon
        CHECK( sun_moon_light_at( noon ) == Approx( 110 ).margin( 10 ) );
        CHECK( sun_moon_light_at( noon + 1_hours ) <
               sun_moon_light_at( noon ) );
        CHECK( sun_moon_light_at( noon + 2_hours ) <
               sun_moon_light_at( noon + 1_hours ) );
        // Dusk begins
        CHECK( sun_moon_light_at( today_sunset - 1_hours ) ==
               Approx( sun_moon_light_at( today_sunrise + 1_hours ) ).margin( 1 ) );
        CHECK( sun_moon_light_at( today_sunset ) ==
               Approx( sun_moon_light_at( today_sunrise ) ).margin( 1 ) );
        CHECK( sun_moon_light_at( today_sunset + 1_hours ) ==
               Approx( sun_moon_light_at( today_sunrise - 1_hours ) ).margin( 1 ) );

        // Eternal night
        calendar::set_eternal_night( true );
        CHECK( sun_moon_light_at( midnight ) == 1.0f );
        CHECK( sun_moon_light_at( today_sunset ) == 1.0f );
        CHECK( sun_moon_light_at( today_sunrise ) == 1.0f );
        CHECK( sun_moon_light_at( noon ) == 1.0f );
        calendar::set_eternal_night( false );
        // Eternal day
        calendar::set_eternal_day( true );
        CHECK( sun_moon_light_at( midnight ) == Approx( 126 ).margin( 5 ) );
        CHECK( sun_moon_light_at( today_sunset ) == Approx( 126 ).margin( 5 ) );
        CHECK( sun_moon_light_at( today_sunrise ) == Approx( 126 ).margin( 5 ) );
        CHECK( sun_moon_light_at( noon ) == Approx( 126 ).margin( 5 ) );
        calendar::set_eternal_day( false );
    }

    // This moonlight test is intentionally simple, only checking new moon (minimal light) and full
    // moon (maximum moonlight). More detailed tests of moon phase and light should be expressed in
    // `moon_test.cpp`. Including here simply to check that `sun_moon_light_at` also calculates
    // moonlight.
    SECTION( "moonlight" ) {
        const time_duration phase_time = calendar::season_length() / 6;
        const time_point new_moon = calendar::turn_zero;
        const time_point full_moon = new_moon + phase_time;
        const time_point full_moon_midnight = full_moon - time_past_midnight( full_moon );

        WHEN( "the moon is new" ) {
            REQUIRE( get_moon_phase( new_moon ) == MOON_NEW );
            THEN( "moonlight is 1.0" ) {
                CHECK( 1.0f == sun_moon_light_at( new_moon ) );
            }
        }

        WHEN( "the moon is full" ) {
            REQUIRE( get_moon_phase( full_moon_midnight ) == MOON_FULL );
            THEN( "moonlight is 7.0" ) {
                CHECK( 7.0f == sun_moon_light_at( full_moon_midnight ) );
            }
        }
    }
}

// sanity-check seasonally-adjusted maximum daylight level
TEST_CASE( "noon_sunlight_levels", "[sun][daylight][equinox][solstice]" )
{
    const time_duration one_season = calendar::season_length();
    const time_point spring = calendar::turn_zero;
    const time_point summer = spring + one_season;
    const time_point autumn = summer + one_season;
    const time_point winter = autumn + one_season;

    SECTION( "baseline 110 daylight on the spring and autumn equinoxes" ) {
        float spring_light = sun_light_at( spring + 12_hours );
        CHECK( spring_light == Approx( 110.0f ).margin( 10 ) );
        CHECK( sun_light_at( autumn + 12_hours ) == Approx( spring_light ).margin( 1 ) );
    }

    SECTION( "125 daylight on the summer solstice" ) {
        CHECK( sun_light_at( summer + 12_hours ) == 125.0f );
    }

    SECTION( "90 daylight on the winter solstice" ) {
        CHECK( sun_light_at( winter + 12_hours ) == Approx( 87.0f ).margin( 10 ) );
    }

    // Many other times of day have peak daylight level, but noon is for sure
    SECTION( "noon is peak daylight level" ) {
        CHECK( sun_moon_light_at( spring + 12_hours ) ==
               Approx( sun_moon_light_at_noon_near( spring ) ).margin( 3 ) );
        CHECK( sun_moon_light_at( summer + 12_hours ) ==
               Approx( sun_moon_light_at_noon_near( summer ) ).margin( 3 ) );
        CHECK( sun_moon_light_at( autumn + 12_hours ) ==
               Approx( sun_moon_light_at_noon_near( autumn ) ).margin( 3 ) );
        CHECK( sun_moon_light_at( winter + 12_hours ) ==
               Approx( sun_moon_light_at_noon_near( winter ) ).margin( 3 ) );
    }

    SECTION( "Eternal day" ) {
        // locked sunlight when eternal day id due
        calendar::set_eternal_day( true );
        CHECK( sun_light_at( spring + 12_hours ) == 125.0f );
        CHECK( sun_light_at( summer + 12_hours ) == 125.0f );
        CHECK( sun_light_at( autumn + 12_hours ) == 125.0f );
        CHECK( sun_light_at( winter + 12_hours ) == 125.0f );
        calendar::set_eternal_day( false );
    }
}

// The times of sunrise and sunset vary throughout the year. Equinoxes occur on the
// first day of spring and autumn, and solstices occur on the first day of summer and winter.
TEST_CASE( "sunrise_and_sunset", "[sun][sunrise][sunset][equinox][solstice]" )
{
    // Due to the "NN_days" math below, this test requires a default 91-day season length
    REQUIRE( calendar::season_from_default_ratio() == Approx( 1.0f ) );

    const time_duration one_season = calendar::season_length();
    const time_point spring = calendar::turn_zero;
    const time_point summer = spring + one_season;
    const time_point autumn = summer + one_season;
    const time_point winter = autumn + one_season;

    auto const sunrise_in_day = []( const time_point & t ) {
        return time_past_midnight( sunrise( t ) );
    };

    auto const sunset_in_day = []( const time_point & t ) {
        return time_past_midnight( sunset( t ) );
    };

    // The expected sunrise/sunset times depend on calculations in `calendar.cpp`
    // They should approximately match Boston in the year 2000.  There is minor
    // variation due to our year length being 364 days and some other minor
    // simplifications in the formulae.

    SECTION( "spring equinox is day 1 of spring" ) {
        // Actual sunrise and sunset on March 21st 2001 are 0545 and 1757
        CHECK( "Year 1, Spring, day 1 5:57:58 AM" == to_string( sunrise( spring ) ) );
        CHECK( "Year 1, Spring, day 1 6:16:07 PM" == to_string( sunset( spring ) ) );
    }

    SECTION( "summer solstice is day 1 of summer" ) {
        // Actual sunrise and sunset on June 21st 2001 are 0407 and 1924
        CHECK( "Year 1, Summer, day 1 4:22:20 AM" == to_string( sunrise( summer ) ) );
        CHECK( "Year 1, Summer, day 1 7:41:38 PM" == to_string( sunset( summer ) ) );
    }

    SECTION( "autumn equinox is day 1 of autumn" ) {
        // Actual sunrise and sunset on September 22nd 2001 are 0531 and 1741
        CHECK( "Year 1, Autumn, day 1 5:45:33 AM" == to_string( sunrise( autumn ) ) );
        CHECK( "Year 1, Autumn, day 1 5:59:37 PM" == to_string( sunset( autumn ) ) );
    }

    SECTION( "winter solstice is day 1 of winter" ) {
        // Actual sunrise and sunset on December 21st 2001 are 0710 and 1614
        CHECK( "Year 1, Winter, day 1 7:25:07 AM" == to_string( sunrise( winter ) ) );
        CHECK( "Year 1, Winter, day 1 4:31:46 PM" == to_string( sunset( winter ) ) );
    }

    SECTION( "spring sunrise gets earlier" ) {
        CHECK( sunrise_in_day( spring + 30_days ) < sunrise_in_day( spring ) );
        CHECK( sunrise_in_day( spring + 60_days ) < sunrise_in_day( spring + 30_days ) );
        CHECK( sunrise_in_day( spring + 90_days ) < sunrise_in_day( spring + 60_days ) );
    }
    SECTION( "spring sunset gets later" ) {
        CHECK( sunset_in_day( spring + 30_days ) > sunset_in_day( spring ) );
        CHECK( sunset_in_day( spring + 60_days ) > sunset_in_day( spring + 30_days ) );
        CHECK( sunset_in_day( spring + 90_days ) > sunset_in_day( spring + 60_days ) );
    }

    SECTION( "summer sunrise gets later" ) {
        CHECK( sunrise_in_day( summer + 30_days ) > sunrise_in_day( summer ) );
        CHECK( sunrise_in_day( summer + 60_days ) > sunrise_in_day( summer + 30_days ) );
        CHECK( sunrise_in_day( summer + 90_days ) > sunrise_in_day( summer + 60_days ) );
    }
    SECTION( "summer sunset gets earlier" ) {
        CHECK( sunset_in_day( summer + 30_days ) < sunset_in_day( summer ) );
        CHECK( sunset_in_day( summer + 60_days ) < sunset_in_day( summer + 30_days ) );
        CHECK( sunset_in_day( summer + 90_days ) < sunset_in_day( summer + 60_days ) );
    }

    SECTION( "autumn sunrise gets later" ) {
        CHECK( sunrise_in_day( autumn + 30_days ) > sunrise_in_day( autumn ) );
        CHECK( sunrise_in_day( autumn + 60_days ) > sunrise_in_day( autumn + 30_days ) );
        CHECK( sunrise_in_day( autumn + 90_days ) > sunrise_in_day( autumn + 60_days ) );
    }
    SECTION( "autumn sunset gets earlier" ) {
        CHECK( sunset_in_day( autumn + 30_days ) < sunset_in_day( autumn ) );
        CHECK( sunset_in_day( autumn + 60_days ) < sunset_in_day( autumn + 30_days ) );
        CHECK( sunset_in_day( autumn + 90_days ) < sunset_in_day( autumn + 60_days ) );
    }

    SECTION( "winter sunrise gets earlier" ) {
        CHECK( sunrise_in_day( winter + 30_days ) < sunrise_in_day( winter ) );
        CHECK( sunrise_in_day( winter + 60_days ) < sunrise_in_day( winter + 30_days ) );
        CHECK( sunrise_in_day( winter + 90_days ) < sunrise_in_day( winter + 60_days ) );
    }
    SECTION( "winter sunset gets later" ) {
        CHECK( sunset_in_day( winter + 30_days ) > sunset_in_day( winter ) );
        CHECK( sunset_in_day( winter + 60_days ) > sunset_in_day( winter + 30_days ) );
        CHECK( sunset_in_day( winter + 90_days ) > sunset_in_day( winter + 60_days ) );
    }
}

static rl_vec2d checked_sunlight_angle( const time_point &t )
{
    const std::optional<rl_vec2d> opt_angle = sunlight_angle( t );
    REQUIRE( opt_angle );
    return *opt_angle;
}

static constexpr time_point first_midnight = calendar::turn_zero;
static constexpr time_point first_noon = first_midnight + 12_hours;

TEST_CASE( "sun_highest_at_noon", "[sun]" )
{
    for( int i = 0; i < 100; ++i ) {
        CAPTURE( i );

        const time_point midnight = first_midnight + i * 1_days;
        CHECK_FALSE( sunlight_angle( midnight ) );

        const time_point noon = first_noon + i * 1_days;
        const time_point before_noon = noon - 2_hours;
        const time_point after_noon = noon + 2_hours;

        const rl_vec2d before_noon_angle = checked_sunlight_angle( before_noon );
        const rl_vec2d noon_angle = checked_sunlight_angle( noon );
        const rl_vec2d after_noon_angle = checked_sunlight_angle( after_noon );

        CAPTURE( before_noon_angle );
        CAPTURE( noon_angle );
        CAPTURE( after_noon_angle );
        // Sun should be highest around noon
        CHECK( noon_angle.magnitude() < before_noon_angle.magnitude() );
        CHECK( noon_angle.magnitude() < after_noon_angle.magnitude() );

        // Sun should always be in the South, meaning angle points North
        // (negative)
        CHECK( before_noon_angle.y < 0 );
        CHECK( noon_angle.y < 0 );
        CHECK( after_noon_angle.y < 0 );

        // Sun should be moving westwards across the sky, so its angle points
        // more eastwards, which means it's increasing
        CHECK( noon_angle.x > before_noon_angle.x );
        CHECK( after_noon_angle.x > noon_angle.x );

        CHECK( before_noon_angle.magnitude() ==
               Approx( after_noon_angle.magnitude() ).epsilon( 0.25 ) );
    }
}

TEST_CASE( "noon_sun_does_not_move_much", "[sun]" )
{
    rl_vec2d noon_angle = checked_sunlight_angle( first_noon );
    for( int i = 1; i < 1000; ++i ) {
        CAPTURE( i );
        const time_point later_noon = first_noon + i * 1_days;
        const rl_vec2d later_noon_angle = checked_sunlight_angle( later_noon );
        CHECK( noon_angle.x == Approx( later_noon_angle.x ).margin( 0.01 ) );
        CHECK( noon_angle.y == Approx( later_noon_angle.y ).epsilon( 0.05 ) );
        noon_angle = later_noon_angle;
    }
}

TEST_CASE( "dawn_dusk_fixed_during_eternal_season", "[sun]" )
{
    on_out_of_scope restore_eternal_season( []() {
        calendar::set_eternal_season( false );
    } );
    calendar::set_eternal_season( true );
    override_option override_eternal_season( "ETERNAL_SEASON", "true" );

    const time_point first_sunrise = sunrise( first_noon );
    const time_point first_sunset = sunset( first_noon );

    for( int i = 1; i < 1000; ++i ) {
        CAPTURE( i );
        const time_point this_noon = first_noon + i * 1_days;
        const time_point this_sunrise = sunrise( this_noon );
        const time_point this_sunset = sunset( this_noon );

        CHECK( this_sunrise < this_noon );
        CHECK( this_sunset > this_noon );

        CHECK( time_past_midnight( this_sunrise ) == time_past_midnight( first_sunrise ) );
        CHECK( time_past_midnight( this_sunset ) == time_past_midnight( first_sunset ) );
    }
}

TEST_CASE( "sun_altitude_fixed_during_eternal_night_or_day", "[sun]" )
{
    const time_point midnight = calendar::turn_zero;
    const time_point noon = calendar::turn_zero + 12_hours;
    const time_point today_sunrise = sunrise( midnight );
    const time_point today_sunset = sunset( midnight );

    // Eternal night
    calendar::set_eternal_night( true );
    CHECK( sun_azimuth_altitude( midnight ).second < -10_degrees );
    CHECK( sun_azimuth_altitude( today_sunset ).second < -10_degrees );
    CHECK( sun_azimuth_altitude( today_sunrise ).second < -10_degrees );
    CHECK( sun_azimuth_altitude( noon ).second < -10_degrees );
    calendar::set_eternal_night( false );

    // Eternal day
    calendar::set_eternal_day( true );
    CHECK( sun_azimuth_altitude( midnight ).second == 90_degrees );
    CHECK( sun_azimuth_altitude( today_sunset ).second == 90_degrees );
    CHECK( sun_azimuth_altitude( today_sunrise ).second == 90_degrees );
    CHECK( sun_azimuth_altitude( noon ).second == 90_degrees );
    calendar::set_eternal_day( false );
}

TEST_CASE( "sunrise_sunset_consistency", "[sun]" )
{
    bool set_eternal = GENERATE( false, true );
    on_out_of_scope restore_eternal_season( []() {
        calendar::set_eternal_season( false );
    } );
    calendar::set_eternal_season( set_eternal );

    for( int i = 1; i < 1000; ++i ) {
        CAPTURE( i );
        const time_point this_noon = first_noon + i * 1_days;
        {
            const time_point this_sunrise = sunrise( this_noon );
            CHECK( this_sunrise < this_noon );
            units::angle azimuth;
            units::angle altitude;
            std::tie( azimuth, altitude ) =
                sun_azimuth_altitude( this_sunrise );
            CHECK( to_degrees( altitude ) == Approx( -1 ).margin( 0.01 ) );
        }
        {
            const time_point this_sunset = sunset( this_noon );
            CHECK( this_sunset > this_noon );
            units::angle azimuth;
            units::angle altitude;
            std::tie( azimuth, altitude ) =
                sun_azimuth_altitude( this_sunset );
            CHECK( to_degrees( altitude ) == Approx( -1 ).margin( 0.01 ) );
        }
        {
            const time_point this_daylight = daylight_time( this_noon );
            CHECK( this_daylight < this_noon );
            units::angle azimuth;
            units::angle altitude;
            std::tie( azimuth, altitude ) =
                sun_azimuth_altitude( this_daylight );
            CHECK( to_degrees( altitude ) == Approx( -6 ).margin( 0.01 ) );
        }
    }
}

using PointSet = std::unordered_set<std::pair<int, int>, cata::tuple_hash>;

static PointSet sun_positions_regular( time_point start, time_point end, time_duration interval,
                                       int azimuth_scale )
{
    CAPTURE( to_days<int>( start - calendar::turn_zero ) );
    std::unordered_set<std::pair<int, int>, cata::tuple_hash> plot_points;

    for( time_point t = start; t < end; t += interval ) {
        CAPTURE( to_minutes<int>( t - start ) );
        units::angle azimuth;
        units::angle altitude;
        std::tie( azimuth, altitude ) = sun_azimuth_altitude( t );
        if( altitude < 0_degrees ) {
            continue;
        }
        // Convert to ASCII-art plot
        // x-axis is azimuth, 4/azimuth_scale degrees per column
        // y-axis is altitude, 3 degrees per column
        azimuth = normalize( azimuth + 180_degrees );
        // Scale azimuth away from 180 by specified scale
        azimuth = 180_degrees + ( azimuth - 180_degrees ) * azimuth_scale;
        REQUIRE( azimuth >= 0_degrees );
        REQUIRE( azimuth <= 360_degrees );
        REQUIRE( altitude >= 0_degrees );
        REQUIRE( altitude <= 90_degrees );
        plot_points.emplace( static_cast<int>( azimuth / 4_degrees ),
                             static_cast<int>( altitude / 3_degrees ) );
    }

    return plot_points;
}

static PointSet sun_throughout_day( time_point day_start )
{
    REQUIRE( time_past_midnight( day_start ) == 0_seconds );
    // Calculate the Sun's position every few minutes thourhgout the day
    time_point day_end = day_start + 1_days;
    return sun_positions_regular( day_start, day_end, 5_minutes, 1 );
}

static PointSet sun_throughout_year( time_point day_start )
{
    REQUIRE( time_past_midnight( day_start ) == 0_seconds );
    // Calculate the Sun's position every noon throughout the year
    time_point first_noon = day_start + 1_days / 2;
    time_point last_noon = first_noon + calendar::year_length();
    return sun_positions_regular( first_noon, last_noon, 1_days, 4 );
}

static void check_sun_plot( const std::vector<PointSet> &points, const std::string &reference )
{
    static constexpr std::array<char, 3> symbols = { { '#', '*', '-' } };
    REQUIRE( points.size() <= symbols.size() );

    std::ostringstream os;
    os << "Altitude\n";

    for( int rough_altitude = 30; rough_altitude >= 0; --rough_altitude ) {
        for( int rough_azimuth = 0; rough_azimuth <= 90; ++rough_azimuth ) {
            std::pair<int, int> p{ rough_azimuth, rough_altitude };
            char c = ' ';
            for( size_t i = 0; i < points.size(); ++i ) {
                if( points[i].count( p ) ) {
                    c = symbols[i];
                    break;
                }
            }
            os << c;
        }
        os << '\n';
    }
    os << std::setw( 92 ) << "Azimuth\n";
    std::string result = os.str();
    CHECK( result == reference );
    // When the test fails, print out something to copy-paste as a new
    // reference output:
    if( result != reference ) {
        result.pop_back();
        for( const std::string &line : string_split( result, '\n' ) ) {
            printf( R"("%s\n")" "\n", line.c_str() );
        }
    }
}

TEST_CASE( "movement_of_sun_through_day", "[sun]" )
{
    PointSet equinox_points = sun_throughout_day( calendar::turn_zero );
    PointSet summer_points =
        sun_throughout_day( calendar::turn_zero + calendar::season_length() );
    PointSet winter_points =
        sun_throughout_day( calendar::turn_zero + calendar::season_length() * 3 );
    std::string reference =
// *INDENT-OFF*
"Altitude\n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                     ################                                      \n"
"                                  ####              ####                                   \n"
"                               ####                     ##                                 \n"
"                              ##                          ##                               \n"
"                            ##                             ###                             \n"
"                           ##                                ##                            \n"
"                          ##                                  ##                           \n"
"                         ##               *******              ##                          \n"
"                        ##            ****      *****           ##                         \n"
"                        #          ***              ***          ##                        \n"
"                       #         ***                  ***         #                        \n"
"                      ##        **                      **        ##                       \n"
"                     ##        **                        **        ##                      \n"
"                     #        **                           *        #                      \n"
"                    ##       *                              *       ##                     \n"
"                   ##       *              ----              *       #                     \n"
"                   #       **          -----  -----           *       #                    \n"
"                  ##      **         ---          ---          *      ##                   \n"
"                 ##      **        ---              --         **      ##                  \n"
"                 #       *        --                  --        **      #                  \n"
"                ##      *        --                    --        *      ##                 \n"
"                #      **       --                      --       **      #                 \n"
"               #      **       --                        --       **      #                \n"
"              ##      *       --                          --       *       #               \n"
"                                                                                    Azimuth\n";
// *INDENT-ON*
    check_sun_plot( { summer_points, equinox_points, winter_points }, reference );
}

TEST_CASE( "movement_of_noon_through_year", "[sun]" )
{
    PointSet points = sun_throughout_year( calendar::turn_zero );
    std::string reference =
// *INDENT-OFF*
// This should yield an analemma
"Altitude\n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                         ######                                            \n"
"                                        ##    ##                                           \n"
"                                         ##   ##                                           \n"
"                                          ## ##                                            \n"
"                                           ###                                             \n"
"                                           ###                                             \n"
"                                           # ##                                            \n"
"                                          #   ##                                           \n"
"                                         ##    ##                                          \n"
"                                         #      ##                                         \n"
"                                        ##       #                                         \n"
"                                        #        #                                         \n"
"                                        #        #                                         \n"
"                                        ##       #                                         \n"
"                                         ##     ##                                         \n"
"                                          #######                                          \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                           \n"
"                                                                                    Azimuth\n";
// *INDENT-ON*
    check_sun_plot( { points }, reference );
}

TEST_CASE( "noon_rises_towards_summer_solsitice_and_falls_towards_winter", "[sun]" )
{
    int old_season_length = to_days<int>( calendar::season_length() );
    on_out_of_scope restore_season_length( [ = ]() {
        calendar::set_season_length( old_season_length );
    } );
    int this_season_length = GENERATE( 5, 14, 91, 1000 );
    CAPTURE( this_season_length );
    calendar::set_season_length( this_season_length );

    const time_point summer_solstice = calendar::turn_zero + calendar::season_length();
    const time_point winter_solstice = calendar::turn_zero + 3 * calendar::season_length();

    // Make some allowance and don't check the days within this range of the
    // solstice.
    const time_duration allowance = calendar::season_length() / 100;

    rl_vec2d last_noon_angle;

    for( time_point noon = first_noon; noon < winter_solstice; noon += 1_days ) {
        CAPTURE( to_days<int>( noon - first_noon ) );

        const rl_vec2d noon_angle = checked_sunlight_angle( noon );

        if( last_noon_angle.magnitude() != 0 ) {
            CAPTURE( last_noon_angle );
            CAPTURE( noon_angle );

            if( noon < summer_solstice - allowance ) {
                // Sun should be higher than yesterday until summer solstice
                CHECK( noon_angle.magnitude() < last_noon_angle.magnitude() );
            } else if( noon >= summer_solstice + allowance &&
                       noon <= winter_solstice - allowance ) {
                // ...and then lower than yesterday until winter solstice
                CHECK( noon_angle.magnitude() > last_noon_angle.magnitude() );
            }
        }

        last_noon_angle = noon_angle;
    }
}