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
|
#include "app.hpp"
#include "compat/debug.hpp"
#include "compat/shared-ptr-wrapper.hpp"
#include "compat/function2.hpp"
#include "src/critter.hpp"
#include "src/world.hpp"
#include "src/wall-atlas.hpp"
#include "src/timer.hpp"
#include "src/log.hpp"
#include "src/point.inl"
#include "loader/loader.hpp"
#include <cinttypes>
#include <cstdio>
// todo! find all places where singed division is used
namespace floormat {
namespace {
using enum rotation;
using fu2::function_view;
using Function = function_view<Ns() const>;
#ifndef __CLION_IDE__
constexpr auto constantly(const auto& x) noexcept {
return [x]<typename... Ts> (const Ts&...) constexpr -> const auto& { return x; };
}
#else
constexpr auto constantly(Ns x) noexcept { return [x] { return x; }; }
#endif
critter_proto make_proto(float accel)
{
critter_proto proto;
proto.atlas = loader.anim_atlas("npc-walk", loader.ANIM_PATH);
proto.name = "Player"_s;
proto.speed = accel;
proto.playable = true;
proto.offset = {};
proto.bbox_offset = {};
proto.bbox_size = Vector2ub(tile_size_xy/2);
return proto;
}
void mark_all_modified(world& w)
{
for (auto& [coord, ch] : w.chunks())
ch.mark_modified();
}
struct Start
{
StringView name, instance;
point pt;
double accel = 1;
enum rotation rotation = N;
#if 1
bool quiet = is_log_quiet() || is_log_standard();
bool verbose = false;
#elif 1
bool verbose = true;
bool quiet = false;
#elif 0
bool quiet = is_log_quiet();
bool verbose = is_log_standard() || is_log_verbose();
#else
bool quiet = is_log_quiet() || is_log_standard();
bool verbose = is_log_verbose();
#endif
};
struct Expected
{
point pt;
Ns time;
};
struct Grace
{
Ns time = Ns{250};
uint32_t distance_L2 = 24;
bool no_crash = false;
};
bool run(world& w, const function_view<Ns() const>& make_dt,
Start start, Expected expected, Grace grace = {})
{
constexpr auto max_time = 120*Second;
constexpr uint32_t max_steps = 10'000;
fm_assert(grace.time != Ns{});
fm_assert(!start.quiet | !start.verbose);
//validate_start(start);
//validate_expected(expected);
//validate_grace(grace);
fm_assert(start.accel > 1e-8);
fm_assert(start.accel <= 50);
fm_assert(start.name);
fm_assert(start.instance);
fm_assert(start.rotation < rotation_COUNT);
expected.time.stamp = uint64_t(expected.time.stamp / start.accel);
fm_assert(expected.time <= max_time);
fm_assert(grace.distance_L2 <= (uint32_t)Vector2((iTILE_SIZE2 * TILE_MAX_DIM)).length());
mark_all_modified(w);
object_id id = 0;
auto npc_ = w.ensure_player_character(id, make_proto((float)start.accel)).ptr;
auto& npc = *npc_;
auto index = npc.index();
npc.teleport_to(index, start.pt, rotation_COUNT);
Ns time{0};
auto last_pos = npc.position();
uint32_t i;
if (!start.quiet) [[unlikely]]
Debug{} << "**" << start.name << start.instance << colon();
constexpr auto print_pos = [](StringView prefix, point start, point pos, Ns time, Ns dt) {
DBG_nospace << prefix
<< " " << pos
<< " time:" << time
<< " dt:" << dt
<< " dist:" << point::distance_l2(pos, start);
};
for (i = 0; true; i++)
{
const auto dt = Ns{make_dt()};
if (dt == Ns{}) [[unlikely]]
{
if (!start.quiet) [[unlikely]]
Debug{} << "| dt == 0, breaking";
break;
}
if (start.verbose) [[unlikely]]
print_pos(" ", expected.pt, npc.position(), time, dt);
fm_assert(dt >= Millisecond*1e-1);
fm_assert(dt <= Second * 1000);
npc.update_movement(index, dt, start.rotation);
const auto pos = npc.position();
const bool same_pos = pos == last_pos;
last_pos = pos;
time += dt;
if (same_pos) [[unlikely]]
{
if (!start.quiet) [[unlikely]]
{
print_pos("->", expected.pt, pos, time, dt);
DBG_nospace << "===>"
<< " iters,"
<< " time:" << time
<< " distance:" << point::distance_l2(last_pos, expected.pt) << " px"
<< Debug::newline;
}
if (i == 0) [[unlikely]] // todo! check for very small dt before dying
{
{ auto dbg = Error{standard_error(), Debug::Flag::NoSpace};
dbg << "!!! fatal: stopped after zero iterations";
dbg << " dt=" << dt << " accel=" << npc.speed;
}
fm_assert(false);
}
break;
}
if (time > max_time) [[unlikely]]
{
if (!start.quiet) [[unlikely]]
print_pos("*", start.pt, last_pos, time, dt);
Error{standard_error()} << "!!! fatal: timeout" << max_time << "reached!";
if (grace.no_crash)
return false;
else
fm_assert(false);
}
if (i > max_steps) [[unlikely]]
{
print_pos("*", start.pt, last_pos, time, dt);
Error{standard_error()} << "!!! fatal: position doesn't converge after" << i << "iterations!";
if (grace.no_crash)
return false;
else
fm_assert(false);
}
}
if (const auto dist_l2 = point::distance_l2(last_pos, expected.pt);
dist_l2 > grace.distance_L2) [[unlikely]]
{
Error{standard_error()} << "!!! fatal: distance" << dist_l2 << "pixels" << "over grace distance of" << grace.distance_L2;
if (grace.no_crash)
return false;
else
fm_assert(false);
}
else if (start.verbose) [[unlikely]]
Debug{} << "*" << "distance:" << dist_l2 << "pixels";
if (expected.time != Ns{})
{
const auto time_diff = Ns{Math::abs((int64_t)expected.time.stamp - (int64_t)time.stamp)};
if (time_diff > grace.time)
{
Error{ standard_error(), Debug::Flag::NoSpace }
<< "!!! fatal: wrong time " << time
<< " expected:" << expected.time
<< " diff:" << grace.time
<< " for " << start.name << "/" << start.instance;
if (grace.no_crash)
return false;
else
fm_assert(false);
}
}
return true;
}
void test1(StringView instance_name, const Function& make_dt, double accel)
{
const auto W = wall_image_proto{ loader.wall_atlas("empty"), 0 };
auto w = world();
w[{{0,0,0}, {8,9}}].t.wall_north() = W;
w[{{0,1,0}, {8,0}}].t.wall_north() = W;
bool ret = run(w, make_dt,
Start{
.name = "test1"_s,
.instance = instance_name,
.pt = {{0,0,0}, {8,15}, {-8, 8}},
.accel = accel,
.rotation = N,
},
Expected{
.pt = {{0,0,0}, {8, 9}, {-6,-15}}, // distance_L2 == 3
.time = 6950*Millisecond,
},
Grace{
.time = 300*Millisecond,
});
fm_assert(ret);
}
void test2(StringView instance_name, const Function& make_dt, double accel)
{
const auto W = wall_image_proto{ loader.wall_atlas("empty"), 0 };
auto w = world();
w[{{-1,-1,0}, {13,13}}].t.wall_north() = W;
w[{{-1,-1,0}, {13,13}}].t.wall_west() = W;
w[{{1,1,0}, {4,5}}].t.wall_north() = W;
w[{{1,1,0}, {5,4}}].t.wall_west() = W;
bool ret = run(w, make_dt,
Start{
.name = "test2"_s,
.instance = instance_name,
.pt = {{-1,-1,0}, {13,14}, {-15,-29}},
.accel = accel,
.rotation = SE,
},
Expected{
.pt = {{1,1,0}, {4, 4}, {8,8}},
.time = 35'000*Millisecond,
},
Grace{
.time = 250*Millisecond,
.distance_L2 = 8,
});
fm_assert(ret);
}
} // namespace
void test_app::test_critter()
{
// todo! add ANSI sequence to stdout to goto start of line and clear to eol
// \r
// <ESC>[2K
// \n
const bool is_noisy = !Start{}.quiet;
if (is_noisy)
DBG_nospace << "";
test1("dt=16.667 accel=1", constantly(Millisecond * 16.667), 1);
test1("dt=16.667 accel=2", constantly(Millisecond * 16.667), 2);
test1("dt=16.667 accel=5", constantly(Millisecond * 16.667), 5);
test1("dt=33.337 accel=1", constantly(Millisecond * 33.337), 1);
test1("dt=33.337 accel=2", constantly(Millisecond * 33.337), 2);
test1("dt=33.337 accel=5", constantly(Millisecond * 33.337), 5);
test1("dt=16.667 accel=1", constantly(Millisecond * 50.000), 1);
test1("dt=16.667 accel=2", constantly(Millisecond * 50.000), 2);
test1("dt=16.667 accel=5", constantly(Millisecond * 50.000), 5);
test1("dt=200 accel=1", constantly(Millisecond * 200.0 ), 1);
test1("dt=100 accel=2", constantly(Millisecond * 100.0 ), 2);
// test1("dt=16.667 accel=0.5", constantly(Millisecond * 16.667),0.5); // todo! fix this!
test1("dt=100 accel=0.5", constantly(Millisecond * 100.0 ), 0.5);
//test1("dt=16.667 ms accel=1", constantly(Millisecond * 16.667), 1); // todo! fix this!
test2("dt=33.334 accel=1", constantly(Millisecond * 33.334), 1);
test2("dt=33.334 accel=2", constantly(Millisecond * 33.334), 2);
test2("dt=33.334 accel=5", constantly(Millisecond * 33.334), 5);
test2("dt=33.334 accel=10", constantly(Millisecond * 33.334), 10);
test2("dt=50.000 accel=1", constantly(Millisecond * 50.000), 1);
test2("dt=50.000 accel=2", constantly(Millisecond * 50.000), 2);
test2("dt=100.00 accel=1", constantly(Millisecond * 100.00), 1);
test2("dt=100.00 accel=2", constantly(Millisecond * 100.00), 2);
test2("dt=100.00 accel=0.5", constantly(Millisecond * 100.00), 0.5);
if (is_noisy)
{
std::fputc('\t', stdout);
std::fflush(stdout);
}
}
} // namespace floormat
|