new articles
This commit is contained in:
@@ -7,6 +7,22 @@
|
|||||||
"author": "jetstream0/Prussia",
|
"author": "jetstream0/Prussia",
|
||||||
"tags": ["meta", "code", "project", "web", "markdown", "typescript_javascript", "css"]
|
"tags": ["meta", "code", "project", "web", "markdown", "typescript_javascript", "css"]
|
||||||
},
|
},
|
||||||
|
"bananopie": {
|
||||||
|
"title": "Bananopie",
|
||||||
|
"slug": "bananopie",
|
||||||
|
"filename": "bananopie",
|
||||||
|
"date": "26/12/2023",
|
||||||
|
"author": "jetstream0/Prussia",
|
||||||
|
"tags": ["code", "python", "cryptocurrency"]
|
||||||
|
},
|
||||||
|
"golfing-and-scheming": {
|
||||||
|
"title": "Golfing and Scheming",
|
||||||
|
"slug": "golfing-and-scheming",
|
||||||
|
"filename": "golfing_and_scheming",
|
||||||
|
"date": "23/12/2023",
|
||||||
|
"author": "jetstream0/Prussia",
|
||||||
|
"tags": ["code", "typescript_javascript", "python", "scheme", "code golf"]
|
||||||
|
},
|
||||||
"hash-functions": {
|
"hash-functions": {
|
||||||
"title": "Hash Functions",
|
"title": "Hash Functions",
|
||||||
"slug": "hash-functions",
|
"slug": "hash-functions",
|
||||||
|
|||||||
262
posts/bananopie.md
Normal file
262
posts/bananopie.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
Bananopie was written with the aim of being the Python equivalent of Banano.js, and furthering my understanding of the Nano/Banano protocol. I learned quite a bit about how blocks were constructed.
|
||||||
|
|
||||||
|
I think it's acheived that goal, and hopefully went a bit above and beyond in simplicity and powerfulness (it has some useful functions that Banano.js does not, like `send_all` and the old message signing, as well as local work generation).
|
||||||
|
|
||||||
|
The only two frustrations I had while writing Bananopie was not knowing whether certain things were big-endian or little-endian, since the Nano docs don't specify (I just tested against the output of Banano.js or wallets), and also dealing with Python's decimal precision fuckery.
|
||||||
|
|
||||||
|
You can see the syntax and documentation on [Github](https://github.com/jetstream0/bananopie) so I won't bother with that.
|
||||||
|
|
||||||
|
What I *do* want to talk about is how blocks are constructed (exciting, I know).
|
||||||
|
|
||||||
|
Remember, Banano/Nano is a DAG, not a blockchain. In Bitcoin, blocks contain multiple transactions are in one long chain. In Nano, each account (address), has it's own chain of blocks, where each transaction is one block. All those chains are connected to each other through sends and receive blocks.
|
||||||
|
|
||||||
|
In general, Nano blocks can be classified into three subtypes: send, receive, and change Representative, each which does exactly what you would expect. There is no restriction that only change blocks can change representatives - send and receive blocks can also change representatives.
|
||||||
|
|
||||||
|
All blocks have the following:
|
||||||
|
|
||||||
|
- type: Always "state"
|
||||||
|
- account: The address that is sending the block
|
||||||
|
- previous: The hash of the previous block of the account, or "0000000000000000000000000000000000000000000000000000000000000000" if the account is unopened (has never done any transactions)
|
||||||
|
- representative: The representative of the account that is sending the block
|
||||||
|
- balance: The balance of the account that is the sending the block
|
||||||
|
- link: Depends on the block type
|
||||||
|
|
||||||
|
## Send Block
|
||||||
|
|
||||||
|
A send block is one where the "balance" decreases. The "link" is the public key of the recipient.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Wallet:
|
||||||
|
...
|
||||||
|
def send(self, to: str, amount: str, work = False, previous = None):
|
||||||
|
amount = whole_to_raw(amount)
|
||||||
|
address_sender = self.get_address()
|
||||||
|
private_key_sender = get_private_key_from_seed(self.seed, self.index)
|
||||||
|
#public_key_sender = get_public_key_from_private_key(get_private_key_from_seed(self.seed, self.index))
|
||||||
|
public_key_receiver = get_public_key_from_address(to)
|
||||||
|
info = self.get_account_info()
|
||||||
|
if not previous:
|
||||||
|
previous = info["frontier"]
|
||||||
|
representative = info["representative"]
|
||||||
|
before_balance = info["balance"]
|
||||||
|
#height not actually needed
|
||||||
|
new_balance = int(int(before_balance)-amount)
|
||||||
|
if new_balance < 0:
|
||||||
|
raise ValueError(f"Insufficient funds to send. Cannot send more than balance (before balance {str(before_balance)} less than send amount {str(amount)})")
|
||||||
|
block = {
|
||||||
|
"type": "state",
|
||||||
|
"account": address_sender,
|
||||||
|
"previous": previous,
|
||||||
|
"representative": representative,
|
||||||
|
"balance": str(new_balance),
|
||||||
|
#link in this case is public key of account to send to
|
||||||
|
"link": public_key_receiver,
|
||||||
|
"link_as_account": to
|
||||||
|
}
|
||||||
|
block_hash = hash_block(block)
|
||||||
|
signature = sign(private_key_sender, block_hash)
|
||||||
|
block["signature"] = signature
|
||||||
|
if work:
|
||||||
|
block["work"] = work
|
||||||
|
return self.send_process(block, "send")
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Receive Block
|
||||||
|
|
||||||
|
A receive block is one where the "balance" increases. The "link" is the hash of the block to receive.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Wallet:
|
||||||
|
...
|
||||||
|
def receive_specific(self, hash: str, work = False, previous = None):
|
||||||
|
#no need to check as opened, I think?
|
||||||
|
#get block info of receiving
|
||||||
|
block_info = self.rpc.get_block_info(hash)
|
||||||
|
amount = int(block_info["amount"])
|
||||||
|
address_sender = self.get_address()
|
||||||
|
private_key_receiver = get_private_key_from_seed(self.seed, self.index)
|
||||||
|
#public_key_sender = get_public_key_from_private_key(get_private_key_from_seed(self.seed, self.index))
|
||||||
|
#public_key_sender = get_public_key_from_address(block_info["block_account"])
|
||||||
|
#these are the defaults, if the account is unopened
|
||||||
|
before_balance = 0
|
||||||
|
representative = address_sender
|
||||||
|
if not previous:
|
||||||
|
try:
|
||||||
|
#if account is opened
|
||||||
|
info = self.get_account_info()
|
||||||
|
previous = info["frontier"]
|
||||||
|
representative = info["representative"]
|
||||||
|
before_balance = info["balance"]
|
||||||
|
except Exception as e:
|
||||||
|
#probably, unopened account
|
||||||
|
previous = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
#height not actually needed
|
||||||
|
block = {
|
||||||
|
"type": "state",
|
||||||
|
"account": address_sender,
|
||||||
|
"previous": previous,
|
||||||
|
"representative": representative,
|
||||||
|
"balance": str(int(before_balance)+amount),
|
||||||
|
#link in this case is hash of send
|
||||||
|
"link": hash
|
||||||
|
}
|
||||||
|
block_hash = hash_block(block)
|
||||||
|
signature = sign(private_key_receiver, block_hash)
|
||||||
|
block["signature"] = signature
|
||||||
|
if work:
|
||||||
|
block["work"] = work
|
||||||
|
return self.send_process(block, "receive")
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Change Representative Block
|
||||||
|
|
||||||
|
A change block is one where the "balance" does not change, but the "representative" does. In this case, "link" is just "0000000000000000000000000000000000000000000000000000000000000000".
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Wallet:
|
||||||
|
...
|
||||||
|
def change_rep(self, new_representative, work = False, previous = None):
|
||||||
|
address_self = self.get_address()
|
||||||
|
private_key_self = get_private_key_from_seed(self.seed, self.index)
|
||||||
|
#public_key_sender = get_public_key_from_private_key(get_private_key_from_seed(self.seed, self.index))
|
||||||
|
#account must be opened to do a change rep
|
||||||
|
info = self.get_account_info()
|
||||||
|
if not previous:
|
||||||
|
previous = info["frontier"]
|
||||||
|
before_balance = info["balance"]
|
||||||
|
block = {
|
||||||
|
"type": "state",
|
||||||
|
"account": address_self,
|
||||||
|
"previous": previous,
|
||||||
|
"representative": new_representative,
|
||||||
|
"balance": before_balance,
|
||||||
|
#link in this case is 0
|
||||||
|
"link": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
}
|
||||||
|
block_hash = hash_block(block)
|
||||||
|
signature = sign(private_key_self, block_hash)
|
||||||
|
block["signature"] = signature
|
||||||
|
if work:
|
||||||
|
block["work"] = work
|
||||||
|
return self.send_process(block, "change")
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hashing Blocks
|
||||||
|
|
||||||
|
Before being signed, the block must be hashed. The resulting block hash is what you usually seen used to identify blocks (eg, to find a block on the block explorer, you would give it the block hash).
|
||||||
|
|
||||||
|
Let's look at Bananopie's `hash_block` function:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def hash_block(block) -> str:
|
||||||
|
blake_obj = blake2b(digest_size=32)
|
||||||
|
blake_obj.update(hex_to_bytes(PREAMBLE))
|
||||||
|
blake_obj.update(hex_to_bytes(get_public_key_from_address(block["account"])))
|
||||||
|
blake_obj.update(hex_to_bytes(block["previous"]))
|
||||||
|
blake_obj.update(hex_to_bytes(get_public_key_from_address(block["representative"])))
|
||||||
|
padded_balance = hex(int(block["balance"])).replace("0x","")
|
||||||
|
while len(padded_balance) < 32:
|
||||||
|
padded_balance = '0' + padded_balance
|
||||||
|
blake_obj.update(hex_to_bytes(padded_balance))
|
||||||
|
blake_obj.update(hex_to_bytes(block["link"]))
|
||||||
|
#return hash
|
||||||
|
return bytes_to_hex(blake_obj.digest())
|
||||||
|
```
|
||||||
|
|
||||||
|
`digest_size=32` means that the blake2b hash will have a 32 byte output, which makes sense, since block hashes are supposed to be 32 bytes.
|
||||||
|
|
||||||
|
The input for the hash is the preamble, the public key of the "account" field of the block, the "previous" of the block (hash of the previous block), the public key of the "representative" field of the block (the new/unchanged representative of the address), the "balance" field of the block, and the "link" field of the block, all concatenated.
|
||||||
|
|
||||||
|
For both Banano and Nano, the `PREAMBLE` in hexadecimal is `0000000000000000000000000000000000000000000000000000000000000006` (0x6). You see, Nano used to have different block types, each with it's own preamble: send (0x2), receive (0x3), open (0x4), change (0x5). Now, **everything is a state block, so all preambles are 0x6**.
|
||||||
|
|
||||||
|
A preamble could also be used to make sure Nano blocks can't be broadcasted to a Nano fork, and vice versa (prevent replay attacks). But as mentioned, Banano and Nano actually use the same preamble, so this actually isn't the case.
|
||||||
|
|
||||||
|
## Signing Blocks
|
||||||
|
|
||||||
|
The block hash must be cryptographically signed of the address to be valid. This proves that the address meant to create the block. If a block signature was not required, people could spend your funds without even having your private key (bad)!
|
||||||
|
|
||||||
|
The cryptography is complicated, but we don't need to worry about that. At a high level, signing blocks is very simple. You just need the private key and block hash, then boom, you got a signature:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def sign(private_key: str, hash: str) -> str:
|
||||||
|
#ed25519_blake2b verify
|
||||||
|
signing_key = ed25519_blake2b.SigningKey(hex_to_bytes(private_key))
|
||||||
|
signature = bytes_to_hex(signing_key.sign(hex_to_bytes(hash)))
|
||||||
|
return signature
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generating Work
|
||||||
|
|
||||||
|
The Kalium public node generates nicely generates the block work for you, but not all nodes do.
|
||||||
|
|
||||||
|
In case the node being used doesn't generate block work, we'll need to do it ourselves. Bananopie has two work generation methods, `gen_work_random` and `gen_work_deterministic`. `gen_work_deterministic` is the default, but `gen_work_random` is easier to explain and basically the same as the deterministic way, so let's look at that:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def gen_work_random(hash: str, threshold: str) -> str:
|
||||||
|
#generate work with random.randbytes()
|
||||||
|
while True:
|
||||||
|
#work is 64 bit (8 byte) nonce
|
||||||
|
#only generate 3 random bytes, first 5 are 0s. I see kalium do that I think, so I dunno if its more efficient but I copied that
|
||||||
|
#nonce = hex_to_bytes("0000000000"+bytes_to_hex(random.randbytes(3)))
|
||||||
|
nonce = random.randbytes(8)
|
||||||
|
#when blake2b hashed with the hash, should be larger than the threshold
|
||||||
|
blake_obj = blake2b(digest_size=8)
|
||||||
|
blake_obj.update(nonce)
|
||||||
|
blake_obj.update(hex_to_bytes(hash))
|
||||||
|
#since hex_to_bytes returns big endian, for BANANO_WORK, after we convert to hex, we convert to bytes with big endian
|
||||||
|
if int.from_bytes(blake_obj.digest(), byteorder="little") > int.from_bytes(hex_to_bytes(threshold), byteorder="big"):
|
||||||
|
#return as big endian
|
||||||
|
return bytes_to_hex(bytearray.fromhex(bytes_to_hex(nonce))[::-1])
|
||||||
|
```
|
||||||
|
|
||||||
|
The block work is just 8 bytes that, when added to the block hash, and hashed again, is larger than the threshold. In Banano, the threshold is `FFFFFE0000000000` (18446741874686296064). In Nano, the threshold is larger, so Nano work takes longer to generate.
|
||||||
|
|
||||||
|
Basically, we generate random bytes until the hash of the block hash plus the random bytes is greater than the threshold (18446741874686296064). If it is, we found valid work for the block. Hurray!
|
||||||
|
|
||||||
|
## Broadcasting Blocks
|
||||||
|
|
||||||
|
Nothing fancy here. We just do a ["process"](https://docs.nano.org/commands/rpc-protocol/#process) RPC call to the node, which will broadcast the block:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Wallet:
|
||||||
|
...
|
||||||
|
def send_process(self, block, subtype: str):
|
||||||
|
payload = {
|
||||||
|
"action": "process",
|
||||||
|
"subtype": subtype,
|
||||||
|
"json_block": "true",
|
||||||
|
"block": block
|
||||||
|
}
|
||||||
|
if "work" not in block:
|
||||||
|
if self.try_work:
|
||||||
|
#if opening block, there is no previous, so use public key as hash instead
|
||||||
|
if block["previous"] == "0000000000000000000000000000000000000000000000000000000000000000":
|
||||||
|
block["work"] = gen_work(self.get_public_key())
|
||||||
|
else:
|
||||||
|
block["work"] = gen_work(block["previous"])
|
||||||
|
else:
|
||||||
|
payload["do_work"] = True
|
||||||
|
return self.rpc.call(payload)
|
||||||
|
...
|
||||||
|
|
||||||
|
class RPC:
|
||||||
|
...
|
||||||
|
#send rpc calls
|
||||||
|
def call(self, payload):
|
||||||
|
headers = {}
|
||||||
|
#add auth header, if exists
|
||||||
|
if self.auth:
|
||||||
|
headers['Authorization'] = self.auth
|
||||||
|
resp = requests.post(self.rpc_url, json=payload, headers=headers)
|
||||||
|
#40x or 50x error codes returned, then there is a failure
|
||||||
|
if resp.status_code >= 400:
|
||||||
|
raise Exception("Request failed with status code "+str(resp.status_code))
|
||||||
|
resp = resp.json()
|
||||||
|
if "error" in resp:
|
||||||
|
raise Exception("Node response: "+resp["error"])
|
||||||
|
return resp
|
||||||
|
...
|
||||||
|
```
|
||||||
172
posts/golfing_and_scheming.md
Normal file
172
posts/golfing_and_scheming.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
Every December, Advent of Code releases a programming puzzle every day until Christmas. To get on the leaderboard, the goal is to solve the puzzle as soon as possible.
|
||||||
|
|
||||||
|
I'm not really into that, and I usually don't participate. Still, it's a good way to learn a new language, and it can be pretty fun if some extra challenges are imposed (eg, writing all the solutions in an esolang). This year though, I decided to use it as an excuse to go code golfing, and learn Scheme. I did days 2, 4, 6, 8, and 10.
|
||||||
|
|
||||||
|
> It feels like this is too much exposition already, but I guess I should quickly explain what code golfing is. Code golfing is trying to write code that solves a problem in the least amount of characters/bytes. There are some programming languages that are specifically made for code golfing but I feel like that's kinda cheating.
|
||||||
|
|
||||||
|
## Node.js
|
||||||
|
|
||||||
|
I initially golfed in Node.js since I thought Javascript's quirky truthy and falsy system would save characters.
|
||||||
|
|
||||||
|
My favourite solution was day 4 part 1 ([input](https://gist.github.com/jetstream0/a0381d894eabb36845ca4b587bdb0494#file-4input-txt), 169 chars):
|
||||||
|
|
||||||
|
```js
|
||||||
|
console.log((require("fs").readFileSync("4input.txt")+"").split`
|
||||||
|
C`.reduce((p,v)=>p+(v+" ").match(/[0-9]+ /g).reduce((w,c,i,a)=>i<=9?a.includes(c,10)?(w*2||1):w:w,0),0))
|
||||||
|
```
|
||||||
|
|
||||||
|
The task is to find the total points in the input. Each line of the input represents a scratchcard, with two lists of numbers separated by a "|". To the left are the winning numbers for that card, and to the right are the numbers we have. The first number we have that is also a winning number is worth one point, and every subsequent match doubles the point value: a card with 5 winning numbers is worth 16 points (`2^(5-1)` or 1,2,4,8,16), a card with 3 matches is worth 4 points, a card with no matches is worth 0.
|
||||||
|
|
||||||
|
There is also a *secret* rule! All the numbers we have are unique - there are no repeats. This means that if 4 is a winning number for a card, there will be no more than one 4 in the numbers we have. This will be important for later.
|
||||||
|
|
||||||
|
The code first reads the file, splits it into lines, then for each line, finds **all** the numbers (both the winning numbers and the numbers we have) with regex. For each number, if it is a winning number (`i<=9`, because the winning numbers are the first 10 numbers on the card), it checks if the numbers we have contain that winning number, and if so, appropriately changes the point total for that line. Finally, all the point totals for each line are added up, and the answer is logged.
|
||||||
|
|
||||||
|
Notice that **instead of checking whether the numbers we have are in the winning numbers, we do the opposite - check that the winning numbers are in the numbers we have**. This is *only* possible because of the secret rule mentioned earlier. Or well, the logic would be significantly longer if we couldn't assume only one of the winning number was present.
|
||||||
|
|
||||||
|
Some explanations about tricks in the code:
|
||||||
|
|
||||||
|
- Some functions can be called with backticks instead of parentheses; essentially, `split("a")` can be rewritten as `split\`a\``. I don't know what this is called, and it doesn't always work, but it does save two characters
|
||||||
|
- The `C` in the `split` is to make sure the last empty line of the file is ignored when splitting the file into lines (as you can see in the input, every line with a card starts with "Card ")
|
||||||
|
- The `(w,c,i,a)` in the reduce function are the accumulator, current value, current index, and array being iterated over. See [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce#syntax)
|
||||||
|
- `(require("fs").readFileSync("4input.txt")+"")` converts the return type to a string (it's shorter than `String(require("fs").readFileSync("4input.txt"))` and `require("fs").readFileSync("4input.txt","utf8")`)
|
||||||
|
- The regex that has a space at the end (`/[0-9]+ /g`) is to ensure that the card number is not counted as one of the numbers, since the card number is always followed by a colon. I found that to take up less characters than doing the `/[0-9]+/g` regex and `shift()`ing or otherwise ignoring the card number. This does create a problem, however. The last number of the line does not end with a space! The `v+" "` fixes that, by adding a space to the end of the line
|
||||||
|
- The question marks and colons are [ternary operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_operator) (a shorthand for if-else statements)
|
||||||
|
- The `a.includes(c,10)` is my favourite part of this. The second parameter of the includes function is actually the index of `a` to start searching from. So, we are ignoring the first 10 elements of the `a` array (remember? we are checking to see if the winning numbers are in the numbers we have). See [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes#syntax)
|
||||||
|
- `w*2||1` handles increasing the points if the winning number is present in the numbers we have. If `w` (the current count of points in the card) is 0, `w*2||1` will return `1`, while if `w` is non-zero, `w*2` will be returned
|
||||||
|
|
||||||
|
## Python
|
||||||
|
|
||||||
|
Surprisingly, Python was actually better for code golfing than Node.js, even though indentation is required and some standard library utils need to be imported. Python makes up for that by providing shorter function names (`len()` instead of `.length`), as well as stuff like `[1:]` instead of `.slice(1)`.
|
||||||
|
|
||||||
|
Here's day 8 ([input](https://gist.github.com/jetstream0/b9d93e734c48c06544380903fb914b8f#file-8input-txt), 181 chars):
|
||||||
|
|
||||||
|
```python
|
||||||
|
import re;a,*b=re.findall("\w+.",open("8input.txt").read());c="AAA";i=d=0
|
||||||
|
while 1:
|
||||||
|
i+=1;c=b[b.index(c+" ")+(2,1)[a[d]=="L"]][:3]
|
||||||
|
d=(d+1,0)[d==len(a)-1]
|
||||||
|
if c=="ZZZ":print(i);break
|
||||||
|
```
|
||||||
|
|
||||||
|
And here's day 10 ([input](https://gist.github.com/jetstream0/d302d53b5159265021b5c30923bdf4f7#file-10input-txt), 294 chars):
|
||||||
|
|
||||||
|
```python
|
||||||
|
f=list(open("10input.txt").read());q=[f.index("S")];v=[];n=141
|
||||||
|
while len(q)>0:
|
||||||
|
c=q[0];d=f[c];s=d=="S";a=["|LJ","|7F"];b=["-7J","-LF"];q=q[1:];v=[*v,c]
|
||||||
|
for o,w in[[-n,a],[n,a[::-1]],[-1,b],[1,b[::-1]]]:
|
||||||
|
if 0<c+o<len(f):
|
||||||
|
if([d,f[c+o]][s]in w[s])&((c+o in v)^1):q.append(c+o)
|
||||||
|
print(len(v)//2)
|
||||||
|
```
|
||||||
|
|
||||||
|
Eaz later pointed out to me that `c=q[0];q=q[1:];` could be rewritten as `c=q.pop(0);`, saving some more characters. After some more tweaking, he trimmed his version of my solution to 271 characters (!).
|
||||||
|
|
||||||
|
With help from Eaz, here's a 253 char version that somehow works on Python 3.11:
|
||||||
|
|
||||||
|
```python
|
||||||
|
f=open("10input.txt").read();q=v=[f.find("S")];n=141
|
||||||
|
while q:d=f[c:=q.pop(0)];a=["|LJ","|7F"];b=["-7J","-LF"];v+=[c];q=[c+o for o,w in[[-n,a],[n,a[::-1]],[-1,b],[1,b[::-1]]]if 0<c+o<len(f)if([d,f[c+o]][s:=d=="S"]in w[s])&((c+o in v)^1)]
|
||||||
|
print(len(v)//2)
|
||||||
|
```
|
||||||
|
|
||||||
|
In day 8, we're using regex to find all the words, storing the first word (the instructions to go left and right) into `a` and the rest into the list `b`. We set the current location as "AAA" and go into a loop. In every iteration of the loop, we look for the position of the current location with `index` (which should be replaced with `find` to save 1 character). The `+" "` takes advantage of the fact that the input is formatted as `SVN = (JGN, FSL)`. The two connected locations end with `,` and `)`, while what we want, the actual location, ends with a space. Based on whether the current instruction is a left or a right, we make either the left or right connection the current location. We increment the instruction location, wrapping back to the beginning if we reached the last instruction. If the current location is "ZZZ", we have reached the end!
|
||||||
|
|
||||||
|
If you read the problems, and know that `[1,2][False] == 1` and `[1,2][True] == 2` (basically, a ternary operator), the code for day 8 should be reasonably understandable. At least for a code golf.
|
||||||
|
|
||||||
|
In day 10, we start at "S", and need to find the length of the connecting pipes (which are in one continuous loop). With that, we can do `//2` to find the distance the pipe furthest from the start is. To do this, we are setting up a queue list (`q`) of locations (of connecting pipes) to visit, and a visited (`v`) list of pipe locations we have already visited. In the loop, we are checking the current pipe's surrounding pipes, adding those that connect to our current pipe and have not already been visited to the queue.
|
||||||
|
|
||||||
|
Some notes about day 10:
|
||||||
|
|
||||||
|
- `v=[*v,c]` is a shortcut for `v.append(c)`\
|
||||||
|
- 141 is the length of the lines in the input. Since we store locations not as a x,y coord, but rather just a char position in the entire file, this is important
|
||||||
|
- `a=["|LJ","|7F"];b=["-7J","-LF"];` and `[[-n,a],[n,a[::-1]],[-1,b],[1,b[::-1]]]` help the logic figure out which pipes connect to which. I don't really want to explain more deeply, just read the code
|
||||||
|
- The `^1` is a "not"
|
||||||
|
|
||||||
|
## Scheme Lisp
|
||||||
|
|
||||||
|
Here's my Scheme solution for day 6 part 1:
|
||||||
|
|
||||||
|
```scheme
|
||||||
|
;extract a list of numbers from the line
|
||||||
|
(define extract-numbers (lambda (line)
|
||||||
|
(define extract-numbers-tail (lambda (line index current-number-string number-list)
|
||||||
|
;string->number list->string
|
||||||
|
(if (< index (string-length line))
|
||||||
|
(begin
|
||||||
|
(if (char-numeric? (string-ref line index))
|
||||||
|
(extract-numbers-tail line (+ index 1) (string-append current-number-string (list->string (cons (string-ref line index) '()))) number-list)
|
||||||
|
(begin
|
||||||
|
(if (string=? current-number-string "")
|
||||||
|
(extract-numbers-tail line (+ index 1) "" number-list)
|
||||||
|
(extract-numbers-tail line (+ index 1) "" (cons (string->number current-number-string) number-list))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(begin
|
||||||
|
(if (string=? current-number-string "")
|
||||||
|
number-list
|
||||||
|
(cons (string->number current-number-string) number-list)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
(reverse (extract-numbers-tail line 0 "" '()))
|
||||||
|
))
|
||||||
|
(define count-and-mul-wins (lambda (time-list distance-list wins-mul)
|
||||||
|
(define count-wins (lambda (time distance secs wins)
|
||||||
|
(if (< secs time)
|
||||||
|
(if (> (* secs (- time secs)) distance)
|
||||||
|
(count-wins time distance (+ secs 1) (+ wins 1))
|
||||||
|
(count-wins time distance (+ secs 1) wins)
|
||||||
|
)
|
||||||
|
wins
|
||||||
|
)
|
||||||
|
))
|
||||||
|
;check to make sure lists aren't empty
|
||||||
|
(if (= (length time-list) 0)
|
||||||
|
wins-mul
|
||||||
|
(let ([wins (count-wins (car time-list) (car distance-list) 0 0)])
|
||||||
|
(if (= wins-mul 0)
|
||||||
|
(count-and-mul-wins (cdr time-list) (cdr distance-list) wins)
|
||||||
|
(count-and-mul-wins (cdr time-list) (cdr distance-list) (* wins wins-mul))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
(define file (open-input-file "6input.txt"))
|
||||||
|
(let* ([line1 (get-line file)] [line2 (get-line file)])
|
||||||
|
(display (count-and-mul-wins (extract-numbers line1) (extract-numbers line2) 0))
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
In comparison, here's my Node.js solution ([input](https://gist.github.com/jetstream0/9241c005d12c039f296a5c53a66236e6#file-6input-txt), 149 chars):
|
||||||
|
|
||||||
|
```js
|
||||||
|
console.log((require("fs").readFileSync("6input.txt")+"").match(/\d+/g).reduce((p,c,i,a)=>{for(o=j=0;j<+c;)j*(c-j++)>+a[i+4]&&o++;return o?p*o:p},1))
|
||||||
|
```
|
||||||
|
|
||||||
|
Yeah, Scheme isn't ideal for code golfing (maybe with macros?). That's fine, since with Scheme, I was just trying to learn the language, not golf.
|
||||||
|
|
||||||
|
It's a bit of a mess since I had no Scheme (or Lisp) experience before I wrote that, unless you count <5 minutes of fiddling around with [try.scheme.org](https://try.scheme.org).
|
||||||
|
|
||||||
|
With the help of [The Scheme Programming Language, 4th Edition](https://www.scheme.com/tspl4/) (a wonderful book), and [Scheme Programming](https://en.wikibooks.org/wiki/Scheme_Programming) to figure out where to get started, everything went smoother than expected.
|
||||||
|
|
||||||
|
I was initially disappointed to see Scheme missing many "basic" utility functions (eg, a string split function), and considered switching to something like Racket instead. But after just a few minutes I fell deep in love. The syntax was clean and beautiful, and rewriting "basic" functions was incredibly fun. Scheme gives enough of a base to be useful (managing memory, records, lists, some string operations...), but leaves the joy of most everything else up to the programmer. As y'all know, I like implementing things from close to scratch, and hate bloat. It's a perfect match.
|
||||||
|
|
||||||
|
It's been a while since I had so much fun programming. To further learn Scheme, I then wrote my first project in Scheme, [rescli](https://github.com/jetstream0/rescli), a CLI interface for [Reservoir](https://github.com/jetstream0/reservoir), a bookmark organizer app I made. I'll talk about those in a different post.
|
||||||
|
|
||||||
|
I also tried to learn Ocaml but just couldn't get over the horrible, horrible, syntax. Maybe I'll try again later (ReasonML's more familiar syntax might help).
|
||||||
|
|
||||||
|
## Why Learn Scheme?
|
||||||
|
|
||||||
|
But anyways, why did I choose to learn Scheme?
|
||||||
|
|
||||||
|
After writing Mingde, and a few Rust projects, I was thinking a lot about types. Specifically, how much I loved them (a lot). I was also a bit curious about functional programming.
|
||||||
|
|
||||||
|
Lisp Scheme is dynamically, not statically, typed (Typed Racket, which is similar, does though), which was a downside. But it's simplicity and lack of bloat was compelling enough for me to give it a try. I'm glad I did. Again, writing in Scheme has been the most fun I've had programming in months, possibly years. Functional programming really is a different, more fun, and arguably better way to think about programming. Avoiding variable mutation and recursing instead of iterating is just... fun. Having the entire language reference as a 3.3 mb PDF is pretty neat too. And I haven't even written a macro yet!
|
||||||
|
|
||||||
|
S-expressions (the name for all those parentheses) are really simple to understand, and everything in general is pretty simple to understand. Do a brief read of Scheme Programming, then TSPL4 for reference.
|
||||||
|
|
||||||
|
You should learn Scheme too. It's simple. It's fun. Do it now.
|
||||||
@@ -70,7 +70,7 @@ builder.serve_template(renderer, "/", "index.html", {
|
|||||||
notices: [
|
notices: [
|
||||||
"Dave got drunk again and fed the chipmunks. As a result, they are more brazen than usual. Be on your guard!",
|
"Dave got drunk again and fed the chipmunks. As a result, they are more brazen than usual. Be on your guard!",
|
||||||
"Please stop anthropomorphizing the rocks. They WILL come alive.",
|
"Please stop anthropomorphizing the rocks. They WILL come alive.",
|
||||||
"Oxygen has decided to retire. Until we find a replacement for him, there will be oxygen and water shortages.",
|
"Oxygen has decided to retire. Until we find a replacement for him, do not be selfish in your consumption of water and air.",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Here's a very incomplete (and maybe actively updated) list of ones that led to m
|
|||||||
- [Oda Nobunaga](https://en.wikipedia.org/wiki/Oda_Nobunaga), extremely notable Japanese warlord, who was killed in the [Honnō-ji Incident](https://en.wikipedia.org/wiki/Honn%C5%8D-ji_Incident)
|
- [Oda Nobunaga](https://en.wikipedia.org/wiki/Oda_Nobunaga), extremely notable Japanese warlord, who was killed in the [Honnō-ji Incident](https://en.wikipedia.org/wiki/Honn%C5%8D-ji_Incident)
|
||||||
- [Ishiyama Hongan-ji](https://en.wikipedia.org/wiki/Ishiyama_Hongan-ji), former [Jōdo Shinshū](https://en.wikipedia.org/wiki/J%C5%8Ddo_Shinsh%C5%AB) temple/fortress, was burned down and replaced by [Osaka Castle](https://en.wikipedia.org/wiki/Osaka_Castle), and the reason why the city of [Osaka](https://en.wikipedia.org/wiki/Osaka) exists
|
- [Ishiyama Hongan-ji](https://en.wikipedia.org/wiki/Ishiyama_Hongan-ji), former [Jōdo Shinshū](https://en.wikipedia.org/wiki/J%C5%8Ddo_Shinsh%C5%AB) temple/fortress, was burned down and replaced by [Osaka Castle](https://en.wikipedia.org/wiki/Osaka_Castle), and the reason why the city of [Osaka](https://en.wikipedia.org/wiki/Osaka) exists
|
||||||
- [Peninsular War](https://en.wikipedia.org/wiki/Peninsular_War), [Napoleon](https://en.wikipedia.org/wiki/Napoleon)'s invasion of Spain and Portugal
|
- [Peninsular War](https://en.wikipedia.org/wiki/Peninsular_War), [Napoleon](https://en.wikipedia.org/wiki/Napoleon)'s invasion of Spain and Portugal
|
||||||
- [Thomas Cochrane, 10th Earl of Dundonald](https://en.wikipedia.org/wiki/Thomas_Cochrane,_10th_Earl_of_Dundonald), successful British Navy officer accused of stock exchange fraud, later participating in [Liberating Expedition of Peru](https://en.wikipedia.org/wiki/Liberating_Expedition_of_Peru) from the Spanish
|
- [Thomas Cochrane, 10th Earl of Dundonald](https://en.wikipedia.org/wiki/Thomas_Cochrane,_10th_Earl_of_Dundonald), successful British Navy officer accused of stock exchange fraud, and later participated in the [Liberating Expedition of Peru](https://en.wikipedia.org/wiki/Liberating_Expedition_of_Peru) from the Spanish
|
||||||
- [Indonesia invades East Timor](https://en.wikipedia.org/wiki/Indonesian_invasion_of_East_Timor) to overthrow [Fretilin](https://en.wikipedia.org/wiki/Fretilin)
|
- [Indonesia invades East Timor](https://en.wikipedia.org/wiki/Indonesian_invasion_of_East_Timor) to overthrow [Fretilin](https://en.wikipedia.org/wiki/Fretilin)
|
||||||
- [Special Region of Yogyakarta](https://en.wikipedia.org/wiki/Special_Region_of_Yogyakarta), a region of Indonesia *currently* hereditarily ruled by the [Yogyakarta Sultanate](https://en.wikipedia.org/wiki/Yogyakarta_Sultanate) and the [Duchy of Pakualaman](https://en.wikipedia.org/wiki/Pakualaman)
|
- [Special Region of Yogyakarta](https://en.wikipedia.org/wiki/Special_Region_of_Yogyakarta), a region of Indonesia *currently* hereditarily ruled by the [Yogyakarta Sultanate](https://en.wikipedia.org/wiki/Yogyakarta_Sultanate) and the [Duchy of Pakualaman](https://en.wikipedia.org/wiki/Pakualaman)
|
||||||
- [Nanboku-chō period](https://en.wikipedia.org/wiki/Nanboku-ch%C5%8D_period), when two opposing Japanese Imperial Courts existed, after the overthrow of the [Kamakura Shogunate](https://en.wikipedia.org/wiki/Kamakura_shogunate) and the failure of the [Kenmu Restoration](https://en.wikipedia.org/wiki/Kenmu_Restoration)
|
- [Nanboku-chō period](https://en.wikipedia.org/wiki/Nanboku-ch%C5%8D_period), when two opposing Japanese Imperial Courts existed, after the overthrow of the [Kamakura Shogunate](https://en.wikipedia.org/wiki/Kamakura_shogunate) and the failure of the [Kenmu Restoration](https://en.wikipedia.org/wiki/Kenmu_Restoration)
|
||||||
@@ -114,7 +114,7 @@ Here's a very incomplete (and maybe actively updated) list of ones that led to m
|
|||||||
- [Simón Bolívar](https://en.wikipedia.org/wiki/Sim%C3%B3n_Bol%C3%ADvar), South American revolutionary hero
|
- [Simón Bolívar](https://en.wikipedia.org/wiki/Sim%C3%B3n_Bol%C3%ADvar), South American revolutionary hero
|
||||||
- [Battle of Dibrivka](https://en.wikipedia.org/wiki/Battle_of_Dibrivka)
|
- [Battle of Dibrivka](https://en.wikipedia.org/wiki/Battle_of_Dibrivka)
|
||||||
- [Battle of the Teutoburg Forest](https://en.wikipedia.org/wiki/Battle_of_the_Teutoburg_Forest)
|
- [Battle of the Teutoburg Forest](https://en.wikipedia.org/wiki/Battle_of_the_Teutoburg_Forest)
|
||||||
- [Intentional Community](https://en.wikipedia.org/wiki/Intentional_community)
|
- [Intentional community](https://en.wikipedia.org/wiki/Intentional_community)
|
||||||
- [Jellyfish](https://en.wikipedia.org/wiki/Jellyfish) are apparently "the informal common names given to the medusa-phase of certain gelatinous members of the subphylum Medusozoa"
|
- [Jellyfish](https://en.wikipedia.org/wiki/Jellyfish) are apparently "the informal common names given to the medusa-phase of certain gelatinous members of the subphylum Medusozoa"
|
||||||
- [Sinecure](https://en.wikipedia.org/wiki/Sinecure), get paid to do nothing
|
- [Sinecure](https://en.wikipedia.org/wiki/Sinecure), get paid to do nothing
|
||||||
- [Yelü Dashi](https://en.wikipedia.org/wiki/Yel%C3%BC_Dashi), founder of the Western Liao dynasty
|
- [Yelü Dashi](https://en.wikipedia.org/wiki/Yel%C3%BC_Dashi), founder of the Western Liao dynasty
|
||||||
@@ -205,3 +205,5 @@ Here's a very incomplete (and maybe actively updated) list of ones that led to m
|
|||||||
- [Cato the Younger](https://en.wikipedia.org/wiki/Cato_the_Younger), Roman politician
|
- [Cato the Younger](https://en.wikipedia.org/wiki/Cato_the_Younger), Roman politician
|
||||||
- [Cicero](https://en.wikipedia.org/wiki/Cicero), prolific writer and Roman politician
|
- [Cicero](https://en.wikipedia.org/wiki/Cicero), prolific writer and Roman politician
|
||||||
- [Moro people](https://en.wikipedia.org/wiki/Moro_people)
|
- [Moro people](https://en.wikipedia.org/wiki/Moro_people)
|
||||||
|
- [Bogd Khan](https://en.wikipedia.org/wiki/Bogd_Khan)
|
||||||
|
- [Pancho Villa Expedition](https://en.wikipedia.org/wiki/Pancho_Villa_Expedition)
|
||||||
|
|||||||
Reference in New Issue
Block a user