Having functions that operate on the data separate also helps in organizing the code and reduces amount of 'doers', 'managers' and 'utilityclassxes'. It also helps to answer the question "if I want to read a scroll, who has the method? character, scroll, some manager?".
I agree with and enjoy the procedural and functional paradigms. However, I think that "OOP" suffers from its own flexibility. The name "method" is poorly chosen, IMO, as is the term "function".
In fact, I also object to "object" as it's objectively been selected only for the purpose of requiring an object lesson in ambiguity rather than objectivity. QED.
I hold a similar stance on the objectively subjective terms "definition" and "declaration", but I digress.
Despite my admittedly unpopular views I do recognize the value of Agent Aware Programming. An agent is like an "object", except is should be clear that an Agent can act, while objects are only acted upon. Thus in your example, the ambiguity is easily resolved. The scroll is an object. It has only properties and does not perform actions of its own. Thus it is the Player (or some other Agent) which should activate its .read() action while passing the Agent which scroll to read. Spells act upon the world so they are Agents invoked by Players or other Magic capable Agents. Which Spell to invoke when reading a Scroll is a property of the scroll entity.
Player.read( scroll ){ scroll.spell.invoke() }
...
IdentifySpell.read( scroll ){ scroll.identified = 1 }
In other words, OOP shouldn't be "objects all the way down".
One of the things many OOP implementations fail at is they neglect to provide their objects with a facility to determine what container object they may be a property of. For instance, an object can access its properties. Properties should be able to determine the object to which they are attached. This is far more useful for the same reason that a doubly linked list is more useful than a singly linked list. In theoretical terms, the "object.property" relationship is broken in nearly all OOP implementations' symbolic dimensions, as the relationship is only accessible from the scope of one half of the pair. This is the cause of many unnecessary headaches in today's OOP languages.
For instance. If the Spell is invoked, how does it know to delete the scroll to which it is attached or if it is attached to a scroll at all? Most OOP systems would force you to pass spell.invoke() an optional scroll reference and if it's a non nil value, then the spell would destroy the scroll object passed in -- Or, perhaps you fall into the trap of adding actions to POD via giving scrolls a state transition, perhaps through data hiding: scroll.setRead( bool ){ ...; destroy( self ) }, which causes the the scroll to be an Agent with actions of its own to perform (or object with methods), thus causing the "Classness" to proliferate onto what could otherwise be merely a data structure.
Perhaps I should explain first that a "symbolic dimension" is a language construct imagined as a dimension of the n-dimensional language-space. So, when you create a construct, such as the ability to give a symbolic name to an offset in memory (create a variable) you are mapping the data dimension to a symbolic dimension. The same symbolic dimension that allows you to name global variables allows you to name properties of structs. Each property of the struct is a named offset in memory, relative to the struct's starting address. Therefore the variable naming symbolic dimension is universally applied in these examples. In a "complete" language you would be able to apply any symbolic dimension (any language construct) to any language primitive. Furthermore you should be able to traverse in either direction along an unbroken dimension, in order to reverse the mapping. C (and most languages) fail at this because there's no way for me to say: Give me the name of the property at memory offset 0x100, or Give me the Nth property name of structs having type T. Likewise the symbolic mapping between Object.property is broken.
Instead, consider an unbroken symbolic dimension of object.property mapping:
// If a spell is the property of a scroll, the scroll is destroyed upon invoking the spell.
Spell.invoke(){ if ( typeof self.owner == Scroll ) destroy ( self.owner ); self.applyMagic(); }
This codifies the relationship of "ownership" which RAII implicitly creates, but I digress.
Though I've been using c-like pseudo code syntax a RL need not be so. However, since Java, JS, C/C++, and a host of other languages use similar syntax, an RL lang might benefit from the principal of least surprise by adopting some commonly familiar syntactical idioms. On the other hand, in keeping with the roguelike spirit a RL lang might utilize as many strange symbols as possible, as RL players (and thus devs) tend to be able to cope with such things more easily than others... I've always found it odd there weren't more roguelikes implemented in Perl (oh look, another C-like syntax [or more correctly called Algo-like syntaxes]).
And yet, on the gripping hand, a Roguelike language that allows new symbols to be added to the Lexer's tokens in order to extend the language, could also have an "import Roguish" which would randomize the language's syntax using the source's file name as the seed. Then the programmer would get to play a Rogue-like identification game where compiler errors act as your identify spell, and you slowly get to learn what symbols to use in the source file in order to program -- Need to rename the file? Sorry, that causes confusion which lasts until the source is reprogrammed to use the new symbols and may result in project permadeath. Note that removing "import Roguish" would have the same effect.
I'm not saying that an OOP or Agent Aware paradigm is required in a RL language, simply that such things are useful and probably should be possible to use as the optimal RL language would probably support multiple programming paradigms. This is one reason why I implement virtual machines before languages, so that one need not cram every paradigm under the sun into one language syntax. Multiple languages can compile to one common bytecode, but I'm getting ahead of myself.
I like syntax of lisp quite a bit, so I think I would take that as a base.
I like the reflection and dynamism of Lisps, but think the syntax could be improved : )))))))
Using the Sexp for everything is powerful because it simplifies the implementation of self modifying code, or self aware code (macro facilities). IMO, Lisp is a nice language to write programs that write programs in... However, any new language can gain the features of and/or surpass Lisp (esp. in terms of usability) so long as: the compiler is self hosting, the macro language is the same syntax as the programming language, and the syntax tree being compiled is accessible from within macros. One could take things a bit further and have the macro language be aware of the tokenizer too.
Personally I prefer compiled languages with strong typing because it catches a crap load of errors at compile time. I can't tell you the number of times I've had to resort to hunting through diffs line by line for hours to chase down a bug in a large library coded in dynamically typed languages. IMO, dynamic typing is great for small projects, but its usefulness is quickly outpaced by the pitfalls introduced. Case and point: Most successful dynamic languages eventually adopt a "use strict" facility to enable stronger type checking. Strong typing need not restrict run time dynamism in a significant way (though in most strongly typed languages have little to no dynamism). Duck typing with compile time type analysis can yield much of the freedom that purely dynamic languages have, while introducing far less headaches. "Adapter typing" is another feature strongly typed languages can use whereby methods with the same parameters but differing names can be mapped from one type onto the other to add more dynamism.
The key feature missing in most compiled languages, I think, is they lack a runtime scripting language that is the same syntax as the compiled language. IMO, if you find yourself "embedding" a scripting language then the core language has failed you as it lacks valuable features you need (namely, ability to compile code at runtime). "I heard you like languages so I put another language in your language so you can program while you program", ugh, no, that's just shameful. IMO, the optimal compilable roguelike language would include an embedded scripting language which is of the same syntax as the compiled "host" language. Thus, enabling the compiled code and scripted code to transparently call from one into the other with no "wrappers" or "binding layer" required.
Scripting languages tend to become prohibitive to use for performance intensive computation when they're not designed to also be compilable. With a compilable scripting language: A complex script running too slow interpreted at runtime? Have it compiled at compile time rather than runtime, and it goes faster with no change to the code. Want to quickly prototype a feature without recompiling a bunch? Just implement it as a script, then have it be compiled later when you're done rapid prototyping. This would also greatly assist in adding "mod support".
Think about it: A macro language is simply a scripting language that runs at compile time. If you could link with the compiler's "macro facility" code in your program, presto you have an embedded run-time scripting language. All the better if all three (compile time, macro, and runtime code) share the same syntax as the compiled language -- Unlike, say, C/C++ or Assembler preprocessor macros. IMO, it's as easy as reusing the compiler's code: Make the compiler part of the standard library's API, and if it's used then the compiler's code gets included. Having a design goal of (optional) compilation tends to improve performance over languages meant to be interpreted like Lisp and JS which suffer loss of language features to compilation constraints (I wish Lisp Machines were still a thing). At least Lisp doesn't actively fight against being compiled (like JS, hence ASM.js & WebAssembly (sacrifice JS features to the compiler gods!))). Additionally, having the goal of linking with the compiler (and potentially an interpretor) in programs helps keep the language lean and fast (design naturally tends towards less bloat then).
While there are many Lisp implementations readily available to extend, they're typically written in C. And if we're going to do that, might as well just port a C compiler to the VM and be done (then you could compile your favorite Lisp on the C compiler for the VM and then code in Lisp on the VM). An explicit VM meant to run only Lisp might be a worthwhile project, but the last time I tried designing a real CPU like that in VHDL (which you could fab or output to a FPGA), the chip grew so large that I couldn't afford a FPGA with enough gates on it to actually run it (maybe because I tried to implement a Lisp with lots of features rather than a minimal Lisp).
Of course a RL lang could ditch the VM underpinnings and just extend an extensible language like Haskel or Lisp, but I'm wondering more along the lines of what a from scratch RLLang syntax would be.
Of note, I think the @ needs to play a prominent role in a RLLang's syntax. Perhaps it could be the "loop" symbol?
say "hello world" @( 1 ); // infinite loop while true of hello world.
[1 2 3 4 5] @ say( "count " #); // map over an array; outputs hello 1 hello 2 ... hello 5, # being a symbol for the value.
That or @ could be a valid identifier character, so that your player var could just be: @
In one of my VM's Assemblers @ is used to denote indirection (like dereferencing a pointer). Here's a manual implementation of a switch / case block (jump table).
// Switch: /r0
up 2 /r0
add main_jt0 /r0
set main_jt0_max /r1
// jump to default case if value (reg 0) is beyond the jump table (reg 1)
jae /r0 /r1 main_jt0_default
set @/r0 /r0 // sets the value at reg 0 into reg 0.
jump /r0
// Jump table #0
:main_jt0
#inline main_jt0_0 main_jt0_1 main_jt0_2 //... snip
:main_jt0_max
// JT0: Case 0: syscall EXIT( status )
:main_jt0_0
// Set the exit status to user specified value, then die.
pop /r1
set /r1 @sys_exitStatus
pop /r0
err E_DEATH
...
Having ability to model the basic constructs like state machines is very good idea.
I tend to agree. Support for state machines at the language level eliminates the headache of huge switch statements within functions, or forgetting to call state transition functions when change.
Another option I would like to have is relational or logic programming. Instead of describing an algorithm, I would describe problem domain and relations of things within it and let the computer to solve the problem.
That's pretty nifty.
I've done something like this before (at work) using neural networks to map and emerge a solution for abstract problem spaces, but I'm having a hard time thinking of how one would implement such things at the language level (rather than programming them).
An AI with weighted decision trees could do the trick. Give the player the ability to access a sufficiently advanced AI agent -- in other words, let the player call upon the AI code that NPCs / enemies use to defuse traps, etc. However, this might be best left up to the programmer rather than the language, since some programmers may prefer that traps be explicitly disarmed rather than have a "move cautiously" trap solver. Perhaps I've also simply misunderstood your intent.
I find this topic fun. Perhaps if you ever end up building a custom chip for your new roguelike language, you could include the option to turn permadeath on, which would instantly fry the chip when you lose. Then you have to go solder another one in!
Coincidentally one of my virtual CPUs can enter an evil state of permanent death. This VM assembly code documents the feature, it's status #13:
// Constantly recognized evils. Encountered when misfortune has befallen.
#const E_BREAK 0 // Debugger demands a break from execution.
#const E_PERM 1 // Permission level not high enough.
#const E_INVALID 2 // Operator is lame or otherwise severely unwell.
#const E_EXEOFLOW 3 // Execution has overflown the stack.
#const E_EXEUFLOW 4 // Execution attempted below the stack.
#const E_NOMEM 5 // RAM's inventory is full.
#const E_ACCESS 6 // Thwarted an improper access attempt.
#const E_ALIGN 7 // Incompatible alignment for RAM.
#const E_UNKNOWN 8 // All attempts to identify this mysterious evil failed.
#const E_ENV 9 // Evil lurks in the environment beyond the skull's safety.
#const E_HALTED 10 // Can't run, but not dead yet; Just waiting for a spell.
#const E_PAROFLOW 11 // Parameters have overflown their stack.
#const E_PORTAL 12 // Engram unable to traverse the skull's portal.
#const E_DEATH 13 // Brain has permanently died, and can not be resurrected.
#const E_NOSUPPORT 14 // Feature known but abandoned (hopefully temporarily).
While I can force a hard reboot of the "hardware", I haven't discovered a way to fry a chip in VERILOG / VHDL (hardware definition language). I guess i could include some NVRAM on chip, then set that to "permadeath", and check the non volatile storage to see if that's been set before executing any instructions... I do accidentally fry a motherboard sometimes working with custom PCI boards and/or badly wired custom peripheral devices. For this reason it's recommended that in-development devices be attached only to a separate controller board not on a port integrated with the mainboard since it's far cheaper to replace a fried expansion card. So, it would be trivial to make an off-chip facility for the feature, however impractical that may be. One could supply an optional serial or parallel device which responds to permadeath messages by suicidally dumping a bank of capacitors onto the system's IO bus, frying your computer when your game is over.
After sleeping a bit, I think I have another perspective to this. Maybe we should look from point of view of primitive, means of combination and means of abstraction too? They're the basic parts of pretty much every programming language and the essence that really tells them apart.
Primitives: the usual suspects like numbers, text, lists and such are obvious. But since we're talking specificially roguelike language, should there be higher concepts here too? Like character/creature, item, coordinates, action, ai-routine and such? If they're very detailed, building different kinds of roguelikes might be tricky, but if they're too vague they're maybe not so useful? Code is one primitive too I would say. Things like lazy and infinite datastructures and asynchronous operations would be nice to have built into language.
[snip]
Yeah, that's the stuff! One way to implement a "code primitive" would be to make them special literals of the type "script" which can be executed by a run-time interpreter. The difference between a String and a Script would be that a Script has been lexicographically analyzed, tokenized, and potentially parsed into a syntax tree (or bytecode) and is ready for interpretation. Scripts that are dynamically linked could be fetched and processed at initialization time. The RL lang would need a standard function (or constructor) that created a Script entity from a plain string. This would allow compiling and running of arbitrary text strings at runtime. One could then implement an optional "console" feature simply by executing user input (like an eval() function). However, due to the nature of roguelikes (and games in general) it would be useful to have a way of specifying the scope available to a piece of runtime code, as well as which API functions can be accessed.
I've often wished I had the ability to write small scripts to search inventory or perform complex repetitive actions... My terminal emulator has sufficed for most games, but an in-game console would potentially enable far greater control and script-ability. With the right data in scope players could even code up an AI / bot to play for them via such "user script" features.
The question of some primitives would be, what characteristics / properties should roguelike primitives have. E.g., a "generic RL item" has: brief text, detail description, possibly text for when its not identified, a symbol (or tile#) to display when it's visible on the map, etc. If it can be equipped, to what slot, what "action" uses the item, etc.
Of course a base "RL Item" could be part of the RL lang's standard library. Coders would be free to implement their own Item, but we could provide a basic item and inventory implementation to extend if they so desired. The trick of good language design is not to provide everything everyone could ever need, but to provide some useful things to some people, and allow those with specialized needs to roll their own.