├── requirements-win.txt
├── images
├── EZUIcPkWoAMG0nK.png
└── EZUIm_7WAAA0QpL.png
├── LICENSE
├── PyDungeon.sln
├── PyDungeon.pyproj
├── README.md
├── .gitattributes
├── cursor15-dungeon-unedited-basic.txt
├── .gitignore
├── dungeon-memory-sim.py
├── cursor15-dungeon-annotated-basic.txt
└── pydungeon.py
/requirements-win.txt:
--------------------------------------------------------------------------------
1 | windows-curses
2 |
--------------------------------------------------------------------------------
/images/EZUIcPkWoAMG0nK.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chgowiz/PyDungeon/HEAD/images/EZUIcPkWoAMG0nK.png
--------------------------------------------------------------------------------
/images/EZUIm_7WAAA0QpL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chgowiz/PyDungeon/HEAD/images/EZUIm_7WAAA0QpL.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Michael Shorten (chgowiz)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PyDungeon.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30104.148
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "PyDungeon", "PyDungeon.pyproj", "{B6894364-5405-4815-A74F-0A5336597418}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DA54300F-2340-4B65-9123-2BEBB815500C}"
9 | ProjectSection(SolutionItems) = preProject
10 | README.md = README.md
11 | EndProjectSection
12 | EndProject
13 | Global
14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
15 | Debug|Any CPU = Debug|Any CPU
16 | Release|Any CPU = Release|Any CPU
17 | EndGlobalSection
18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
19 | {B6894364-5405-4815-A74F-0A5336597418}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20 | {B6894364-5405-4815-A74F-0A5336597418}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | EndGlobalSection
22 | GlobalSection(SolutionProperties) = preSolution
23 | HideSolutionNode = FALSE
24 | EndGlobalSection
25 | GlobalSection(ExtensibilityGlobals) = postSolution
26 | SolutionGuid = {8991B57B-3B81-485A-9450-DA40B54F43F3}
27 | EndGlobalSection
28 | EndGlobal
29 |
--------------------------------------------------------------------------------
/PyDungeon.pyproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Debug
4 | 2.0
5 | b6894364-5405-4815-a74f-0a5336597418
6 | .
7 | pydungeon.py
8 |
9 |
10 | .
11 | .
12 | PyDungeon
13 | PyDungeon
14 |
15 |
16 | true
17 | false
18 |
19 |
20 | true
21 | false
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PyDungeon
2 |
3 | UPDATE: 6/5/20 - With this last checkin and merge to master branch, I've achieved all my goals.
4 | - understanding the 1979 Commodore BASIC code
5 | - implemented (mostly) as-is in Python
6 | - improve code to more modern approach
7 | - fix bugs, make small tweaks.
8 |
9 | If I started to do more, like add more features, I might as well make a rogue clone. And that's not really the purposes of this. If you're checking out my code, I hope you enjoy! If you have comments/questions, you can get ahold of me at chgowiz@gmail.com
10 |
11 | ------
12 |
13 | This is a project to recreate DUNGEON - a graphical dungeon crawler developed by Brian Sawyer for Cursor Magazine, issue #15. It was my very first computer D&D game, at a time when I had just started reading and playing the tabletop roleplaying game. DUNGEON was an eye-opener to me at the time (1979/1980).
14 |
15 | I'm attempting to recreate the game based on the source code, implemented in Python. Obviously, some things will be very different - the Commodore PET implemented a crude, but effective graphics mode which DUNGEON took advantage of. No such luck in console-based Python! I have vague dreams of figuring out the basic algorithms and then implementing using something a bit more graphical. We'll see..
16 |
17 | The source code from the original game is in the repo, and my comments on both an annotated copy of the source and the Python code should hopefully give some insights into what Messr. Sawyer had to do in order to implement this game on the old PET! I hope you enjoy this as much as I do.
18 |
19 | ## Prerequisites
20 |
21 | ### Unix (Linux, OS X)
22 |
23 | The game should run out of the box. Just run the game as described below.
24 |
25 | ### Windows
26 |
27 | - [Python 3](https://www.python.org/) is needed
28 | - The Curses library for windows is needed. Run `pip install -r requirements-win.txt` once before the first start of the game.
29 |
30 | ## To run:
31 |
32 | - Under Unix: `python3 ./pydungeon.py`
33 | - Under Windows should be `.\pydungeon.py` enough
34 |
35 | |CBM PET DUNGEON|PyDungeon (dungeon memory map dump)|
36 | :--------------:|:-----------------:
37 |
|
38 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/cursor15-dungeon-unedited-basic.txt:
--------------------------------------------------------------------------------
1 | 0 CLR:POKE59468,12
2 | 1 REM DUNGEON COPYRIGHT (C) 1979 BRIAN SAWYER
3 | 2 REM 1310 DOVER HILL ROAD
4 | 3 REM SANTA BARBARA, CA. 93103
5 | 4 :
6 | 5 REM CURSOR #15, NOV/DEC 1979
7 | 6 REM BOX 550, GOLETA, CA. 93017
8 | 7 REM LINES 61000-65000 (C) 1979 CURSOR MAGAZINE
9 | 8 :
10 | 10 REM AS OF 12/29/79
11 | 90 PG$="DUNGEON":NM$="15":GOSUB62000
12 | 100 RS=23:CS=40:SZ=RS*CS:BL=(25-RS)*40:RS=RS-1:CS=CS-1
13 | 130 REM TRICK: DG$() STRINGS GO AT END OF MEMORY!
14 | 140 DIMDG$(24):E$=" "
15 | 150 FORI=0TO24:DG$(I)=E$+"":NEXTI
16 | 160 ER$=CHR$(19)+CHR$(17)+" "+CHR$(19)+CHR$(17)
17 | 170 E2$=CHR$(19)+" "+CHR$(19)
18 | 180 PRINTCHR$(147);"SETTING UP..."
19 | 190 TS=PEEK(QM)+256*PEEK(QM+1)-SZ:AX=32768
20 | 200 FORI=TS+40TOTS+SZ-41:POKEI,32:NEXTI
21 | 210 HP=50:MG=0:EX=0:PX=0:HG=0:Z=0:FG=0:K1=0:E=0:S=0:W=160:ET=160
22 | 220 TI$="000000":TM=TI+3600
23 | 230 GOSUB380
24 | 240 TS=TS-BL
25 | 250 L=INT(RND(1)*SZ+TS):IFPEEK(L)<>160THEN250
26 | 260 TM=0:GOSUB1410:L=L+AX-TS:W=PEEK(L):GOSUB600:POKEL,209:W=160
27 | 270 POKEQK,0:POKEQP,255:GOSUB1240
28 | 280 HP=HP-.15-2*SX:IFHP<0THEN1190
29 | 290 Q=VAL(MID$("808182404142000102",A*2-1,2))-41
30 | 300 IF(PEEK(L+Q)=32)AND(SX<>1)THEN270
31 | 310 IFPEEK(L+Q)=127THEN270
32 | 320 POKEL,W:L=L+Q:W=PEEK(L):POKEL,209:GOSUB600:POKEL,209
33 | 330 IFW=135THENGOSUB1200
34 | 340 IFW>=214ANDW<=219THENGOSUB1000
35 | 350 IFE>0THENS=S+1
36 | 360 IFS>1THENGOSUB830
37 | 370 GOTO270
38 | 380 PRINTINT((TM-TI)/60);CHR$(157);" ";CHR$(145)
39 | 400 W=INT(RND(1)*9+2):L=INT(RND(1)*9+2)
40 | 410 R0=INT(RND(1)*(RS-L-1))+1:C0=INT(RND(1)*(CS-W-1))+1:P=TS+40*R0+C0
41 | 420 IFP+40*L+W>=TS+SZTHEN530
42 | 430 FORN=0TOL+1:FORN1=0TOW+1:IFPEEK(P+(N*40)+N1)<>32THEN530
43 | 440 NEXTN1,N
44 | 450 FORN=1TOL:FORN1=1TOW:POKEP+(N*40)+N1,160:NEXTN1,N
45 | 460 FORN=P+42+(L*40)TOTS+999STEP40
46 | 470 IFPEEK(N)=160THENFORN1=P+42TONSTEP40:POKEN1,160:NEXT:POKEN1-80,102:GOTO490
47 | 480 NEXTN
48 | 490 FORN=P+81+WTOP+121+W:IF(N-TS)/40=INT((N-TS)/40)THEN520
49 | 500 IFPEEK(N)=160THENFORN1=P+81TON-1:POKEN1,160:NEXT:POKEN1-1,102:GOTO520
50 | 510 NEXT
51 | 520 S=INT(RND(1)*L)+1:S1=INT(RND(1)*W+1):POKEP+S1+S*40,INT(RND(1)*6+214)
52 | 530 IFTI160THEN550
55 | 560 POKEU,135:HG=HG+1:NEXT
56 | 570 FORR0=0TORS:POKETS+40*R0,127:POKETS+40*R0+CS,127:NEXTR0
57 | 580 FORC0=0TOCS:POKETS+C0,127:POKETS+C0+40*RS,127:NEXTC0
58 | 590 RETURN
59 | 600 K=-40:J=3:M=40:R=3:GN=0
60 | 610 IFSM=1THENK=-80:J=5:M=80:R=4:SM=0
61 | 620 O=L-32767-R
62 | 630 IFO+32811>33768THENM=0
63 | 640 FORN=-40TO40STEP40:FORN1=1TO3:IFN=0ANDN1=2THEN820
64 | 650 Y=O+N+N1:V=PEEK(Y+TS):POKEY+AX,V
65 | 660 IFV<135ORV=160THEN820
66 | 670 V=V-128:IFV<>7THEN710
67 | 680 K1=1+K1+INT((MG+1)*(RND(1)))
68 | 690 GN=GN+1:IFGN>FGTHENGOSUB1410:PRINT"GOLD IS NEAR!":GOSUB1430:FG=HG+1
69 | 700 GOTO820
70 | 710 V1=V+128:S=0:POKEY+TS,160
71 | 720 IFV=86THENE$="SPIDER":I=3
72 | 730 IFV=87THENE$="GRUE":I=7
73 | 740 IFV=88THENE$="DRAGON":I=1
74 | 750 IFV=89THENE$="SNAKE":I=2
75 | 760 IFV=90THENE$="NUIBUS":I=9
76 | 770 IFV=91THENE$="WYVERN":I=5
77 | 780 I=INT(RND(1)*HP+(PX/I)+HP/4)
78 | 790 IFE>0THENPOKETS+E,QQ
79 | 800 QQ=V+128:E=Y
80 | 810 GOSUB1410:PRINT"A "E$;" WITH";I;"POINTS IS NEAR.":GOSUB1430:CC=I
81 | 820 NEXTN1:NEXTN:FG=GN:RETURN
82 | 830 O1=0:A=0:E1=E+AX:IFABS(E1+40-L)128)THENO1=O1+A
85 | 860 IFABS(E1-1-L)128)THENO1=O1+A
88 | 890 A=O1:IFE1+A=LTHEN960
89 | 900 IFE1+A1THEN1160
109 | 1100 GOSUB1410:W=160:S=0:E=0:POKEL,209:PRINT"THE "E$" IS DEAD!":GOSUB1430
110 | 1110 EX=EX+I:Z=Z+1
111 | 1120 IFEX0THENRETURN
122 | 1230 GOTO1350
123 | 1240 IFIU=0THENGOSUB1430
124 | 1250 GOSUB1340
125 | 1260 IFIUTHENIFTI>TMTHENGOSUB1410:PRINT"YOU MAY MOVE."
126 | 1270 GETL$:IFL$=""THEN1260
127 | 1280 A=ASC(L$):SX=ABS(A>127):A=AAND127
128 | 1290 IFA=ASC("5")THENHP=HP+1+SQR(EX/HP)
129 | 1300 IFA>48ANDA<58THENA=A-48:TM=0:GOSUB1410:RETURN
130 | 1310 IFL$="S"THENSM=1:HP=HP-2
131 | 1320 IFL$="Q"THEN1350
132 | 1330 GOTO1250
133 | 1340 PRINTE2$;"HIT PTS.";INT(HP+.5);CHR$(157);" EXP.";EX;CHR$(157);" GOLD";MG;" ":RETURN
134 | 1350 GOSUB1410:PRINTE2$;"GOLD:";MG;" EXP:";EX;" KILLED";Z;"BEASTS"
135 | 1360 FORN=BLTOSZ-1+BL:A=PEEK(TS+N):POKEAX+N,A:NEXT
136 | 1375 GETL$:IFL$<>""THEN1375
137 | 1380 GOSUB1410:PRINT"WANT TO PLAY AGAIN";
138 | 1390 GOSUB1500:IFL$<>"N"THEN180
139 | 1400 TM=0:GOSUB1410:PRINTCHR$(145);:END
140 | 1410 IFIUTHENIFTI""THEN1550
147 | 1520 IFTI>ZTTHENPRINTMID$("? ",ZC,1);CHR$(157);:ZT=TI+30:ZC=3-ZC
148 | 1530 GOTO1510
149 | 1550 PRINT"? ";L$:RETURN
150 | 60300 PRINTCHR$(147):CLR:GOSUB60400:GOTO100
151 | 60400 QK=525:QM=134:QP=515:CR$=CHR$(13)
152 | 60410 IFPEEK(50000)=0THENRETURN
153 | 60420 QK=158:QM=52:QP=151
154 | 60430 RETURN
155 | 60500 FORI=1TO10:PRINTCHR$(192);CHR$(192);CHR$(192);CHR$(192);:NEXTI:RETURN
156 | 62000 PRINTCHR$(147);CHR$(17);CHR$(17);TAB(9);"CURSOR #";NM$;" ";PG$
157 | 62010 PRINTCHR$(17);" COPYRIGHT (C) 1979 BY BRIAN SAWYER";CHR$(17)
158 | 62020 GOSUB60500
159 | 62030 PRINTCHR$(17);"SEARCH FOR GOLD IN THE ANCIENT RUINS"
160 | 62080 PRINTCHR$(17);CHR$(17);CHR$(17);"PRESS ";CHR$(18);"RETURN";CHR$(146);" TO BEGIN"
161 | 62090 GETT$:IFT$=""THEN62090
162 | 62100 GOTO60300
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | [Aa][Rr][Mm]/
24 | [Aa][Rr][Mm]64/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 |
30 | # Visual Studio 2015/2017 cache/options directory
31 | .vs/
32 | # Uncomment if you have tasks that create the project's static files in wwwroot
33 | #wwwroot/
34 |
35 | # Visual Studio 2017 auto generated files
36 | Generated\ Files/
37 |
38 | # MSTest test Results
39 | [Tt]est[Rr]esult*/
40 | [Bb]uild[Ll]og.*
41 |
42 | # NUNIT
43 | *.VisualState.xml
44 | TestResult.xml
45 |
46 | # Build Results of an ATL Project
47 | [Dd]ebugPS/
48 | [Rr]eleasePS/
49 | dlldata.c
50 |
51 | # Benchmark Results
52 | BenchmarkDotNet.Artifacts/
53 |
54 | # .NET Core
55 | project.lock.json
56 | project.fragment.lock.json
57 | artifacts/
58 |
59 | # StyleCop
60 | StyleCopReport.xml
61 |
62 | # Files built by Visual Studio
63 | *_i.c
64 | *_p.c
65 | *_h.h
66 | *.ilk
67 | *.meta
68 | *.obj
69 | *.iobj
70 | *.pch
71 | *.pdb
72 | *.ipdb
73 | *.pgc
74 | *.pgd
75 | *.rsp
76 | *.sbr
77 | *.tlb
78 | *.tli
79 | *.tlh
80 | *.tmp
81 | *.tmp_proj
82 | *_wpftmp.csproj
83 | *.log
84 | *.vspscc
85 | *.vssscc
86 | .builds
87 | *.pidb
88 | *.svclog
89 | *.scc
90 |
91 | # Chutzpah Test files
92 | _Chutzpah*
93 |
94 | # Visual C++ cache files
95 | ipch/
96 | *.aps
97 | *.ncb
98 | *.opendb
99 | *.opensdf
100 | *.sdf
101 | *.cachefile
102 | *.VC.db
103 | *.VC.VC.opendb
104 |
105 | # Visual Studio profiler
106 | *.psess
107 | *.vsp
108 | *.vspx
109 | *.sap
110 |
111 | # Visual Studio Trace Files
112 | *.e2e
113 |
114 | # TFS 2012 Local Workspace
115 | $tf/
116 |
117 | # Guidance Automation Toolkit
118 | *.gpState
119 |
120 | # ReSharper is a .NET coding add-in
121 | _ReSharper*/
122 | *.[Rr]e[Ss]harper
123 | *.DotSettings.user
124 |
125 | # JustCode is a .NET coding add-in
126 | .JustCode
127 |
128 | # TeamCity is a build add-in
129 | _TeamCity*
130 |
131 | # DotCover is a Code Coverage Tool
132 | *.dotCover
133 |
134 | # AxoCover is a Code Coverage Tool
135 | .axoCover/*
136 | !.axoCover/settings.json
137 |
138 | # Visual Studio code coverage results
139 | *.coverage
140 | *.coveragexml
141 |
142 | # NCrunch
143 | _NCrunch_*
144 | .*crunch*.local.xml
145 | nCrunchTemp_*
146 |
147 | # MightyMoose
148 | *.mm.*
149 | AutoTest.Net/
150 |
151 | # Web workbench (sass)
152 | .sass-cache/
153 |
154 | # Installshield output folder
155 | [Ee]xpress/
156 |
157 | # DocProject is a documentation generator add-in
158 | DocProject/buildhelp/
159 | DocProject/Help/*.HxT
160 | DocProject/Help/*.HxC
161 | DocProject/Help/*.hhc
162 | DocProject/Help/*.hhk
163 | DocProject/Help/*.hhp
164 | DocProject/Help/Html2
165 | DocProject/Help/html
166 |
167 | # Click-Once directory
168 | publish/
169 |
170 | # Publish Web Output
171 | *.[Pp]ublish.xml
172 | *.azurePubxml
173 | # Note: Comment the next line if you want to checkin your web deploy settings,
174 | # but database connection strings (with potential passwords) will be unencrypted
175 | *.pubxml
176 | *.publishproj
177 |
178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
179 | # checkin your Azure Web App publish settings, but sensitive information contained
180 | # in these scripts will be unencrypted
181 | PublishScripts/
182 |
183 | # NuGet Packages
184 | *.nupkg
185 | # The packages folder can be ignored because of Package Restore
186 | **/[Pp]ackages/*
187 | # except build/, which is used as an MSBuild target.
188 | !**/[Pp]ackages/build/
189 | # Uncomment if necessary however generally it will be regenerated when needed
190 | #!**/[Pp]ackages/repositories.config
191 | # NuGet v3's project.json files produces more ignorable files
192 | *.nuget.props
193 | *.nuget.targets
194 |
195 | # Microsoft Azure Build Output
196 | csx/
197 | *.build.csdef
198 |
199 | # Microsoft Azure Emulator
200 | ecf/
201 | rcf/
202 |
203 | # Windows Store app package directories and files
204 | AppPackages/
205 | BundleArtifacts/
206 | Package.StoreAssociation.xml
207 | _pkginfo.txt
208 | *.appx
209 |
210 | # Visual Studio cache files
211 | # files ending in .cache can be ignored
212 | *.[Cc]ache
213 | # but keep track of directories ending in .cache
214 | !?*.[Cc]ache/
215 |
216 | # Others
217 | ClientBin/
218 | ~$*
219 | *~
220 | *.dbmdl
221 | *.dbproj.schemaview
222 | *.jfm
223 | *.pfx
224 | *.publishsettings
225 | orleans.codegen.cs
226 |
227 | # Including strong name files can present a security risk
228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
229 | #*.snk
230 |
231 | # Since there are multiple workflows, uncomment next line to ignore bower_components
232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
233 | #bower_components/
234 |
235 | # RIA/Silverlight projects
236 | Generated_Code/
237 |
238 | # Backup & report files from converting an old project file
239 | # to a newer Visual Studio version. Backup files are not needed,
240 | # because we have git ;-)
241 | _UpgradeReport_Files/
242 | Backup*/
243 | UpgradeLog*.XML
244 | UpgradeLog*.htm
245 | ServiceFabricBackup/
246 | *.rptproj.bak
247 |
248 | # SQL Server files
249 | *.mdf
250 | *.ldf
251 | *.ndf
252 |
253 | # Business Intelligence projects
254 | *.rdl.data
255 | *.bim.layout
256 | *.bim_*.settings
257 | *.rptproj.rsuser
258 | *- Backup*.rdl
259 |
260 | # Microsoft Fakes
261 | FakesAssemblies/
262 |
263 | # GhostDoc plugin setting file
264 | *.GhostDoc.xml
265 |
266 | # Node.js Tools for Visual Studio
267 | .ntvs_analysis.dat
268 | node_modules/
269 |
270 | # Visual Studio 6 build log
271 | *.plg
272 |
273 | # Visual Studio 6 workspace options file
274 | *.opt
275 |
276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
277 | *.vbw
278 |
279 | # Visual Studio LightSwitch build output
280 | **/*.HTMLClient/GeneratedArtifacts
281 | **/*.DesktopClient/GeneratedArtifacts
282 | **/*.DesktopClient/ModelManifest.xml
283 | **/*.Server/GeneratedArtifacts
284 | **/*.Server/ModelManifest.xml
285 | _Pvt_Extensions
286 |
287 | # Paket dependency manager
288 | .paket/paket.exe
289 | paket-files/
290 |
291 | # FAKE - F# Make
292 | .fake/
293 |
294 | # JetBrains Rider
295 | .idea/
296 | *.sln.iml
297 |
298 | # CodeRush personal settings
299 | .cr/personal
300 |
301 | # Python Tools for Visual Studio (PTVS)
302 | __pycache__/
303 | *.pyc
304 |
305 | # Cake - Uncomment if you are using it
306 | # tools/**
307 | # !tools/packages.config
308 |
309 | # Tabs Studio
310 | *.tss
311 |
312 | # Telerik's JustMock configuration file
313 | *.jmconfig
314 |
315 | # BizTalk build output
316 | *.btp.cs
317 | *.btm.cs
318 | *.odx.cs
319 | *.xsd.cs
320 |
321 | # OpenCover UI analysis results
322 | OpenCover/
323 |
324 | # Azure Stream Analytics local run output
325 | ASALocalRun/
326 |
327 | # MSBuild Binary and Structured Log
328 | *.binlog
329 |
330 | # NVidia Nsight GPU debugger configuration file
331 | *.nvuser
332 |
333 | # MFractors (Xamarin productivity tool) working folder
334 | .mfractor/
335 |
336 | # Local History for Visual Studio
337 | .localhistory/
338 |
339 | # BeatPulse healthcheck temp database
340 | healthchecksdb
--------------------------------------------------------------------------------
/dungeon-memory-sim.py:
--------------------------------------------------------------------------------
1 | # Simulating what goes on in the memory structure
2 | # of DUNGEON
3 |
4 | from random import random, randint, choice
5 | import sys
6 | from os import system, name
7 |
8 | WIDTH = 40
9 | HEIGHT = 25
10 | RM_GEN_RETRIES = 50
11 | DESIRED_RMS = 8
12 | ASCII_a = ord("a")
13 | t_vars = {}
14 | MONSTERS = ["R","G","D","S","N","W"]
15 | FLOOR = "."
16 | DOOR = "+"
17 | GOLD = "g"
18 | BORDER = "*"
19 | RS=23; CS=40; SZ=RS*CS; BL=(25-RS)*40 ; RS=RS-1; CS=CS-1
20 |
21 | def draw_memory(data, vars_dict, step=""):
22 | cls() # Works only if run from true terminal, not from IDE
23 |
24 | # Draw the board data structure
25 | margin = " "
26 | tens_line = margin + " " # Initial space for left side of board
27 | for i in range(1,(WIDTH//10)):
28 | tens_line += (" " * 9) + str(i)
29 | digits_line = margin + ("0123456789" * (WIDTH//10))
30 | if step != "":
31 | print("=== {} ===".format(step))
32 | print(tens_line)
33 | print(digits_line)
34 |
35 | for row in range(HEIGHT):
36 | # single digit numbers are padded with extra space
37 | if row < 10:
38 | extra_space = " "
39 | else:
40 | extra_space = ""
41 |
42 | print("{}{}| ".format(extra_space, row), end="")
43 | for col in range(WIDTH):
44 | print(data[row][col], end="")
45 | print(" |{}{}".format(extra_space,row))
46 | print(digits_line)
47 | print(tens_line)
48 | get_continue(vars_dict)
49 |
50 |
51 | def init_memory():
52 | # Create new data structure
53 | # Returns: list of [WIDTH][HEIGHT] elements
54 | board = []
55 | for row in range(HEIGHT):
56 | board.append([])
57 | for col in range(WIDTH):
58 | board[row].append("~")
59 |
60 | return board
61 |
62 |
63 | def get_continue(vars_dict):
64 | var_str = ""
65 | for k,v in vars_dict.items():
66 | var_str += "{}={}, ".format(k,v)
67 | print(var_str)
68 | input("Press return to continue...")
69 |
70 |
71 | def POKE(memory, location, value):
72 | memory[(location//WIDTH)][(location%WIDTH)] = value
73 |
74 |
75 | def PEEK(memory, location):
76 | return memory[(location//WIDTH)][(location%WIDTH)]
77 |
78 |
79 | def cls():
80 | if name == "nt":
81 | _ = system("cls")
82 | else:
83 | _ = system("clear")
84 |
85 |
86 | def gen_room_loc(TS):
87 | W = int(random()*9+2); L = int(random()*9+2)
88 | R0=int(random()*(RS-L-1))+1; C0=int(random()*(CS-W-1))+1; P=TS+40*R0+C0
89 | return W, L, R0, C0, P
90 |
91 |
92 | def gen_dungeon(TS, SZ):
93 | # We return a list data structure that represents a map of 40 columns,
94 | # 25 rows. The dungeon is generated inside this structure and serves
95 | # to feed what will be seen on the screen. In a sense, we'll have two
96 | # structures - the full map, and then the map that the player reveals
97 | # and what gets painted to the screen.
98 | mem = []
99 | mem = init_memory()
100 |
101 | # 200 FOR I= TS + 40 TO TS + SZ-41:POKEI,32:NEXTI
102 | for i in range(TS+40, (TS+SZ-41)+1):
103 | # POKE treats this as a continuous area of data, not divided
104 | # into an x,y list like we have here. So, create a function POKEds that will
105 | # take a number, and then map to our row/col coordinate system
106 | # Also, BASIC FOR is inclusive of start, stop, so we have to add 1
107 | # because Python for is exclusive of stop
108 | POKE(mem, i, " ")
109 | # t_vars["i"] = i
110 |
111 | # draw_memory(mem, t_vars, "INIT")
112 |
113 | retries = 0
114 | rooms_generated = 0
115 | # Keep generating rooms until we've hit a limit of DESIRED_RMS rooms or we've maxed out with
116 | # retries of so many times (RM_GEN_RETRIES) .
117 | while rooms_generated < DESIRED_RMS and retries < RM_GEN_RETRIES:
118 | W, L, R0, C0, P = gen_room_loc(TS)
119 |
120 | # 400 W=INT(RND(1)*9+2):L=INT(RND(1)*9+2)
121 | # 410 R0=INT(RND(1)*(RS-L-1))+1:C0=INT(RND(1)*(CS-W-1))+1:P=TS+40*R0+C0
122 | # 420 IFP+40*L+W>=TS+SZTHEN530
123 |
124 | # This was a check to see if the room in the data structure would go over
125 | # the boundaries (end point) of the area of memory allocated. (TS+SZ)
126 | if P+40*L+W >= TS+SZ:
127 | fail_on_size += 1
128 | continue
129 |
130 | # t_vars["W"] = W; t_vars["L"] = L
131 | # t_vars["R0"] = R0; t_vars["C0"] = C0; t_vars["P"] = P
132 |
133 | #430 FORN=0TOL+1:FORN1=0TOW+1:IFPEEK(P+(N*40)+N1)<>32THEN530
134 | #440 NEXTN1,N
135 | # This looks to see if the room overlaps other rooms.
136 | # I know the -1 looks strange, but the game should keep rooms at least 1
137 | # space apart. Since I'm 0 index and BASIC was 1 indexed, this is how
138 | # it will work.
139 | failed_check = False
140 | for N in range(-1,(L+1)+1):
141 | for N1 in range(-1, (W+1)+1):
142 | if PEEK(mem, P+(N*40+N1)) != " ":
143 | failed_check = True
144 | break
145 |
146 | if failed_check:
147 | retries += 1
148 | break
149 |
150 | if not failed_check:
151 | #450 FORN=1TOL:FORN1=1TOW:POKEP+(N*40)+N1,160:NEXTN1,N
152 | # We have a good room!
153 | # This fills the room space. For now, I'll use # to denote room space.
154 | rooms_generated += 1
155 | for N in range(0,L+1):
156 | for N1 in range(0, W+1):
157 | POKE(mem, P+(N*40+N1), FLOOR)
158 |
159 | # Generate vertical passages from generated room down.
160 | # 460 FOR N=P+42+(L*40) TO TS+999 STEP 40
161 | # 470 IFPEEK(N)=160 THEN FOR N1=P+42 TO N STEP 40:POKE N1,160 : NEXT: POKEN1-80,102: GOTO490
162 | # 480 NEXT N
163 | for N in range(P+42+(L*40), TS+999+1, 40):
164 | if PEEK(mem,N) == FLOOR:
165 | for N1 in range(P+42, N+1, 40):
166 | POKE(mem, N1, FLOOR)
167 | # I think this used to be N1-80 because FOR NEXT in PET would increment, check
168 | # and leave the value at the incremented, which meant you'd have to back up
169 | # two rows. It seems that Python doesn't do that, so if I only back up 40, the
170 | # door symbol (in memory map, remember) is in the right spot.
171 | POKE(mem, N1-40, DOOR)
172 | break
173 |
174 | # draw_memory(mem, t_vars, "END VPASS GEN")
175 |
176 | # Generate horizontal passages from generated room right.
177 | # 490 FOR N= P+81+W TO P+121+W:IF(N-TS)/40=INT((N-TS)/40)THEN520
178 | # N = 32303 + 81 + 6 (32390) TO 32303 + 121 + 6(32430)
179 | # IF (32390-31848)/40 = INT((32390-31848)/40) then no more passage/break;
180 | # 500 IFPEEK(N)=160THENFORN1=P+81TON-1:POKEN1,160:NEXT:POKEN1-1,102:GOTO520
181 | # 510 NEXT
182 | start = P+81+W; end = P+121+W
183 | for N in range(start, end+1):
184 | if (N-TS)/40 == int((N-TS)/40):
185 | break
186 |
187 | if PEEK(mem, N) == FLOOR:
188 | for N1 in range(P+81, (N-1)+1):
189 | POKE(mem, N1, FLOOR)
190 | POKE(mem, N1, DOOR)
191 | break
192 |
193 | # draw_memory(mem, t_vars, "END HPASS GEN")
194 | # Generate a monster in the room. Every room has a monster!
195 | # 520 S=INT(RND(1)*L)+1:S1=INT(RND(1)*W+1):POKEP+S1+S*40,INT(RND(1)*6+214)
196 | S = int(random()*L)+1; S1 = int(random()*W+1)
197 | POKE(mem, P+S1+S*40, choice(MONSTERS))
198 |
199 | # Distribute 11 gold around the dungeon
200 | # 540 FORN=1TO11
201 | # 550 U=INT(RND(1)*SZ)+TS:IFPEEK(U)<>160THEN550
202 | # 560 POKEU,135:HG=HG+1:NEXT
203 | for N in range(0,11):
204 | is_room = False
205 | while not is_room:
206 | U = int(random()*SZ)+TS
207 | is_room = (PEEK(mem,U) == FLOOR)
208 | POKE(mem, U, GOLD)
209 | # HG +=1
210 |
211 | # Generate borders (I think)
212 | # 570 FORR0=0TORS:POKETS+40*R0,127:POKETS+40*R0+CS,127:NEXTR0
213 | # 580 FORC0=0TOCS:POKETS+C0,127:POKETS+C0+40*RS,127:NEXTC0
214 | for R0 in range(0,RS+1):
215 | POKE(mem, TS+40*R0, BORDER)
216 | POKE(mem, TS+40*R0+CS, BORDER)
217 |
218 | for C0 in range(0, CS+1):
219 | POKE(mem, TS+C0, BORDER)
220 | POKE(mem, TS+C0+40*RS, BORDER)
221 |
222 | return mem
223 |
224 | # Simulating the values from Dungeon
225 | # 100 RS=23: CS=40: SZ=RS*CS: BL=(25-RS)*40:RS=RS-1:CS=CS-1
226 | # SZ = 920 - the number of spaces in a 23*40 data space
227 |
228 |
229 | #t_vars["RS"] = RS
230 | #t_vars["CS"] = CS
231 | #t_vars["SZ"] = SZ
232 | #t_vars["BL"] = BL
233 |
234 | # 190 TS=PEEK(QM)+256*PEEK(QM+1)-SZ:AX=32768
235 | TS = 0 # In PET/Dungeon, this is 31848
236 | # but it doesn't matter here.
237 | # We'll assume the starting point is 0
238 |
239 | AX = TS + 920 # Since TS and AX start off at the same point,
240 | # and then TS is backed off by 920, we'll reverse
241 | # that for now.
242 |
243 | # In PETs, 32768 was start of screen RAM! POKEing to this location put
244 | # something at 0 row, 0 col on the screen!
245 |
246 | # t_vars["TS"] = TS
247 | # t_vars["AX"] = AX
248 |
249 | # 210 HP=50:MG=0:EX=0:PX=0:HG=0:Z=0:FG=0:K1=0:E=0:S=0:W=160:ET=160
250 |
251 | # For giggles/grins, generate a whole bunch of maps
252 | for dngmap in range(1,11):
253 | dungeon_map = gen_dungeon(TS, SZ)
254 | draw_memory(dungeon_map, t_vars, "MAP #{} GENERATED".format(dngmap))
255 |
256 |
--------------------------------------------------------------------------------
/cursor15-dungeon-annotated-basic.txt:
--------------------------------------------------------------------------------
1 | 0 POKE59468,12 # set computer into graphics mode
2 | 1 REM DUNGEON COPYRIGHT (C) 1979 BRIAN SAWYER
3 | 2 REM 1310 DOVER HILL ROAD
4 | 3 REM SANTA BARBARA, CA. 93103
5 | 4 :
6 | 5 REM CURSOR #15, NOV/DEC 1979
7 | 6 REM BOX 550, GOLETA, CA. 93017
8 | 7 REM LINES 61000-65000 (C) 1979 CURSOR MAGAZINE
9 |
10 | # display related - program name for std CURSOR display
11 | # Cursor issue #
12 | 90 PG$="DUNGEON":NM$="15":GOSUB62000
13 |
14 | ==> GOSUB 62000
15 | # Print mag info
16 | ====> GOSUB 60500
17 | # print a line of 40 graphic chars (CHR$192)
18 | <==== RETURN TO 62030
19 | ++++> GOTO 60300
20 | # Clear variables and screen
21 | ======> GOSUB 60400
22 | # Determine which version of BASIC we have and set key vars
23 | # to get sys related info later on.
24 | <====== RETURN TO 60300
25 | ++++> GOTO 100
26 | # What happens to the stack w/the original gosub back in line 90?
27 | # The CLR in 60300 zaps the stack. So here we are...
28 |
29 | # Key variables used for screen size and amount of memory needed
30 | # for the in-memory dungeon map.
31 | 100 RS=23: CS=40: SZ=RS*CS: BL=(25-RS)*40:RS=RS-1:CS=CS-1
32 | $ SZ will equal 920
33 | $ BL = 80
34 | $ RS = 22
35 | $ CS = 39
36 |
37 | # In PET, strings are stored at end of memory and pointers are stored lower
38 | # (closer to beginning) to those strings.
39 | # Brian intends on storing the in-memory map as far down in memory as he
40 | # can.
41 | # Not quite sure why he did this... as the "end of memory" is set
42 | # later on line 200. Maybe he forgot he did one or the other?
43 | 130 REM TRICK: DG$() STRINGS GO AT END OF MEMORY!
44 | 140 DIMDG$(24):E$=" "
45 | 150 FORI=0TO24:DG$(I)=E$+"":NEXTI
46 |
47 | # Setting up 2 status bars lines and set screen.
48 | 160 ER$="{HOME}{DOWN} {HOME}{DOWN}"
49 | 170 E2$="{HOME} {HOME}"
50 | 180 PRINT"{CLEAR}SETTING UP..."
51 |
52 | # AX is assumed screen ram location
53 | # This seems to look at the point for "Top of memory" which is also
54 | # where screen ram starts, then backs off 920.
55 | # running this on emulator gives me 31848
56 | 190 TS=PEEK(QM)+256*PEEK(QM+1)-SZ:AX=32768
57 | $ TS=PEEK(52)+256*PEEK(52+1)-920:AX=32768
58 | $ TS = 31848
59 |
60 | # Putting spaces into this memory - perhaps to clear the in-mem map?
61 | 200 FOR I= TS + 40 TO TS + SZ-41:POKEI,32:NEXTI
62 | $ FOR I= 31848 + 40 TO 31848 + 920 - 41:POKEI,32:NEXTI
63 |
64 | 210 HP=50:MG=0:EX=0:PX=0:HG=0:Z=0:FG=0:K1=0:E=0:S=0:W=160:ET=160
65 | #* HP is player hit points
66 | #* MG is gold recovered
67 | #* EX is experience earned
68 | #* HG is hidden gold (max of 11)
69 | #* Z is number of monsters killed
70 | #* W is for what was in the space that the player just moved
71 | #* K1 is how much gold has been uncovered
72 | #* FG is found gold flag - used for showing "Gold is near" text
73 | # E is current monster location
74 | # PX is value needed for player to level up
75 | # S is a delay counter - if the player just spotted the monster,
76 | # then this is used elsewhere to give them a chance to retreat!
77 | # ET is used to hold what the monster moves over.
78 | # There are other values sprinkled around that are initialized and
79 | # used globally later, mostly for monster stats.
80 |
81 | # 220 is related to timer and how long of a pause for printing messages or
82 | # getting input. TI was a magic BASIC variable that gave number of "jiffies"
83 | # See Commodore documentation on that lovely concept.
84 | 220 TI$="000000":TM=TI+3600
85 |
86 | # Build the Dungeon!!
87 | 230 GOSUB 380
88 |
89 | ==>380
90 | Generate the dungeon
91 | <== RETURN FROM 380 <==
92 |
93 | 240 TS = TS - BL
94 | $ 31848 = 31848 - 80
95 | $ TS = 31768 # Now 1000 away from AX/32768
96 |
97 | #* Where to put the player? L is (L)ocation
98 | 250 L=INT(RND(1)*SZ+TS):IFPEEK(L)<>160THEN250
99 |
100 | #* Put player onto the screen and reset W to be 160
101 | 260 TM=0:GOSUB1410:L=L+AX-TS:W=PEEK(L):GOSUB600:POKEL,209:W=160
102 | # AX is start of screen mem.
103 | # Look at the screen/player map to see what is at L.
104 | # W is what is on the map prior to 'gosub 600', which is the player map
105 | # display/movement loop. Once we've done all that, put the player
106 | # token down, which is a dot (but ends up being a reversed dot?)
107 | # and then W will be 160 (floor) again.
108 |
109 | ==> GOSUB 1410
110 | Timer to slow things down
111 | <== RETURN FROM 1410
112 |
113 | ==> GOSUB 600
114 | # Compute effect of putting player on screen... what can they see around
115 | # them initially?
116 | <== RETURN from 600 back to 260
117 |
118 | #* Start of input loop
119 | # These two pokes set the keyboard buffer count to 0
120 | # and set the "key pressed" to be nothing (FF)
121 | 270 POKEQK,0:POKEQP,255:GOSUB1240
122 | $ QK = 158
123 | $ QP = 151
124 |
125 | ==> GOSUB 1240
126 | GET PLAYER MOVE, direction move stored in A. Also, SX might be set to 1
127 | SX is "shift mode", allowing players to move through the blank spaces
128 | <== RETURN FROM 1240
129 |
130 | #* Movement costs HP! Moreso if moving shifted through the dark spaces
131 | # If HP<0, then you're dead! 1190 is death sequence.
132 | 280 HP=HP-.15-2*SX:IFHP<0THEN1190
133 |
134 | #* Are we moving? If so, are we moving into a space between floors? If so,
135 | # are we in shift mode? If not, ignore the entry.
136 | # Did we hit an impassable border (around the map)?
137 | # If so, ignore the entry
138 | # going back to 270 is back to beginning of input loop
139 | # Here's the deal about line 290
140 | # It takes your 1-9 input and through this little trick, converts it
141 | # into the number of memory positions you're moving. If we go back to thinking
142 | # of this as a grid 40cols x 25rows display - then hitting 6 moves you right (+1)
143 | # position. Hitting 1 (diagonal down/left) moves you +39 positions. It's very
144 | # clever!
145 | 290 Q=VAL(MID$("808182404142000102",A*2-1,2))-41
146 | 300 IF(PEEK(L+Q)=32)AND(SX<>1)THEN270
147 | 310 IFPEEK(L+Q)=127THEN270
148 |
149 | #*Move the player
150 | 320 POKEL,W:L=L+Q:W=PEEK(L):POKEL,209:GOSUB600:POKEL,209
151 |
152 | ==> GOSUB 600
153 | # What does the player see? Activate the last monster revealed,
154 | # set up the random gold value to be found.
155 | <== RETURN FROM 600
156 |
157 | #* If we found gold..
158 | 330 IFW=135THENGOSUB1200
159 |
160 | ==> GOSUB 1200
161 | # We found gold sub
162 | <== RETURN 1200 to 330
163 |
164 | # If we move onto a monster - attack routine!
165 | 340 IFW>=214ANDW<=219THENGOSUB1000
166 |
167 | ==> GOSUB 1000
168 | # A fight! Who will win?
169 | <== RETURN FROM 1000
170 |
171 | # If there's an active monster (E is set w/location)
172 | # then increment S, and if S is > 1, move the monster.
173 | # This gives the player time to escape. If they start out
174 | # next to the monster, S is 0. E is set to monster location.
175 | # This code will run, S increments to one, S>1 is false, so the
176 | # game loop continues to the next player move. THEN, with monster
177 | # still on screen, E>0, S will increment, then the condition is met
178 | # and the monster will move.
179 | 350 IFE>0THENS=S+1
180 | 360 IFS>1THENGOSUB830
181 |
182 | ==> GOSUB 830
183 | # Finally, the monsters move, if they've seen the player...
184 | <== RETURN FROM 830
185 |
186 | # Continue game!
187 | 370 GOTO270
188 | # == End of game loop
189 |
190 |
191 | #* SUB - Build the dungeon
192 | 380 PRINT INT((TM-TI)/60);"{LEFT} {UP}" #print the countdown
193 |
194 | #* Generate width/length of a room
195 | 400 W=INT(RND(1)*9+2):L=INT(RND(1)*9+2)
196 | #* RND(1) = 0.999 - .999 * 9 + 2 = 10 - so rooms no more than 10x10? perhaps?
197 |
198 | # Calculate where on map room will be
199 | 410 R0=INT(RND(1)*(RS-L-1))+1:C0=INT(RND(1)*(CS-W-1))+1:P=TS+40*R0+C0
200 | #* RS is 22, CS is 39
201 | # example R0 = 11, C0 = 15, P=31848+40*R0+C0
202 | $ R0 = 11
203 | $ C0 = 15
204 | $ P = 32303
205 |
206 | # If room size will take us beyond map limit, don't use this room
207 | 420 IFP+40*L+W>=TS+SZTHEN530
208 | # IF 32303+40*4+6 >= TS+SZ THEN (GOTO) 530
209 |
210 | # The GOTO 530 checks to see if we've run out of time
211 | # to build the dungeon. So the computer will keep
212 | # building until the countdown is over!
213 |
214 | #* This checks to see if this space is taken up. If it is, then we goto 530...
215 | 430 FORN=0TOL+1:FORN1=0TOW+1:IFPEEK(P+(N*40)+N1)<>32THEN530
216 | 440 NEXTN1,N
217 |
218 | #* This puts char160, which is a filled in solid block, into the room area.
219 | 450 FORN=1TOL:FORN1=1TOW:POKEP+(N*40)+N1,160:NEXTN1,N
220 |
221 | #* Generate vertical passages from generated room down.
222 | 460 FORN=P+42+(L*40)TOTS+999STEP40
223 | # FOR N = (P=start of room in memory) + 42 + (Length*40) to (TS = 31848)+999=32847 - Step every 40
224 | 470 IFPEEK(N)=160THENFORN1=P+42TONSTEP40:POKEN1,160:NEXT:POKEN1-80,102:GOTO490
225 | 480 NEXTN
226 |
227 | #* Generate horizontal passages from generate room right.
228 | 490 FORN=P+81+WTOP+121+W:IF(N-TS)/40=INT((N-TS)/40)THEN520
229 | 500 IFPEEK(N)=160THENFORN1=P+81TON-1:POKEN1,160:NEXT:POKEN1-1,102:GOTO520
230 | 510 NEXT
231 |
232 | #* Generate a monster in the room. Every room has a monster!
233 | 520 S=INT(RND(1)*L)+1:S1=INT(RND(1)*W+1):POKEP+S1+S*40,INT(RND(1)*6+214)
234 |
235 | # Timer check - if we've enough time, continue building rooms.
236 | 530 IFTI160THEN550
241 | 560 POKEU,135:HG=HG+1:NEXT
242 |
243 | #* Generate borders
244 | 570 FORR0=0TORS:POKETS+40*R0,127:POKETS+40*R0+CS,127:NEXTR0
245 | 580 FORC0=0TOCS:POKETS+C0,127:POKETS+C0+40*RS,127:NEXTC0
246 | 590 RETURN
247 | # === END SUB Generate dungeon
248 |
249 |
250 | # SUB == evaluate player move and what they see as a result
251 | # NOTE: K, J, M are not used! I think they're leftovers from when perhaps
252 | # Brian intended on implementing SM mode / SEE further mode!
253 | # That's why S costs HP - you were then supposed to see 2 squares in each direction.
254 | # He just never finished it, apparently.
255 | 600 K=-40:J=3:M=40:R=3:GN=0
256 | 610 IFSM=1THENK=-80:J=5:M=80:R=4:SM=0
257 |
258 | # O is meant to give the location to view from, taking into account where memory stops/starts.
259 | 620 O=L-32767-R
260 | 630 IFO+32811>33768THENM=0
261 |
262 | # Look at what is around from the dungeon map and put it on the screen
263 | 640 FORN=-40TO40STEP40:FORN1=1TO3:IFN=0ANDN1=2THEN820
264 | 650 Y=O+N+N1:V=PEEK(Y+TS):POKEY+AX,V
265 |
266 | # If we've only revealed a floor or space, continue with the N/N1 loop
267 | 660 IFV<135ORV=160THEN820
268 |
269 | # If we've revealed a monster, then go to that code (710)
270 | 670 V=V-128:IFV<>7THEN710
271 |
272 | # We've revealed gold - how much? Here's a funny by-product of this. Because we redo
273 | # this "what's seen" code, each time we repaint the gold, we recalculate K1! So it's
274 | # truly random as to what we might find, and that value will always change! Later on,
275 | # we use the latest value of K1 when we actually touch gold. (and repaint the screen.)
276 | # Cute and clever, Brian... you don't have to track the amount when you see it. Just
277 | # use the last value...
278 | 680 K1=1+K1+INT((MG+1)*(RND(1)))
279 | # GN and FG are about how often the message "GOLD IS NEAR" is displayed.
280 | # It's an odd way to set up a "we've seen this already" True/False flag.
281 | 690 GN=GN+1:IFGN>FGTHENGOSUB1410:PRINT"GOLD IS NEAR!":GOSUB1430:FG=HG+1
282 | 700 GOTO820
283 |
284 | # Revealed a monster - put a floor in its place on the dungeon map
285 | 710 V1=V+128:S=0:POKEY+TS,160
286 | # V is the character representing monster type. I is its HP.
287 | 720 IFV=86THENE$="SPIDER":I=3
288 | 730 IFV=87THENE$="GRUE":I=7
289 | 740 IFV=88THENE$="DRAGON":I=1
290 | 750 IFV=89THENE$="SNAKE":I=2
291 | 760 IFV=90THENE$="NUIBUS":I=9
292 | 770 IFV=91THENE$="WYVERN":I=5
293 | #Generate monster hp
294 | 780 I=INT(RND(1)*{Player HP}+(PX/I)+{Player HP}/4)
295 | # if we've generated a previous monster, E holds its position. QQ holds
296 | # what type of monster it is. Put it back on the dungeon map.
297 | # This means that only one monster at a time can be 'active'
298 | 790 IFE>0THENPOKETS+E,QQ
299 | 800 QQ=V+128:E=Y
300 | 810 GOSUB1410:PRINT"A "E$;" WITH";I;"POINTS IS NEAR.":GOSUB1430:CC=I
301 | 820 NEXTN1:NEXTN:FG=GN:RETURN
302 | # === END move/what do you see sub
303 |
304 |
305 | # SUB == Monster reaction/movement AI
306 | # The math here is a little baffling and there's a bug. The
307 | # bottom line is that it looks at player position vs.
308 | # monster position and tries to calculate the best move.
309 | # It's tracking itself across the screen, since it's been
310 | # "removed" from the dungeon memory map.
311 | 830 O1=0:A=0:E1=E+AX:IFABS(E1+40-L)128)THENO1=O1+A
314 | 860 IFABS(E1-1-L)128)THENO1=O1+A
317 | 890 A=O1:IFE1+A=LTHEN960
318 | # The following lines 900-950 baffle me. I'm not sure if Brian
319 | # programmed in some dumbness, but the overall math is wonky and
320 | # doesn't quite work. This is probably one of the areas I'd like to
321 | # "improve" at some point.
322 | 900 IFE1+A GOSUB 1500
355 | # Input routine - makes ? blink, puts value in L$
356 | <== RETURN FROM 1500
357 |
358 | #* If yes, then monster disappears!
359 | 1080 IFL$="Y"THENMG=MG-MG/2:W=160:E=0:S=0:POKEL,209:RETURN
360 |
361 | #* if monster is still alive.
362 | 1090 IFCC>1THEN1160
363 |
364 | #* if monsters is dead
365 | 1100 GOSUB1410:W=160:S=0:E=0:POKEL,209:PRINT"THE "E$" IS DEAD!":GOSUB1430
366 | # Monsters HP is experience reward. Increment monster kill counter.
367 | 1110 EX=EX+I:Z=Z+1
368 | # If we haven't doubled our experience, then return
369 | 1120 IFEX0THENRETURN
394 | 1230 GOTO1350
395 |
396 |
397 | #* SUB - Get player move
398 | 1240 IFIU=0THENGOSUB1430
399 |
400 | ==> GOSUB 1430
401 | 1430 TM=TI+3*60:IU=1:RETURN
402 | <== RETURN from 1430
403 |
404 | 1250 GOSUB1340
405 |
406 | == > 1340
407 | #* Refresh status line
408 | 1340 PRINTE2$;"HIT PTS.";INT({Player HP}+.5);"{LEFT} EXP.";EX;"{LEFT} GOLD";MG;" ":RETURN
409 | <== RETURN FROM 1340
410 |
411 | 1260 IF IU THEN IF TI>TM THEN GOSUB1410: PRINT"YOU MAY MOVE."
412 |
413 | #* Get player input - set it to a known value
414 | # What this does is to convert values into the < 127 range.
415 | # If a player is moving "shifted" - they can go through walls.
416 | # but it will cost them HP (elsewhere).
417 | 1270 GETL$:IFL$=""THEN1260
418 | 1280 A=ASC(L$):SX=ABS(A>127):A=AAND127
419 |
420 | #* If waiting in place regain {Player HP}
421 | 1290 IFA=ASC("5")THEN{Player HP}={Player HP}+1+SQR(EX/{Player HP})
422 |
423 | #* player can move on keypad in any direction. A holds the value of their move
424 | # converted to 123456789
425 | 1300 IFA>48ANDA<58THENA=A-48:TM=0:GOSUB1410:RETURN
426 |
427 | #* The not-quite-finished "See More" mode, if you hit S
428 | 1310 IFL$="S"THENSM=1:{Player HP}={Player HP}-2
429 |
430 | #*quit
431 | 1320 IFL$="Q"THEN1350
432 | 1330 GOTO1250
433 | #=== End of get player move sub
434 |
435 |
436 | #* SUB - Refresh status line 1
437 | 1340 PRINTE2$;"HIT PTS.";INT({Player HP}+.5);"{LEFT} EXP.";EX;"{LEFT} GOLD";MG;" ":RETURN
438 |
439 |
440 | #* SUB - Display game totals
441 | 1350 GOSUB1410:PRINTE2$;"GOLD:";MG;" EXP:";EX;" KILLED";Z;"BEASTS"
442 |
443 |
444 | #* SUB - This routine displays the map and asks if player wants to go again
445 | 1360 FORN=BLTOSZ-1+BL:A=PEEK(TS+N):POKEAX+N,A:NEXT
446 | 1375 GETL$:IFL$<>""THEN1375
447 | 1380 GOSUB1410:PRINT"WANT TO PLAY AGAIN";
448 | 1390 GOSUB1500:IFL$<>"N"THEN180
449 | 1400 TM=0:GOSUB1410:PRINT"{UP}";:END
450 |
451 |
452 | # SUB - Input delay. I think about a second or so.
453 | 1410 IF IU THEN IF TI""THEN1550
471 | 1520 IFTI>ZTTHENPRINTMID$("? ",ZC,1);"{LEFT}";:ZT=TI+30:ZC=3-ZC
472 | 1530 GOTO1510
473 | 1550 PRINT"? ";L$:RETURN
474 |
475 |
476 | #* SUB - Clear screen - reset all variables - Start game (goto 100)
477 | 60300 PRINT"{CLEAR}":CLR:GOSUB60400:GOTO100
478 |
479 |
480 | #* SUB - Which BASIC do I have? Set vars that will be used by various
481 | # PEEKS/POKES that are system related.
482 | # QM is the "top of memory" - when it ends
483 | # QK is the num of chars in keyboard buffer
484 | # QP is the location that says what key is pressed.
485 | # 134 = 0x86, 52 = 0x34
486 | # Assume Basic 1
487 | 60400 QK=525:QM=134:QP=515:CR$=CHR$(13)
488 | 60410 IFPEEK(50000)=0THENRETURN # This check confirms BASIC 1 - not sure how/why but there you have it.
489 | 60420 QK=158:QM=52:QP=151 #Not Basic 1, these are Basic 2/4 values
490 | 60430 RETURN
491 |
492 |
493 | #* SUB print 40 "lines" (CHR$(192) apparently)
494 | 60500 FORI=1TO10:PRINT"{192}{192}{192}{192}";:NEXTI:RETURN
495 |
496 |
497 | #* SUB - Print intro
498 | 62000 PRINT"{CLEAR}{DOWN}{DOWN}";TAB(9);"CURSOR #";NM$;" ";PG$
499 | 62010 PRINT"{DOWN} COPYRIGHT (C) 1979 BY BRIAN SAWYER{DOWN}"
500 | 62020 GOSUB60500 # Print a line across the screen
501 | 62030 PRINT"{DOWN}SEARCH FOR GOLD IN THE ANCIENT RUINS"
502 | 62080 PRINT"{DOWN}{DOWN}{DOWN}PRESS {RVS ON}RETURN{RVS OFF} TO BEGIN"
503 | 62090 GETT$:IFT$=""THEN62090
504 | 62100 GOTO60300
--------------------------------------------------------------------------------
/pydungeon.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | # Recreation of DUNGEON, a game written by Brian Sawyer for Cursor Magazine,
3 | # issue #15. Published in Dec 1979.
4 | # Michael Shorten, June 2020, Public Domain, I have no rights to original
5 | # source. I am doing this for educational purposes to demonstrate computer
6 | # history and what it might look like in modern programming.
7 |
8 | from random import random, randint, choice
9 | import sys
10 | from time import sleep
11 | from math import sqrt
12 | import curses
13 |
14 | WIDTH = 40
15 | HEIGHT = 23
16 | ROOM_SIZE_MAX = 10
17 | DESIRED_RMS = 8
18 | MONSTERS = ["X","G","D","S","N","W"]
19 | BLANK = " "
20 | FLOOR = "."
21 | DOOR = "+"
22 | GOLD = "g"
23 | NUM_HIDDEN_GOLD = 11
24 | BORDER = "*"
25 | PLAYER = "@"
26 | VALIDMOVES = "qs123456789"
27 | MOVEMAP = [(0,0),(-1,1),(0,1),(1,1),(-1,0),(0,0),(1,0),(-1,-1),(0,-1),(1,-1)]
28 | PRINT_PAUSE = 1
29 | WINDOWS_KEYPAD = ["n/a","key_c1","key_c2","key_c3","key_b1","key_b2",
30 | "key_b3","key_a1","key_a2","key_a3"]
31 | LINUX_KEYPAD = ["n/a","key_end","key_down","key_npage","key_left",
32 | "n/a","key_right","key_home","key_up","key_ppage"]
33 | STATUS_ROW = 0
34 | MESSAGE_ROW = 1
35 |
36 | class GameState():
37 |
38 | def __init__(self):
39 | self.dungeon_map = []
40 | self.player_map = []
41 |
42 | self.hidden_gold = NUM_HIDDEN_GOLD
43 | self.gold_stash = 0
44 | self.found_gold = False
45 |
46 | self.whats_here = FLOOR
47 | self.monster_whats_here = FLOOR
48 | self.see_more = False
49 | self.shift_mode = 0
50 |
51 | self.player_locX = 0
52 | self.player_locY = 0
53 | self.player_HP = 50
54 | self.player_gold = 0
55 | self.player_experience = 0
56 | self.monsters_killed = 0
57 | self.next_level = 0
58 |
59 | self.active_monster = ""
60 | self.monster_name = ""
61 | self.monster_level = 0
62 | self.monster_hp = 0
63 | self.monster_locX = 0
64 | self.monster_locY = 0
65 | self.monster_delay = 0
66 | self.prev_monster = ""
67 |
68 |
69 | def display_welcome(screen):
70 | screen.clear()
71 | screen.addstr(" PYDUNGEON\n A recreation of CURSOR #15 DUNGEON\n")
72 | screen.addstr("Original (C)1979 by Brian Sawyer.\nPyDungeon is public domain.\n")
73 | screen.addstr(("-" * 40) + "\n")
74 | screen.addstr("Search for GOLD in the ancient ruins\n\n")
75 | screen.addstr("Press ")
76 | screen.addstr("RETURN", curses.A_REVERSE)
77 | screen.addstr(" to begin ")
78 | screen.refresh()
79 | keystr = ""
80 | while keystr != "\n" and keystr != "padenter":
81 | keystr = screen.getkey().lower()
82 |
83 |
84 | def message_update(screen, msg, reversed=False):
85 | if reversed:
86 | attrib = curses.A_REVERSE
87 | else:
88 | attrib = curses.A_NORMAL
89 |
90 | screen.addstr(MESSAGE_ROW, 0, " " * WIDTH)
91 | screen.addstr(MESSAGE_ROW, 0, msg, attrib)
92 | screen.refresh()
93 | sleep(1)
94 |
95 |
96 | def get_player_move(screen, game_state):
97 | screen.addstr(STATUS_ROW, 0,
98 | "HIT PTS. {} EXP. {} GOLD {}".format(
99 | int(game_state.player_HP+.5),
100 | game_state.player_experience,
101 | game_state.player_gold))
102 |
103 | move = "X"
104 | while not move in VALIDMOVES:
105 | move = get_input(screen, "You may move. ")
106 |
107 | # Check to see if the move is one of the special
108 | # shift mode values that might occur in windows
109 | # or linux/unix. If so, set the move to be the
110 | # keypad number and the game state shift mode to on.
111 | # otherwise, set shift mode to off.
112 | if move in WINDOWS_KEYPAD:
113 | move = str(WINDOWS_KEYPAD.index(move))
114 | game_state.shift_mode = 1
115 | elif move in LINUX_KEYPAD:
116 | move = str(LINUX_KEYPAD.index(move))
117 | game_state.shift_mode = 1
118 | else:
119 | game_state.shift_mode = 0
120 |
121 | return move
122 |
123 |
124 | def get_input(screen, msg=""):
125 | if msg != "":
126 | num_rows, num_cols = screen.getmaxyx()
127 | str = msg + (" " * (num_cols-len(msg)))
128 | screen.addstr(MESSAGE_ROW, 0, str)
129 | screen.move(MESSAGE_ROW, len(msg))
130 | screen.refresh()
131 | sleep(.5)
132 | keystr = ""
133 | while keystr == "":
134 | keystr = screen.getkey().lower()
135 |
136 | return keystr
137 |
138 |
139 | def init_map():
140 | # Create new map/screen data structure
141 | # Returns: list of [WIDTH][HEIGHT] elements
142 | map_struct = []
143 | for col in range(WIDTH):
144 | map_struct.append([])
145 | for row in range(HEIGHT):
146 | map_struct[col].append(BLANK)
147 |
148 | return map_struct
149 |
150 |
151 | def generate_rooms(map):
152 |
153 | max_rooms = DESIRED_RMS + choice((-1,0,1))
154 |
155 | # Keep generating rooms until we've hit a limit of DESIRED_RMS(+/- 1) rooms
156 | for rooms_generated in range(0,max_rooms):
157 | room_width=0; room_height=0
158 | roomX=0; roomY=0
159 | monsterX=0; monsterY=0
160 |
161 | bad_room = True
162 | while bad_room:
163 | room_width = randint(2,ROOM_SIZE_MAX)
164 | room_height = randint(2,ROOM_SIZE_MAX)
165 | # Starting at 1 because 0 is always the border and we want a gap of
166 | # at least a space around things.
167 | roomX = randint(2,WIDTH-1); roomY = randint(2,HEIGHT-1)
168 |
169 | # Check to see if the room would extend over borders
170 | if roomX+room_width > WIDTH-2 or roomY+room_height > HEIGHT-2:
171 | continue
172 |
173 | # Check to see if room would extend over something already generated
174 | # We want a gap of at least 1 space between rooms
175 | overlap = False
176 | for x in range(roomX-1,roomX+room_width+2):
177 | for y in range(roomY-1,roomY+room_height+2):
178 | if map[x][y]!= BLANK: # is something already here?
179 | overlap = True # then try again
180 | break
181 | if overlap:
182 | break
183 | if overlap:
184 | continue
185 |
186 | bad_room = False
187 |
188 | # Fill room floor
189 | for x in range(roomX,roomX+room_width+1):
190 | for y in range(roomY,roomY+room_height+1):
191 | map[x][y] = FLOOR
192 |
193 | # Generate vertical passages from generated room down.
194 | passageX = roomX + randint(0,room_width)
195 | for row in range(roomY+room_height+1,HEIGHT-2):
196 | if map[passageX][row] not in (BLANK, BORDER, DOOR):
197 | for passageY in range(roomY+room_height+1, row):
198 | map[passageX][passageY]=FLOOR
199 | map[passageX][row-1]=DOOR
200 | break
201 |
202 | # Generate horizontal passages from generated room right.
203 | passageY = roomY + randint(0,room_height)
204 | for col in range(roomX + room_width+1, WIDTH-2):
205 | if map[col][passageY] not in (BLANK, BORDER, DOOR):
206 | for passageX in range(roomX+room_width+1,col):
207 | map[passageX][passageY]=FLOOR
208 | map[col-1][passageY]=DOOR
209 | break
210 |
211 | # Generate a monster in the room. Every room has a monster!
212 | monsterX = randint(roomX, roomX+room_width)
213 | monsterY = randint(roomY, roomY+room_height)
214 | map[monsterX][monsterY] = choice(MONSTERS)
215 |
216 |
217 | def gen_dungeon():
218 | # We return a list data structure that represents a map of 40 columns,
219 | # 25 rows. The dungeon is generated inside this structure and serves
220 | # to feed what will be seen on the screen. In a sense, we'll have two
221 | # structures - the full map, and then the map that the player reveals
222 | # and what gets painted to the screen.
223 | # See "dungeon-memory-sim.py" for my notes on how this all worked in the
224 | # original source. I've since "pythonized" the code to reflect the data
225 | # structures I'm working with, instead of a memory<->screen mapping that
226 | # the original program worked with.
227 | map = []
228 | map = init_map()
229 |
230 | # Create border around map
231 | for x in range(0,WIDTH):
232 | for y in range(0,HEIGHT):
233 | if x==0 or y==0 or x==WIDTH-1 or y==HEIGHT-1:
234 | map[x][y]=BORDER
235 |
236 | generate_rooms(map)
237 |
238 | # Distribute 11 gold around the dungeon
239 | for N in range(1, NUM_HIDDEN_GOLD+1):
240 | is_floor = False
241 | while not is_floor:
242 | goldX = randint(1,WIDTH-1); goldY = randint(1,HEIGHT-1)
243 | is_floor = (map[goldX][goldY]==FLOOR)
244 | map[goldX][goldY]=GOLD
245 |
246 | return map
247 |
248 |
249 | def what_is_seen(screen, game_state):
250 |
251 | gold_near = False
252 |
253 | if game_state.see_more:
254 | distance = 2; game_state.see_more = False
255 | else:
256 | distance = 1
257 |
258 | for x in range(distance*-1, distance+1):
259 | for y in range(distance*-1, distance+1):
260 | viewx = game_state.player_locX + x
261 | viewy = game_state.player_locY + y
262 |
263 | # If we're trying to view off map, or our current position,
264 | # continue on.
265 | if viewx < 1 or viewy < 1 or \
266 | viewx > 39 or viewy > 39 or (x==0 and y==0):
267 | continue
268 |
269 | whats_here = game_state.dungeon_map[viewx][viewy]
270 | game_state.player_map[viewx][viewy] = whats_here
271 |
272 | if whats_here in (DOOR, FLOOR, BORDER):
273 | continue
274 | if whats_here==GOLD:
275 | # We found gold! How much?
276 | game_state.gold_stash += 1+int(
277 | (game_state.player_gold+1)*(random()))
278 | gold_near = True
279 | # If we've not been near this gold, announce it.
280 | if not game_state.found_gold:
281 | message_update(screen, "Gold is near!")
282 | continue
283 | if whats_here in MONSTERS:
284 | if whats_here=="X":
285 | game_state.monster_name="Spider"
286 | game_state.monster_level=3
287 | elif whats_here=="G":
288 | game_state.monster_name="Grue"
289 | game_state.monster_level=7
290 | elif whats_here=="D":
291 | game_state.monster_name="Dragon"
292 | game_state.monster_level=1
293 | elif whats_here=="S":
294 | game_state.monster_name="Snake"
295 | game_state.monster_level=2
296 | elif whats_here=="N":
297 | game_state.monster_name="Nuibus"
298 | game_state.monster_level=9
299 | elif whats_here=="W":
300 | game_state.monster_name="Wyvern"
301 | game_state.monster_level=5
302 |
303 | game_state.active_monster = whats_here
304 | game_state.dungeon_map[viewx][viewy] = FLOOR
305 | game_state.monster_delay = 0
306 |
307 | # Monster HP based on our HP, experience and random
308 | # We save the level for later if we defeat the monster, to
309 | # get XP from!
310 | # HP will change each time we see this monster. Heh.
311 | game_state.monster_hp = game_state.monster_level = \
312 | int(random()*game_state.player_HP +
313 | (game_state.player_experience/game_state.monster_level) +
314 | game_state.player_HP/4)
315 |
316 | # If we've already revealed a monster, put it back on the
317 | # dungeon map.
318 | if game_state.monster_locX > 0:
319 | game_state.dungeon_map[game_state.monster_locX][game_state.monster_locY] = \
320 | game_state.prev_monster
321 | game_state.prev_monster = game_state.active_monster
322 | game_state.monster_locX = viewx
323 | game_state.monster_locY = viewy
324 | message_update(screen, "A {} with {} points is near!".format(
325 | game_state.monster_name, game_state.monster_hp))
326 |
327 | continue
328 |
329 | game_state.found_gold = gold_near
330 |
331 |
332 | def monster_move(screen, game_state):
333 | pX, pY = game_state.player_locX, game_state.player_locY
334 | mX, mY = game_state.monster_locX, game_state.monster_locY
335 | dirX = 0; dirY = 0
336 |
337 | if pX==mX and pY != mY:
338 | if pY < mY:
339 | dirY = -1
340 | elif pY > mY:
341 | dirY = 1
342 | elif pY==mY and pX != mX:
343 | if pX < mX:
344 | dirX = -1
345 | elif pX > mX:
346 | dirX = 1
347 | elif pX < mX and pY < mY:
348 | dirX = -1; dirY = -1
349 | elif pX < mX and pY > mY:
350 | dirX = -1; dirY = 1
351 | elif pX > mX and pY < mY:
352 | dirX = 1; dirY = -1
353 | elif pX > mX and pY > mY:
354 | dirX = 1; dirY = 1
355 |
356 | target = game_state.dungeon_map[mX+dirX][mY+dirY]
357 |
358 | # If we can't move into the target because there is something there that
359 | # we can't cross, then just stay put.
360 | if target == BLANK or target == BORDER or target==DOOR:
361 | game_state.player_map[mX][mY] = game_state.active_monster
362 | return
363 |
364 | game_state.player_map[mX][mY] = game_state.monster_whats_here
365 | mX+=dirX; mY+=dirY
366 | game_state.monster_whats_here = game_state.player_map[mX][mY]
367 | game_state.player_map[mX][mY]=game_state.active_monster
368 | game_state.monster_locX, game_state.monster_locY = mX, mY
369 | if mX==pX and mY==pY:
370 | attack(screen, game_state)
371 |
372 |
373 | def attack(screen, game_state):
374 | message_update(screen, "AN ATTACK!", True)
375 | player_power = game_state.player_HP+game_state.player_experience
376 | monster_attack=random()*game_state.monster_hp/2+game_state.monster_hp/4
377 | player_attack=random()*player_power/2+player_power/4
378 | game_state.monster_hp=int(game_state.monster_hp-player_attack)
379 | game_state.player_HP=int(game_state.player_HP-monster_attack)
380 |
381 | # Is the PC dead? If not, continue with attack results
382 | if game_state.player_HP > 0:
383 | # If monster is twice as strong as player, it will make
384 | # an offer that the player can't refuse...
385 | if (game_state.monster_hp/game_state.player_HP+1)>2:
386 | message_update(screen,
387 | "The {} will leave, IF you will give it half your gold.".format(
388 | game_state.monster_name))
389 | responded = False
390 | answer = ""
391 | while not responded:
392 | answer = get_input(screen, "Will you consent to this (Y or N)? ")
393 | if answer.startswith("y") or answer.startswith("n"):
394 | responded = True
395 | if answer == "y":
396 | # Take the gold, the monster disappears!
397 | game_state.player_gold -= int(game_state.player_gold/2)
398 | remove_monster(game_state)
399 | return
400 | elif game_state.monster_hp > 0:
401 | message_update(screen,
402 | "The {} has {} hit points".format(game_state.monster_name,
403 | game_state.monster_hp))
404 | return
405 | else:
406 | # The monster is dead
407 | game_state.player_experience += game_state.monster_level
408 | game_state.monsters_killed += 1
409 | message_update(screen,
410 | "The {} is dead!".format(game_state.monster_name))
411 | remove_monster(game_state)
412 | if game_state.player_experience >= game_state.next_level * 2:
413 | game_state.next_level = game_state.player_experience
414 | game_state.player_HP *= 3
415 | message_update(screen, "Your hit pts. have been raised")
416 | return
417 | else:
418 | # Death routine will be handled from check in main loop.
419 | return
420 |
421 |
422 | def remove_monster(game_state):
423 | game_state.whats_here = FLOOR
424 | game_state.monster_locX = 0
425 | game_state.monster_locY = 0
426 | game_state.active_monster = ""
427 | game_state.monster_delay = 0
428 | game_state.monster_whats_here = FLOOR
429 |
430 | game_state.player_map[game_state.player_locX][game_state.player_locY] = PLAYER
431 |
432 |
433 | def display_dungeon_map(screen, map, final=False):
434 | for row in range(0, HEIGHT):
435 | rowstr = ""
436 | for col in range(0,WIDTH):
437 | rowstr += map[col][row]
438 | screen.addstr(row+2, 0, rowstr)
439 | screen.refresh()
440 | if final:
441 | sleep(.5)
442 |
443 | def end_game(screen, game_state, end_message):
444 | screen.clear()
445 | if end_message != "":
446 | message_update(screen, end_message)
447 | sleep(2)
448 |
449 | screen.addstr(STATUS_ROW, 0, "Gold: {} Exp: {} Killed {} Beasts".format(
450 | game_state.player_gold,
451 | game_state.player_experience,
452 | game_state.monsters_killed))
453 |
454 | screen.refresh()
455 |
456 | def main(screen):
457 | # Display welcome
458 | display_welcome(screen)
459 |
460 | # Game loop
461 | while True:
462 | screen.clear()
463 | screen.addstr(STATUS_ROW, 0, "Setting up...")
464 | screen.refresh()
465 |
466 | # Ensure that our terminal/output device supports the screen size.
467 | num_rows, num_cols = screen.getmaxyx()
468 | assert num_rows >= HEIGHT and num_cols >= WIDTH, \
469 | "Terminal/screen needs to be {} rows by {} cols.".format(HEIGHT, WIDTH)
470 |
471 | # Initialize vars and maps
472 | # Look at annotated source and line 210 to see several of the variables needed
473 | # to be tracked through this game. Everything was global. To manage that mess,
474 | # I created a class struct to hold the game state.
475 | game = GameState()
476 |
477 | # Dungeon Map is the generated map
478 | game.dungeon_map = gen_dungeon()
479 |
480 | # Player Map is what is displayed to the player as they navigate the dungeon
481 | game.player_map = init_map()
482 |
483 | good_location = False
484 | while not good_location:
485 | game.player_locX = randint(1,WIDTH-1)
486 | game.player_locY = randint(1,HEIGHT-1)
487 | good_location = \
488 | (game.dungeon_map[game.player_locX][game.player_locY]==FLOOR)
489 |
490 | # Determine/display what is visible
491 | what_is_seen(screen, game)
492 |
493 | game.player_map[game.player_locX][game.player_locY]=PLAYER
494 | game.whats_here = FLOOR
495 |
496 | # Input/Move loop
497 | playing = True
498 | while playing:
499 | map_move = False
500 | # Eligible moves are 1-9 for directions
501 | # except 5, which is a "wait" (and heal!)
502 | # S is 'see more' and q is quit.
503 | # so if 5 or S, update HP, continue to get input.
504 | # if q, then break out of this while and stop playing.
505 | # if invalid input (""), continue to get input till valid.
506 | # Otherwise, continue on to calculate the results of the move!
507 | while not map_move:
508 | display_dungeon_map(screen, game.player_map)
509 | move = get_player_move(screen, game)
510 |
511 | if move == "":
512 | pass
513 |
514 | elif move == "5": # Rest/recover HP
515 | game.player_HP += 1+sqrt(game.player_experience/game.player_HP)
516 | map_move = True
517 | move = 5
518 |
519 | elif move == "s": # See more mode
520 | game.see_more = True
521 | game.player_HP -= 2
522 | map_move = True # Force a map move/screen refresh
523 | move = 5 # but make it a "stay put" move.
524 |
525 | elif move == "q": # Quit game
526 | playing = False
527 | break
528 |
529 | else:
530 | map_move = True
531 | move = int(move)
532 |
533 | # You lose HP as you move, doubly so if you are in shift
534 | # mode to move through walls. If we drop below 0XP, end of game.
535 | game.player_HP -= .15 + (2 * game.shift_mode)
536 | if game.player_HP <= 0:
537 | playing = False
538 | break
539 |
540 | # If we're here, we're moving. Check to see if we're moving
541 | # thru the spaces between rooms and not in in shift-move
542 | # mode. Then, check to see if we're trying to move through an
543 | # impassable border. In either case, go get player input again.
544 | # Otherwise make the move.
545 | if map_move:
546 | moveX,moveY = MOVEMAP[move]
547 | locX,locY = game.player_locX,game.player_locY
548 | if (game.dungeon_map[locX+moveX][locY+moveY]==BLANK \
549 | and game.shift_mode!=1) or \
550 | (game.dungeon_map[locX+moveX][locY+moveY]==BORDER):
551 | continue
552 | else:
553 | game.player_map[locX][locY]=game.whats_here
554 | game.whats_here = game.dungeon_map[locX+moveX][locY+moveY]
555 | game.player_map[locX+moveX][locY+moveY]=PLAYER
556 | game.player_locX=locX+moveX
557 | game.player_locY=locY+moveY
558 |
559 | # What do we see now as a result of the move?
560 | what_is_seen(screen, game)
561 |
562 | # If we moved onto gold...
563 | if game.whats_here == GOLD:
564 | game.player_gold+=game.gold_stash
565 | message_update(screen, "You found {} gold pieces!".format(game.gold_stash))
566 | game.dungeon_map[game.player_locX][game.player_locY] = FLOOR
567 | game.whats_here = FLOOR
568 | game.hidden_gold -= 1
569 | if game.hidden_gold == 0:
570 | playing = False
571 | break
572 |
573 | # If we move onto a monster, attack!
574 | if game.whats_here in MONSTERS:
575 | attack(screen, game)
576 |
577 | if game.player_HP <= 0:
578 | playing = False
579 | break
580 |
581 | # If there's an active monster on the map,
582 | # check its delay, if its delay is > 1, then
583 | # it can move. This gives the player a chance
584 | # to escape!
585 | if game.monster_locX > 0:
586 | game.monster_delay += 1
587 | if game.monster_delay > 1:
588 | monster_move(screen, game)
589 | # There can be an attack after the monster
590 | # move, so check player HP again.
591 | if game.player_HP <= 0:
592 | playing = False
593 | break
594 |
595 | if game.player_HP <= 0:
596 | end_game(screen, game,"You're dead!")
597 | elif game.hidden_gold == 0:
598 | end_game(screen, game,"You found all the gold! You won!")
599 | else:
600 | end_game(screen, game, "")
601 |
602 | # Display the dungeon map
603 | display_dungeon_map(screen, game.dungeon_map, True)
604 |
605 | playagain = get_input(screen, "Want to play again? ")
606 | if not playagain.startswith("y"):
607 | sys.exit(0)
608 |
609 | # Safely start/end curses windowing
610 | curses.wrapper(main)
611 |
--------------------------------------------------------------------------------