-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathwordle.html
4447 lines (3801 loc) · 293 KB
/
wordle.html
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
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<!-- 2022-06-27 Mon 01:02 -->
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wordle</title>
<meta name="author" content="Jb Doyon" />
<meta name="generator" content="Org Mode" />
<style>
#content { max-width: 60em; margin: auto; }
.title { text-align: center;
margin-bottom: .2em; }
.subtitle { text-align: center;
font-size: medium;
font-weight: bold;
margin-top:0; }
.todo { font-family: monospace; color: red; }
.done { font-family: monospace; color: green; }
.priority { font-family: monospace; color: orange; }
.tag { background-color: #eee; font-family: monospace;
padding: 2px; font-size: 80%; font-weight: normal; }
.timestamp { color: #bebebe; }
.timestamp-kwd { color: #5f9ea0; }
.org-right { margin-left: auto; margin-right: 0px; text-align: right; }
.org-left { margin-left: 0px; margin-right: auto; text-align: left; }
.org-center { margin-left: auto; margin-right: auto; text-align: center; }
.underline { text-decoration: underline; }
#postamble p, #preamble p { font-size: 90%; margin: .2em; }
p.verse { margin-left: 3%; }
pre {
border: 1px solid #e6e6e6;
border-radius: 3px;
background-color: #f2f2f2;
padding: 8pt;
font-family: monospace;
overflow: auto;
margin: 1.2em;
}
pre.src {
position: relative;
overflow: auto;
}
pre.src:before {
display: none;
position: absolute;
top: -8px;
right: 12px;
padding: 3px;
color: #555;
background-color: #f2f2f299;
}
pre.src:hover:before { display: inline; margin-top: 14px;}
/* Languages per Org manual */
pre.src-asymptote:before { content: 'Asymptote'; }
pre.src-awk:before { content: 'Awk'; }
pre.src-authinfo::before { content: 'Authinfo'; }
pre.src-C:before { content: 'C'; }
/* pre.src-C++ doesn't work in CSS */
pre.src-clojure:before { content: 'Clojure'; }
pre.src-css:before { content: 'CSS'; }
pre.src-D:before { content: 'D'; }
pre.src-ditaa:before { content: 'ditaa'; }
pre.src-dot:before { content: 'Graphviz'; }
pre.src-calc:before { content: 'Emacs Calc'; }
pre.src-emacs-lisp:before { content: 'Emacs Lisp'; }
pre.src-fortran:before { content: 'Fortran'; }
pre.src-gnuplot:before { content: 'gnuplot'; }
pre.src-haskell:before { content: 'Haskell'; }
pre.src-hledger:before { content: 'hledger'; }
pre.src-java:before { content: 'Java'; }
pre.src-js:before { content: 'Javascript'; }
pre.src-latex:before { content: 'LaTeX'; }
pre.src-ledger:before { content: 'Ledger'; }
pre.src-lisp:before { content: 'Lisp'; }
pre.src-lilypond:before { content: 'Lilypond'; }
pre.src-lua:before { content: 'Lua'; }
pre.src-matlab:before { content: 'MATLAB'; }
pre.src-mscgen:before { content: 'Mscgen'; }
pre.src-ocaml:before { content: 'Objective Caml'; }
pre.src-octave:before { content: 'Octave'; }
pre.src-org:before { content: 'Org mode'; }
pre.src-oz:before { content: 'OZ'; }
pre.src-plantuml:before { content: 'Plantuml'; }
pre.src-processing:before { content: 'Processing.js'; }
pre.src-python:before { content: 'Python'; }
pre.src-R:before { content: 'R'; }
pre.src-ruby:before { content: 'Ruby'; }
pre.src-sass:before { content: 'Sass'; }
pre.src-scheme:before { content: 'Scheme'; }
pre.src-screen:before { content: 'Gnu Screen'; }
pre.src-sed:before { content: 'Sed'; }
pre.src-sh:before { content: 'shell'; }
pre.src-sql:before { content: 'SQL'; }
pre.src-sqlite:before { content: 'SQLite'; }
/* additional languages in org.el's org-babel-load-languages alist */
pre.src-forth:before { content: 'Forth'; }
pre.src-io:before { content: 'IO'; }
pre.src-J:before { content: 'J'; }
pre.src-makefile:before { content: 'Makefile'; }
pre.src-maxima:before { content: 'Maxima'; }
pre.src-perl:before { content: 'Perl'; }
pre.src-picolisp:before { content: 'Pico Lisp'; }
pre.src-scala:before { content: 'Scala'; }
pre.src-shell:before { content: 'Shell Script'; }
pre.src-ebnf2ps:before { content: 'ebfn2ps'; }
/* additional language identifiers per "defun org-babel-execute"
in ob-*.el */
pre.src-cpp:before { content: 'C++'; }
pre.src-abc:before { content: 'ABC'; }
pre.src-coq:before { content: 'Coq'; }
pre.src-groovy:before { content: 'Groovy'; }
/* additional language identifiers from org-babel-shell-names in
ob-shell.el: ob-shell is the only babel language using a lambda to put
the execution function name together. */
pre.src-bash:before { content: 'bash'; }
pre.src-csh:before { content: 'csh'; }
pre.src-ash:before { content: 'ash'; }
pre.src-dash:before { content: 'dash'; }
pre.src-ksh:before { content: 'ksh'; }
pre.src-mksh:before { content: 'mksh'; }
pre.src-posh:before { content: 'posh'; }
/* Additional Emacs modes also supported by the LaTeX listings package */
pre.src-ada:before { content: 'Ada'; }
pre.src-asm:before { content: 'Assembler'; }
pre.src-caml:before { content: 'Caml'; }
pre.src-delphi:before { content: 'Delphi'; }
pre.src-html:before { content: 'HTML'; }
pre.src-idl:before { content: 'IDL'; }
pre.src-mercury:before { content: 'Mercury'; }
pre.src-metapost:before { content: 'MetaPost'; }
pre.src-modula-2:before { content: 'Modula-2'; }
pre.src-pascal:before { content: 'Pascal'; }
pre.src-ps:before { content: 'PostScript'; }
pre.src-prolog:before { content: 'Prolog'; }
pre.src-simula:before { content: 'Simula'; }
pre.src-tcl:before { content: 'tcl'; }
pre.src-tex:before { content: 'TeX'; }
pre.src-plain-tex:before { content: 'Plain TeX'; }
pre.src-verilog:before { content: 'Verilog'; }
pre.src-vhdl:before { content: 'VHDL'; }
pre.src-xml:before { content: 'XML'; }
pre.src-nxml:before { content: 'XML'; }
/* add a generic configuration mode; LaTeX export needs an additional
(add-to-list 'org-latex-listings-langs '(conf " ")) in .emacs */
pre.src-conf:before { content: 'Configuration File'; }
table { border-collapse:collapse; }
caption.t-above { caption-side: top; }
caption.t-bottom { caption-side: bottom; }
td, th { vertical-align:top; }
th.org-right { text-align: center; }
th.org-left { text-align: center; }
th.org-center { text-align: center; }
td.org-right { text-align: right; }
td.org-left { text-align: left; }
td.org-center { text-align: center; }
dt { font-weight: bold; }
.footpara { display: inline; }
.footdef { margin-bottom: 1em; }
.figure { padding: 1em; }
.figure p { text-align: center; }
.equation-container {
display: table;
text-align: center;
width: 100%;
}
.equation {
vertical-align: middle;
}
.equation-label {
display: table-cell;
text-align: right;
vertical-align: middle;
}
.inlinetask {
padding: 10px;
border: 2px solid gray;
margin: 10px;
background: #ffffcc;
}
#org-div-home-and-up
{ text-align: right; font-size: 70%; white-space: nowrap; }
textarea { overflow-x: auto; }
.linenr { font-size: smaller }
.code-highlighted { background-color: #ffff00; }
.org-info-js_info-navigation { border-style: none; }
#org-info-js_console-label
{ font-size: 10px; font-weight: bold; white-space: nowrap; }
.org-info-js_search-highlight
{ background-color: #ffff00; color: #000000; font-weight: bold; }
.org-svg { }
</style>
</head>
<body>
<div id="content" class="content">
<h1 class="title">Wordle</h1>
<div id="table-of-contents" role="doc-toc">
<h2>Table of Contents</h2>
<div id="text-table-of-contents" role="doc-toc">
<ul>
<li><a href="#orgd28fade">1. Mise en bouche: picking an answer</a>
<ul>
<li><a href="#org1fca288">1.1. TDD for picking word functionality</a></li>
<li><a href="#orgcac504c">1.2. Solutions dictionary file</a></li>
<li><a href="#org83c5133">1.3. Importing dictionary: static/packaged asset file read</a></li>
<li><a href="#org34b421e">1.4. Debriefing on the method</a></li>
</ul>
</li>
<li><a href="#org045205d">2. Confirming guess is a valid word</a>
<ul>
<li><a href="#orgef876b7">2.1. Test setup</a></li>
<li><a href="#orgc5867ab">2.2. Implementing the feature, one test at a time</a></li>
<li><a href="#org8c2cae0">2.3. Performance trick</a></li>
<li><a href="#orgded3d64">2.4. Bug!</a></li>
<li><a href="#orga57ac51">2.5. Tangle out all the code</a></li>
</ul>
</li>
<li><a href="#org34cfd7e">3. Calculating guessed word’s score</a>
<ul>
<li><a href="#org634efa7">3.1. Can we start coding yet?</a></li>
<li><a href="#org3d264c7">3.2. Finalizing the scoring scenarios</a></li>
<li><a href="#orgbe6c98d">3.3. Writing up acceptance tests</a></li>
<li><a href="#orge723d42">3.4. More tests</a></li>
<li><a href="#orga7e2c27">3.5. Implementing the feature</a></li>
<li><a href="#org49f0475">3.6. Tangle it all out</a></li>
</ul>
</li>
<li><a href="#org0f13ec8">4. Playing a round of Wordle</a>
<ul>
<li><a href="#org8ef8668">4.1. Implementing the feature</a></li>
<li><a href="#org7906dc7">4.2. Tangling out the whole thing</a></li>
</ul>
</li>
<li><a href="#orgf6fba64">5. Final round: command line interface</a>
<ul>
<li><a href="#org9ade166">5.1. Tangling it out</a></li>
<li><a href="#orgf14166e">5.2. Launching as CLI</a></li>
</ul>
</li>
<li><a href="#orgaf8d61b">6. Conclusion</a></li>
<li><a href="#orgef86e54">7. Post-script: scoring bug</a>
<ul>
<li><a href="#orgd1bf9a8">7.1. The bug report</a></li>
<li><a href="#orge1dc1b0">7.2. Why are we scoring badly?</a></li>
<li><a href="#org8addd11">7.3. Fixing the issue</a></li>
<li><a href="#org8a606ca">7.4. Tangling again</a></li>
<li><a href="#orgf6ad4e1">7.5. Why didn’t you catch this earlier? Is it TDD/BDD’s fault or are you just a bad dev?</a></li>
<li><a href="#orgbc194d9">7.6. Why should this be a failure story?</a></li>
</ul>
</li>
</ul>
</div>
</div>
<p>
I’ve recently written <a href="https://jiby.tech/post/gherkin-features-user-requirements/">a series of blog posts about Gherkin</a>, the <a href="https://jiby.tech/post/bdd-dreams-cucumber-and-gherkin/">Behaviour-driven
development movement,</a> and how Cucumber (the BDD tool of choice) failed to
perform to expectations.
</p>
<p>
I wanted to showcase the BDD-inspired low-tech solution I came up with via a
toy project, demonstrating a small but significant programming task, broken down
as series of design-implementation cycles.
</p>
<p>
Wordle is a perfect target: it’s a small codebase, with a half dozen features to
string together into a useable game.
</p>
<p>
In order to document the process, the code is written via literate programming.
</p>
<blockquote>
<p>
Literate programming is the art of writing code as if it was a novel (or blogpost), writing down what’s needed, explaining the reasoning, and weaving in code snippets that add up to the codebase as we grow in understanding. The result is a “story” which can be read, but also “tangled” back into a proper codebase that works normally.
</p>
</blockquote>
<p>
For more context on the code repository (how to use, etc), please read the project
readme.
</p>
<p>
See also the online, pretty rendered version of this document on my personal
website: <a href="https://jiby.tech/project/literate_wordle/wordle.html">https://jiby.tech/project/literate_wordle/wordle.html</a>
</p>
<div id="outline-container-orgd28fade" class="outline-2">
<h2 id="orgd28fade"><span class="section-number-2">1.</span> Mise en bouche: picking an answer</h2>
<div class="outline-text-2" id="text-1">
<p>
To get us started, let’s cover the very first behaviour Wordle has to do: pick a
word that will become our secret answer.
</p>
<p>
As the first iteration in a test-driven project, it’s important that we set up
all the components we’ll need going forwards.
</p>
<p>
First, let’s formalise a little our first requirement, using Gherkin Features.
For context as to why/how we’re doing this, read <a href="https://jiby.tech/post/gherkin-features-user-requirements/">my post on gathering
requirements via Gherkin.</a>
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 1: </span>Our first requirement. Tangled (exported) to <code>features/pick_answer_word.feature</code></label><pre class="src src-feature" id="orga88343b"><span style="color: #51afef;">Feature:</span><span style="color: #ECBE7B;"> Pick an answer word</span>
As a Wordle game
I need to pick a random 5 letter word
In order to let players guess it
</pre>
</div>
<p>
Right. That’s fairly straightforward, but the secret word can’t just be random
characters, it needs to be a proper word. So we need to find a dictionary to
pick from.
</p>
</div>
<div id="outline-container-org1fca288" class="outline-3">
<h3 id="org1fca288"><span class="section-number-3">1.1.</span> TDD for picking word functionality</h3>
<div class="outline-text-3" id="text-1-1">
<p>
We want to write a test that validates that we can indeed pick a random word. But “Random” and “test” together should make anybody wince at the idea of
non-deterministic testing.
</p>
<p>
We <i>could</i> write a test that picks a word, then confirm the word came
from the dictionary file, but writing test would mean re-implementing the entirety of the
feature we’re testing, as well as rely on the internals of the implementation
being correct. That’s very wrong.
</p>
<p>
A good alternative would be to pin down the randomness (making the test
deterministic) by anchoring the randomness seed to known value, allowing
repeatable testing. But this is just the first test in a new project, so we want a
simple check to start with, so we compromise by making the assertion “is the
random word picked of five letter length”?
</p>
<p>
So we write down a new test file, under <code>tests/</code> folder, starting with a
file-level docstring that references the Gherkin feature this enforces.
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 2: </span>Test driving the first feature. Note the <code>pick_answer_word()</code> function doesn’t exist yet, we’re dreaming it up!</label><pre class="src src-python"><span style="color: #83898d;">"""Validates the Gherkin file features/pick_answer_word.feature:</span>
<span style="color: #83898d;">Feature: Pick an answer word</span>
<span style="color: #83898d;"> As a Wordle game</span>
<span style="color: #83898d;"> I need to pick a random 5 letter word</span>
<span style="color: #83898d;"> In order to let players guess it</span>
<span style="color: #83898d;">"""</span>
<span style="color: #51afef;">from</span> literate_wordle.words <span style="color: #51afef;">import</span> pick_answer_word
<span style="color: #51afef;">def</span> <span style="color: #c678dd;">test_pick_word_ok_length</span><span style="color: #FF8811;">()</span>:
<span style="color: #83898d;">"""Confirm a wordle solution is of right size"""</span>
<span style="color: #51afef;">assert</span> <span style="color: #c678dd;">len</span><span style="color: #FF8811;">(</span>pick_answer_word<span style="color: #8F0;">()</span><span style="color: #FF8811;">)</span> == <span style="color: #da8548; font-weight: bold;">5</span>, <span style="color: #98be65;">"Picked wordle solution is wrong size!"</span>
</pre>
</div>
<p>
Of course, since that feature isn’t implemented (not even the module’s
skeleton), running tests right now would crash as import errors, rather than
give a red light.
</p>
<p>
So let’s implement the barest hint of the <code>pick_answer_word</code> function that
returns the wrong thing, to make the test run and fail:
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 3: </span>First, the docstring for a new python module <code>words.py</code>, under the <code>literate_wordle</code> package.</label><pre class="src src-python"><span style="color: #83898d;">"""Dictionary features to back wordle solutions"""</span>
</pre>
</div>
<p>
In that module, let’s add the skeleton for our <code>pick_answer_word</code> function, but
return an invalid result, to make test explicitly fail:
</p>
<div class="org-src-container">
<pre class="src src-python"><span style="color: #51afef;">def</span> <span style="color: #c678dd;">pick_answer_word</span><span style="color: #FF8811;">()</span> -> <span style="color: #c678dd;">str</span>:
<span style="color: #83898d;">"""Pick a Wordle solution/answer from wordle dictionary"""</span>
<span style="color: #51afef;">return</span> <span style="color: #98be65;">""</span> <span style="color: #5B6268;"># </span><span style="color: #5B6268;">Incorrect solution to get RED test</span>
</pre>
</div>
<p>
With our test ready, and a dummy function in place, let’s see the tests go red:
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 4: </span>Running the tests.</label><pre class="src src-shell"><span style="color: #ECBE7B;">make</span> test
</pre>
</div>
<pre class="example" id="orga93de98">
poetry run pytest
============================= test session starts ==============================
platform linux -- Python 3.9.5, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /home/jiby/dev/ws/short/literate_wordle/.venv/bin/python
cachedir: .pytest_cache
rootdir: /home/jiby/dev/ws/short/literate_wordle, configfile: pyproject.toml
plugins: cov-3.0.0, datadir-1.3.1, clarity-1.0.1
collecting ... collected 2 items
tests/test_pick_word.py::test_pick_word_ok_length FAILED [ 50%]
tests/test_version.py::test_version PASSED [100%]
=================================== FAILURES ===================================
___________________________ test_pick_word_ok_length ___________________________
def test_pick_word_ok_length():
"""Confirm a wordle solution is of right size"""
> assert len(pick_answer_word()) == 5, "Picked wordle solution is wrong size!"
E AssertionError: Picked wordle solution is wrong size!
E assert == failed. [pytest-clarity diff shown]
E
E LHS vs RHS shown below
E
E 0
E 5
E
tests/test_pick_word.py:13: AssertionError
- generated xml file: /home/jiby/dev/ws/short/literate_wordle/test_results/results.xml -
=========================== short test summary info ============================
FAILED tests/test_pick_word.py::test_pick_word_ok_length - AssertionError: Pi...
========================= 1 failed, 1 passed in 0.07s ==========================
make: *** [Makefile:16: test] Error 1
</pre>
<p>
As pytest mentions, we should see a wordle solution of 5 letters, not zero.
So the test indeed failed as expected, we can now make it pass by implementing
the feature.
</p>
<p>
Taking a quick step back, think of how conveniently TDD lets us “dream up an
API”, by describing functions and files that don’t need to exist yet.
</p>
</div>
</div>
<div id="outline-container-orgcac504c" class="outline-3">
<h3 id="orgcac504c"><span class="section-number-3">1.2.</span> Solutions dictionary file</h3>
<div class="outline-text-3" id="text-1-2">
<p>
Since we’re trying to match the Wordle website’s implementation, let’s reuse
Wordle’s own dictionary. Someone <a href="https://raw.githubusercontent.com/AllValley/WordleDictionary/main/wordle_solutions_alphabetized.txt">helpfully uploaded it</a>. Let’s download it:
</p>
<div class="org-src-container">
<pre class="src src-shell">wget <span style="color: #98be65;">\</span>
--output-document <span style="color: #98be65;">"wordle_answers_dict.txt"</span> <span style="color: #98be65;">\</span>
<span style="color: #98be65;">"https://raw.githubusercontent.com/AllValley/WordleDictionary/6f14d2f03d01c36fe66e3ccc0929394251ab139d/wordle_solutions_alphabetized.txt"</span>
</pre>
</div>
<p>
Except an alphabetically sorted text file takes space for no good reason. Let’s
compress it preventively.
</p>
<p>
While this can legitimately be seen as a premature optimization, we can see this
as trying to “flatten” a static text file into a binary “asset” that can be
packaged into the project’s package, like icons are part of webapps.
</p>
<div class="org-src-container">
<pre class="src src-shell"><span style="color: #dcaeea;">ANSWERS_FILE</span>=<span style="color: #98be65;">"wordle_answers_dict.txt"</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">Get raw file size in kilobytes</span>
du -k <span style="color: #98be65;">"</span><span style="color: #a9a1e1;">$</span><span style="color: #dcaeea;">{ANSWERS_FILE}</span><span style="color: #98be65;">"</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">Compress the file (removes original)</span>
gzip <span style="color: #98be65;">"</span><span style="color: #a9a1e1;">$</span><span style="color: #dcaeea;">ANSWERS_FILE</span><span style="color: #98be65;">"</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">Check size after compression</span>
du -k <span style="color: #98be65;">"</span><span style="color: #a9a1e1;">$</span><span style="color: #dcaeea;">{ANSWERS_FILE}</span><span style="color: #98be65;">.gz"</span>
</pre>
</div>
<pre class="example">
16 wordle_answers_dict.txt
8 wordle_answers_dict.txt.gz
</pre>
<p>
Sweet, we have cut down the filesize by half.
</p>
</div>
</div>
<div id="outline-container-org83c5133" class="outline-3">
<h3 id="org83c5133"><span class="section-number-3">1.3.</span> Importing dictionary: static/packaged asset file read</h3>
<div class="outline-text-3" id="text-1-3">
<p>
At first glance, the implementation of the function we want is simple, it looks
roughly like this:
</p>
<div class="org-src-container">
<pre class="src src-python"><span style="color: #51afef;">with</span> <span style="color: #c678dd;">open</span><span style="color: #FF8811;">(</span><span style="color: #98be65;">"my_dictionary.txt"</span>, <span style="color: #98be65;">"r"</span><span style="color: #FF8811;">)</span> <span style="color: #51afef;">as</span> fd:
<span style="color: #dcaeea;">my_text</span> = fd.read<span style="color: #FF8811;">()</span>
</pre>
</div>
<p>
One just needs to find the right file path to open, just add sprinkles to deal
with compression. Sure enough, that is fairly easy.
</p>
<p>
The issue is that we’re trying to write a python package here, which means it could
be downloaded via <code>pip install</code> and installed in an arbitary location on
someone’s computer. Our code needs to refer to the file as “the file XYZ inside
the assets folder of our package”. We need to look up how to express that.
</p>
<p>
From <a href="https://stackoverflow.com/a/20885799">Stackoverflow on reading static files from inside Python package</a>, we can
use the <code>importlib.resources</code> module, since our project requires <code>Python 3.9</code> onwards.
</p>
<p>
So we’ll move our dictionary zip file into a new module (folder) called
<code>assets</code>, which will be a proper python module that can be imported from:
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 5: </span>Moving our dictionary to the new <code>assets</code> sub-module.</label><pre class="src src-shell"><span style="color: #ECBE7B;">mkdir</span> -p src/literate_wordle/assets/
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">A proper python module means an __init__.py: Give it a docstring</span>
<span style="color: #ECBE7B;">echo</span> <span style="color: #98be65;">'"""Static binary assets (dictionaries) required to perform Wordle"""'</span> > src/literate_wordle/assets/__init__.py
<span style="color: #ECBE7B;">mv</span> wordle_answers_dict.txt.gz src/literate_wordle/
</pre>
</div>
<p>
With the file in correct position, let’s redefine the <code>words</code> module we left empty, to provide the <code>pick_answer_word</code> function.
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 6: </span>Defining new python module under <code>src/literate_wordle/words.py</code>, starting with docstrings.</label><pre class="src src-python" id="orgab7ad14"><span style="color: #83898d;">"""Dictionary features to back wordle solutions"""</span>
</pre>
</div>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 7: </span>Necessary imports from the standard library.</label><pre class="src src-python" id="org033e952"><span style="color: #51afef;">import</span> gzip
<span style="color: #51afef;">import</span> importlib.resources <span style="color: #51afef;">as</span> pkg_resources
</pre>
</div>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 8: </span>Local import of new <code>assets/</code> folder</label><pre class="src src-python" id="org61ce7ab"><span style="color: #51afef;">from</span> . <span style="color: #51afef;">import</span> assets <span style="color: #5B6268;"># </span><span style="color: #5B6268;">Relative import of the assets/ folder</span>
</pre>
</div>
<p>
We need a convenience function to load the zip file into a list of strings.
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 9: </span>Actual function to unzip dictionary. Note newline-delimited words now lowercased into a list-of-word strings with no trailing whitespace</label><pre class="src src-python" id="org43f87df"><span style="color: #51afef;">def</span> <span style="color: #c678dd;">get_words_list</span><span style="color: #FF8811;">()</span> -> <span style="color: #c678dd;">list</span><span style="color: #FF8811;">[</span><span style="color: #c678dd;">str</span><span style="color: #FF8811;">]</span>:
<span style="color: #83898d;">"""Decompress the wordle dictionary"""</span>
<span style="color: #dcaeea;">dict_compressed_bytes</span> = pkg_resources.read_binary<span style="color: #FF8811;">(</span>
assets, <span style="color: #98be65;">"wordle_answers_dict.txt.gz"</span>
<span style="color: #FF8811;">)</span>
<span style="color: #dcaeea;">dict_string</span> = gzip.decompress<span style="color: #FF8811;">(</span>dict_compressed_bytes<span style="color: #FF8811;">)</span>.decode<span style="color: #FF8811;">(</span><span style="color: #98be65;">"ascii"</span><span style="color: #FF8811;">)</span>
<span style="color: #dcaeea;">answer_word_list</span> = <span style="color: #FF8811;">[</span>word.strip<span style="color: #8F0;">()</span>.lower<span style="color: #8F0;">()</span>.strip<span style="color: #8F0;">()</span> <span style="color: #51afef;">for</span> word <span style="color: #51afef;">in</span> dict_string.split<span style="color: #8F0;">(</span><span style="color: #98be65;">"\n"</span><span style="color: #8F0;">)</span><span style="color: #FF8811;">]</span>
<span style="color: #51afef;">return</span> answer_word_list
</pre>
</div>
<p>
Ideally we would make a test dedicated for proving this function, but our
already-failing acceptance test is pretty much covering this entire feature, so
it’s not worth it just now. This is one of those tradeoffs we make between toy
projects and long-term maintainability of code as a team.
</p>
<p>
With the word list in hand, writing out the pick function is trivial:
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 10: </span>Import from standard library for randomness</label><pre class="src src-python" id="org95702e4"><span style="color: #51afef;">from</span> random <span style="color: #51afef;">import</span> choice
</pre>
</div>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 11: </span>Pick-a-word! Pretty simple, with all the legwork we did already.</label><pre class="src src-python" id="orgdd93d16"><span style="color: #51afef;">def</span> <span style="color: #c678dd;">pick_answer_word</span><span style="color: #FF8811;">()</span> -> <span style="color: #c678dd;">str</span>:
<span style="color: #83898d;">"""Pick a single word out of the dictionary of answers"""</span>
<span style="color: #51afef;">return</span> choice<span style="color: #FF8811;">(</span>get_words_list<span style="color: #8F0;">()</span><span style="color: #FF8811;">)</span>
</pre>
</div>
<p>
With the function implemented, we can try it out in a Python REPL (Read Eval
Print Loop, also known as an interactive interpreter):
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 12: </span>Open an interactive python session, ask for a random word twice.</label><pre class="src src-shell">poetry run python3
>> from literate_wordle import words
>> print<span style="color: #FF8811;">(</span>words.pick_answer_word<span style="color: #8F0;">()</span><span style="color: #FF8811;">)</span>
stink
>> print<span style="color: #FF8811;">(</span>words.pick_answer_word<span style="color: #8F0;">()</span><span style="color: #FF8811;">)</span>
blank
</pre>
</div>
<p>
Perfect! So the test should now pass, right?
</p>
<div class="org-src-container">
<pre class="src src-shell"><span style="color: #ECBE7B;">make</span> test
</pre>
</div>
<pre class="example" id="org2e33998">
poetry run pytest
============================= test session starts ==============================
platform linux -- Python 3.9.5, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /home/jiby/dev/ws/short/literate_wordle/.venv/bin/python
cachedir: .pytest_cache
rootdir: /home/jiby/dev/ws/short/literate_wordle, configfile: pyproject.toml
plugins: cov-3.0.0, datadir-1.3.1, clarity-1.0.1
collecting ... collected 2 items
tests/test_pick_word.py::test_pick_word_ok_length PASSED [ 50%]
tests/test_version.py::test_version PASSED [100%]
- generated xml file: /home/jiby/dev/ws/short/literate_wordle/test_results/results.xml -
============================== 2 passed in 0.03s ===============================
</pre>
<p>
Acceptance tests pass, and linters are happy (not pictured, use <code>make</code> to
check).
</p>
<p>
Because the acceptance test pass, that means the feature is ready to ship!
That’s the BDD guarantee.
</p>
<p>
Of course, keen readers will notice sub-optimal code, like how we’re unzipping
the entire solutions file on each requested answer. Because “picking a solution
word” is something done on the order of <i>once</i> over the <i>entire runtime</i> of a
Wordle session, we choose to leave this performance wart be.
</p>
</div>
</div>
<div id="outline-container-org34b421e" class="outline-3">
<h3 id="org34b421e"><span class="section-number-3">1.4.</span> Debriefing on the method</h3>
<div class="outline-text-3" id="text-1-4">
<p>
We just completed our first loop: determine a small component that needs
implemented to build towards the Wordle goal, spell it out with Gherkin features,
explicit the feature via acceptance test, and iterate on the new RED test until it becomes
green, then ship the feature.
</p>
<p>
Common TDD workflow adds a refactor or “blue” component to the cycle, which is
indeed necessary for production code, as it lends maintainability (the first
draft of a codebase is usually taking big shortcuts). But this project is
meant as entertainment material, and proper refactoring would mean refactoring the <code>wordle.org</code>
source file, which would drown out the nice narrative we’re building here, so
let’s leave it here.
</p>
<p>
Along the way, the code blocks spelled out in this narrative-oriented file is
tangled out into proper code paths, so that the <code>Makefile</code> can pick it up and
validate the proper package-ness. We’ll see as we implement the next feature how
such a weaving of code snippets works.
</p>
</div>
</div>
</div>
<div id="outline-container-org045205d" class="outline-2">
<h2 id="org045205d"><span class="section-number-2">2.</span> Confirming guess is a valid word</h2>
<div class="outline-text-2" id="text-2">
<p>
Now that we can pick secret words, we need to start processing guesses. The very
first thing we need is validating guesses are proper words, and of the right
size. This feature will give us a familiar context (dictionaries), while slowly
ramping up the details of the Gherkin features:
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 13: </span>New Gherkin feature file <code>features/checking_guess_valid_word.feature</code></label><pre class="src src-feature" id="orgac55b97"><span style="color: #51afef;">Feature:</span><span style="color: #ECBE7B;"> Checking a guess is a valid word</span>
As a Wordle game
I need to confirm each guessed word is valid
So that I only accept real words, no kwyjibo
</pre>
</div>
<p>
In practice, this means multiple things:
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 14: </span>Scenarios to describe the feature in details</label><pre class="src src-feature" id="orgcee7bff"><span style="color: #51afef;">Scenario:</span><span style="color: #c678dd;"> Reject long words</span>
<span style="color: #51afef;"> When</span> guessing <span style="color: #98be65;">"affable"</span>
<span style="color: #51afef;"> Then</span> the guess is rejected
<span style="color: #51afef;"> And</span> reason for rejection is <span style="color: #98be65;">"Guess too long"</span>
<span style="color: #51afef;">Scenario:</span><span style="color: #c678dd;"> Reject short words</span>
<span style="color: #51afef;"> When</span> guessing <span style="color: #98be65;">"baby"</span>
<span style="color: #51afef;"> Then</span> the guess is rejected
<span style="color: #51afef;"> And</span> reason for rejection is <span style="color: #98be65;">"Guess too short"</span>
<span style="color: #51afef;">Scenario:</span><span style="color: #c678dd;"> Reject fake words via dictionary</span>
<span style="color: #51afef;"> When</span> guessing <span style="color: #98be65;">"vbpdj"</span>
<span style="color: #51afef;"> Then</span> the guess is rejected
<span style="color: #51afef;"> And</span> reason for rejection is <span style="color: #98be65;">"Not a word from the dictionary"</span>
<span style="color: #51afef;">Scenario:</span><span style="color: #c678dd;"> Accept five letter dictionary words</span>
<span style="color: #51afef;"> When</span> guessing <span style="color: #98be65;">"crane"</span>
<span style="color: #51afef;"> Then</span> the guess is accepted
</pre>
</div>
<p>
So, with a feature covering these scenarios, we can start laying out acceptance
tests.
</p>
<p>
Since I quite like to use the Gherkin feature file inside the
docstrings of Python tests, I’m going to take advantage of having already
written the feature above, to reference it, so I can template it out in code snippets:
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 15: </span>New test file’s module-level docstring, using (invisible during rendering) templating to fill in the gherkin feature from Listing <a href="#orgac55b97">13</a></label><pre class="src src-python" id="org379df24"><span style="color: #83898d;">"""Validates the Gherkin file features/checking_guess_valid_word.feature:</span>
<span style="color: #83898d;">Feature: Checking a guess is a valid word</span>
<span style="color: #83898d;"> As a Wordle game</span>
<span style="color: #83898d;"> I need to confirm each guessed word is valid</span>
<span style="color: #83898d;"> So that I only accept real words, no kwyjibo</span>
<span style="color: #83898d;">"""</span>
</pre>
</div>
<p>
Just this once, I’ll show how the templating happens behind the scene:
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 16: </span>Same code block as Listing <a href="#org379df24">15</a>, but without the magic templating enabled: each block with two chevrons around references a code block from above.</label><pre class="src src-python" id="org52953d2"><span style="color: #83898d;">"""Validates the Gherkin file features/checking_guess_valid_word.feature:</span>
<span style="color: #83898d;"><<feature-check-valid-guess>></span>
<span style="color: #83898d;"><<scenario-check-valid-guess>></span>
<span style="color: #83898d;">"""</span>
</pre>
</div>
</div>
<div id="outline-container-orgef876b7" class="outline-3">
<h3 id="orgef876b7"><span class="section-number-3">2.1.</span> Test setup</h3>
<div class="outline-text-3" id="text-2-1">
<p>
With the feature described, let’s import our hypothetical test code
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 17: </span>Import a new function we’ll be defining</label><pre class="src src-python" id="org72fdda2"><span style="color: #51afef;">from</span> literate_wordle.words <span style="color: #51afef;">import</span> check_valid_word
</pre>
</div>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 18: </span>A simple test using the first scenario</label><pre class="src src-python" id="org0389e45"><span style="color: #51afef;">def</span> <span style="color: #c678dd;">test_reject_long_words</span><span style="color: #FF8811;">()</span>:
<span style="color: #83898d;">"""Scenario: Reject long words"""</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">When guessing "affable"</span>
<span style="color: #dcaeea;">guess</span> = <span style="color: #98be65;">"affable"</span>
<span style="color: #dcaeea;">is_valid</span>, <span style="color: #dcaeea;">reject_reason</span> = check_valid_word<span style="color: #FF8811;">(</span>guess<span style="color: #FF8811;">)</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">Then the guess is rejected</span>
<span style="color: #51afef;">assert</span> <span style="color: #51afef;">not</span> is_valid, <span style="color: #98be65;">"Overly long guess should have been rejected"</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">And reason for rejection is "Guess too long"</span>
<span style="color: #51afef;">assert</span> reject_reason == <span style="color: #98be65;">"Guess too long"</span>
</pre>
</div>
<p>
Notice the pattern of referencing the Gherkin Scenario as comments inside the
test. This practice is something I came up with on my own after being a bit
disappointed with Cucumber. You can read more about it in <a href="https://jiby.tech/post/low-tech-cucumber-replacement/">my post on low-tech
cucumber replacement</a>.
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 19: </span>The opposite test, text too short</label><pre class="src src-python" id="org2e39d8f"><span style="color: #51afef;">def</span> <span style="color: #c678dd;">test_reject_overly_short_words</span><span style="color: #FF8811;">()</span>:
<span style="color: #83898d;">"""Scenario: Reject short words"""</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">When guessing "baby"</span>
<span style="color: #dcaeea;">guess</span> = <span style="color: #98be65;">"baby"</span>
<span style="color: #dcaeea;">is_valid</span>, <span style="color: #dcaeea;">reject_reason</span> = check_valid_word<span style="color: #FF8811;">(</span>guess<span style="color: #FF8811;">)</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">Then the guess is rejected</span>
<span style="color: #51afef;">assert</span> <span style="color: #51afef;">not</span> is_valid, <span style="color: #98be65;">"Overly short guess should have been rejected"</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">And reason for rejection is "Guess too short"</span>
<span style="color: #51afef;">assert</span> reject_reason == <span style="color: #98be65;">"Guess too short"</span>
</pre>
</div>
<p>
And finally, the dictionary checks:
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 20: </span>Non-dictionary words test</label><pre class="src src-python" id="orgf43c3d3"><span style="color: #51afef;">def</span> <span style="color: #c678dd;">test_reject_nondict_words</span><span style="color: #FF8811;">()</span>:
<span style="color: #83898d;">"""Scenario: Reject fake words via dictionary"""</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">When guessing "vbpdj"</span>
<span style="color: #dcaeea;">guess</span> = <span style="color: #98be65;">"vbpdj"</span>
<span style="color: #dcaeea;">is_valid</span>, <span style="color: #dcaeea;">reject_reason</span> = check_valid_word<span style="color: #FF8811;">(</span>guess<span style="color: #FF8811;">)</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">Then the guess is rejected</span>
<span style="color: #51afef;">assert</span> <span style="color: #51afef;">not</span> is_valid, <span style="color: #98be65;">"Word not in dictionary should have been rejected"</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">And reason for rejection is "Not a word from the dictionary"</span>
<span style="color: #51afef;">assert</span> reject_reason == <span style="color: #98be65;">"Not a word from the dictionary"</span>
</pre>
</div>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 21: </span>Dictionary words test</label><pre class="src src-python" id="org3c6584b"><span style="color: #51afef;">def</span> <span style="color: #c678dd;">test_accept_dict_words</span><span style="color: #FF8811;">()</span>:
<span style="color: #83898d;">"""Scenario: Accept five letter dictionary words"""</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">When guessing "crane"</span>
<span style="color: #dcaeea;">guess</span> = <span style="color: #98be65;">"crane"</span>
<span style="color: #dcaeea;">is_valid</span>, <span style="color: #dcaeea;">reject_reason</span> = check_valid_word<span style="color: #FF8811;">(</span>guess<span style="color: #FF8811;">)</span>
<span style="color: #5B6268;"># </span><span style="color: #5B6268;">Then the guess is accepted</span>
<span style="color: #51afef;">assert</span> is_valid, <span style="color: #98be65;">"Correct length word in dictionary should have been accepted"</span>
</pre>
</div>
<p>
One tiny detail regarding this last example, which highlights why separating
Gherkin from actual code is important: We describe in the positive scenario the
need to accept a correct word in terms of “not rejecting”, which in code maps to
the <code>is_valid</code> boolean. That’s suffficient to validate the originalGherkin
scenario, which is what we think of when designing the software.
</p>
<p>
But as we see in the implementation, there’s also the matter of the
<code>reject_reason</code> component, which we should check for emptiness. That emptiness is an
implementation detail, which has no reason to be laid out in the original
scenario, but is still valid to make assertions on as part of the
implementation’s check. So we add the following line to the test:
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 22: </span>Appended line to Listing <a href="#org3c6584b">21</a>. Doesn’t map back to Gherkin, because it is an implementation detail, not part of the feature’s requirement itself. Still worth checking, in practice.</label><pre class="src src-python" id="orgedd197e"> <span style="color: #51afef;">assert</span> reject_reason <span style="color: #51afef;">is</span> <span style="color: #a9a1e1;">None</span>, <span style="color: #98be65;">"Accepted word should have no reason to be rejected"</span>
</pre>
</div>
<p>
With all these (high level) tests in hand, let’s write up some small
implementation to get RED tests instead of a crash.
</p>
<p>
First up is defining the function’s signature: Simple enough, we take a string guess
in, and return a boolean and a string for justification. Except sometimes (as
seen in Listing <a href="#orgedd197e">22</a>) the reason is <code>None</code>, so that’s more of an
<code>Optional</code> string, which we’ll need to import.
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 23: </span>Import type hints for function type definition</label><pre class="src src-python" id="org854300c"><span style="color: #51afef;">from</span> typing <span style="color: #51afef;">import</span> Optional
</pre>
</div>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 24: </span>Function signature without its content</label><pre class="src src-python" id="orgba280a5"><span style="color: #51afef;">def</span> <span style="color: #c678dd;">check_valid_word</span><span style="color: #FF8811;">(</span>guess: <span style="color: #c678dd;">str</span><span style="color: #FF8811;">)</span> -> <span style="color: #c678dd;">tuple</span><span style="color: #FF8811;">[</span><span style="color: #c678dd;">bool</span>, Optional<span style="color: #8F0;">[</span><span style="color: #c678dd;">str</span><span style="color: #8F0;">]</span><span style="color: #FF8811;">]</span>:
</pre>
</div>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 25: </span>Fill the function, to give valid-but-nonsensical output</label><pre class="src src-python" id="org34e59d3"> <span style="color: #98be65;">"""Pretends to check if guess is a valid word"""</span>
<span style="color: #51afef;">return</span> <span style="color: #a9a1e1;">False</span>, <span style="color: #98be65;">"Not implemented"</span>
</pre>
</div>
<p>
All right, so we have tests, let’s see them fail!
</p>
<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 26: </span>Run the tests. The <code>2>&1 || true</code> part is to ensure any failed test’s output goes to stdout (in this document) and bad exit codes don’t get marked as failures of the code block’s execution.</label><pre class="src src-shell" id="orgb17d0cb"><span style="color: #ECBE7B;">make</span> test <span style="color: #da8548; font-weight: bold;">2</span>>&<span style="color: #da8548; font-weight: bold;">1</span> || true
</pre>
</div>
<pre class="example" id="org745070c">
poetry run pytest
============================= test session starts ==============================
platform linux -- Python 3.9.5, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /home/jiby/dev/ws/short/literate_wordle/.venv/bin/python
cachedir: .pytest_cache
rootdir: /home/jiby/dev/ws/short/literate_wordle, configfile: pyproject.toml
plugins: cov-3.0.0, clarity-1.0.1
collecting ... collected 5 items
tests/test_checking_guess_valid_word.py::test_reject_long_words FAILED [ 20%]
tests/test_checking_guess_valid_word.py::test_reject_overly_short_words FAILED [ 40%]
tests/test_checking_guess_valid_word.py::test_reject_nondict_words FAILED [ 60%]
tests/test_checking_guess_valid_word.py::test_accept_dict_words FAILED [ 80%]
tests/test_pick_word.py::test_pick_word_ok_length PASSED [100%]
=================================== FAILURES ===================================
____________________________ test_reject_long_words ____________________________
def test_reject_long_words():
"""Scenario: Reject long words"""
# When guessing "affable"
guess = "affable"
is_valid, reject_reason = check_valid_word(guess)
# Then the guess is rejected
assert not is_valid, "Overly long guess should have been rejected"
# And reason for rejection is "Guess too long"
> assert reject_reason == "Guess too long"
E assert == failed. [pytest-clarity diff shown]
E
E LHS vs RHS shown below
E
E Not implemented
E Guess too long
E
tests/test_checking_guess_valid_word.py:39: AssertionError
________________________ test_reject_overly_short_words ________________________
def test_reject_overly_short_words():
"""Scenario: Reject short words"""
# When guessing "baby"
guess = "baby"
is_valid, reject_reason = check_valid_word(guess)
# Then the guess is rejected
assert not is_valid, "Overly short guess should have been rejected"
# And reason for rejection is "Guess too short"
> assert reject_reason == "Guess too short"
E assert == failed. [pytest-clarity diff shown]
E
E LHS vs RHS shown below
E
E Not implemented
E Guess too short
E
tests/test_checking_guess_valid_word.py:50: AssertionError
__________________________ test_reject_nondict_words ___________________________
def test_reject_nondict_words():
"""Scenario: Reject fake words via dictionary"""
# When guessing "vbpdj"
guess = "vbpdj"
is_valid, reject_reason = check_valid_word(guess)
# Then the guess is rejected
assert not is_valid, "Word not in dictionary should have been rejected"
# And reason for rejection is "Not a word from the dictionary"
> assert reject_reason == "Not a word from the dictionary"
E assert == failed. [pytest-clarity diff shown]
E
E LHS vs RHS shown below
E
E Not implemented
E Not a word from the dictionary
E
tests/test_checking_guess_valid_word.py:61: AssertionError
____________________________ test_accept_dict_words ____________________________
def test_accept_dict_words():
"""Scenario: Accept five letter dictionary words"""
# When guessing "crane"
guess = "crane"
is_valid, reject_reason = check_valid_word(guess)
# Then the guess is accepted
> assert is_valid, "Correct length word in dictionary should have been accepted"
E AssertionError: Correct length word in dictionary should have been accepted