Monday, January 16, 2023

Project Gemini

Project Gemini is a neat modern take on the Gopher protocol. You can read the Gemini FAQ or the Gemini specification to learn more details, but the home page has a nice summary:

Gemini is a new internet protocol which

  • Is heavier than gopher
  • Is lighter than the web
  • Will not replace either
  • Strives for maximum power to weight ratio
  • Takes user privacy very seriously

There are some nice Gemini clients implemented in various languages, for both the command-line and with nice user interfaces. I happen to enjoy using AV-98 and Lagrange, but many others are also great.

In a similar manner to my Gopher implementation in Factor, I recently implemented the Gemini protocol as well as a Gemini server and a Gemini user interface:

Instead of going into how the protocol or the user interface is implemented, I wanted to go over the Gemini command-line interface. In the spirit of Python's cmd module, I contributed the command-loop vocabulary to support generic line-oriented command interpreters.

We start by making a sequence of commands that our Gemini interpreter will support:

CONSTANT: COMMANDS {
    T{ command
        { name "back" }
        { quot [ drop gemini-back ] }
        { help "Go back to the previous Gemini URL." }
        { abbrevs { "b" } } }
    T{ command
        { name "forward" }
        { quot [ drop gemini-forward ] }
        { help "Go forward to the next Gemini URL." }
        { abbrevs { "f" } } }
    T{ command
        { name "history" }
        { quot [ drop gemini-history ] }
        { help "Display recently viewed Gemini URLs." }
        { abbrevs { "h" "hist" } } }
    T{ command
        { name "less" }
        { quot [ drop gemini-less ] }
        { help "View the most recent Gemini URL in a pager." }
        { abbrevs { "l" } } }
    T{ command
        { name "ls" }
        { quot [ gemini-ls ] }
        { help "List the currently available links." }
        { abbrevs f } }
    T{ command
        { name "go" }
        { quot [ gemini-go ] }
        { help "Go to a Gemini URL" }
        { abbrevs { "g" } } }
    T{ command
        { name "gus" }
        { quot [ drop "gemini://gus.guru/search" gemini-go ] }
        { help "Submit a query to the GUS search engine." }
        { abbrevs f } }
    T{ command
        { name "up" }
        { quot [ drop gemini-up ] }
        { help "Go up one directory from the recent Gemini URL." }
        { abbrevs { "u" } } }
    T{ command
        { name "url" }
        { quot [ drop gemini-url ] }
        { help "Print the most recent Gemini URL." }
        { abbrevs f } }
    T{ command
        { name "reload" }
        { quot [ drop gemini-reload ] }
        { help "Reload the most recent Gemini URL." }
        { abbrevs { "r" } } }
    T{ command
        { name "root" }
        { quot [ drop gemini-root ] }
        { help "Navigate to the most recent Gemini URL's root." }
        { abbrevs f } }
    T{ command
        { name "shell" }
        { quot [ gemini-shell ] }
        { help "'cat' the most recent Gemini URL through a shell." }
        { abbrevs { "!" } } }
    T{ command
        { name "quit" }
        { quot [ drop gemini-quit ] }
        { help "Quit the program." }
        { abbrevs { "q" "exit" } } }
}

And then we define a custom command-loop that will allow us to number the links on a Gemini page, and then by typing a number we can navigate to one of the links by detecting a "missing command":

TUPLE: gemini-command-loop < command-loop ;

M: gemini-command-loop missing-command
    over string>number [ 1 - LINKS ?nth ] [ f ] if* [
        gemini-go 3drop
    ] [
        call-next-method
    ] if* ;

You can see it in action:

$ ./factor -run=gemini.cli
Welcome to Project Gemini!
GEMINI> go gemini.circumlunar.space/news/

Official Project Gemini news feed

[1] Atom feed

2023 News

[2] 2023-01-14 - Tidying up gemini.circumlunar.space user capsules
[3] 2023-01-08 - Changing DNS server

2022 News

[4] 2022-06-20 - Three years of Gemini!
[5] 2022-01-30 - Minor specification update (0.16.1)
[6] 2022-01-22 - Mailing list archives, Atom feed for official news
[7] 2022-01-16 - Mailing list downtime, official news feed

Sunday, January 15, 2023

Guided Tour of Factor

Many years ago, Andrea Ferretti created a Factor tutorial which I had written about at the time.

One of our new core developers, Raghu Ranganathan, got permission to include the tutorial in the main Factor repository, and has reformatted it and updated it for the not-yet-released version 0.99 as the Guided tour of Factor.

You can access it in a recent nightly build by doing:

IN: scratchpad "tour" help

Check it out!

Friday, February 4, 2022

Speedrun Feedback

Recently, Tomasz Wegrzanowski chose Factor for his 100 Languages Speedrun: Episode 71: Factor and it encouraged us to make some improvements that I wanted to describe.

Many of our users use the Factor environment through the UI developer tools or on the command-line with the listener. Another important use case is being able to eval and run scripts -- and this is where much of Tomasz' criticism was focused.

We now do command-line eval and run scripts with auto-use? enabled. This will be available in the nightly builds and as part of an upcoming 0.99 release.

So this works now:

$ ./factor -e="1 2 + ."
3

$ cat foo.factor
USE: io
"Hello World" print
12

$ ./factor foo.factor
Hello World

--- Data stack:
12

Previously, the first example would error with a "No word named “+” found in current vocabulary search path" and the second example would complain that the "Quotation's stack effect does not match call site" because the script did not have a ( -- ) stack effect.

I appreciate that some users approach Factor differently than I do, and we love getting feedback. I wish we could solve the name conflict with factor(1), but that is more challenging.

We may adjust this slightly as it just landed last night, and if anyone has further suggestions, please keep them coming!

Tuesday, July 31, 2018

Factor 0.98 now available

“Even though you're growing up, you should never stop having fun.” - Nina Dobrev

I'm very pleased to announce the release of Factor 0.98!

OS/CPUWindowsMac OS XLinux
x86
x86-64

Source code: 0.98

This release is brought to you with almost 4,300 commits by the following individuals:

Alexander Ilin, Arkady Rost, Benjamin Pollack, Björn Lindqvist, Cat Stevens, Chris Double, Dimage Sapelkin, Doug Coleman, Friedrich von Never, John Benediktsson, Jon Harper, Mark Green, Mark Sweeney, Nicolas Pénet, Philip Dexter, Robert Vollmert, Samuel Tardieu, Sankaranarayanan Viswanathan, Shane Pelletier, @catb0t, @hyphz, @thron7, @xzenf

Besides several years of bug fixes and library improvements, I want to highlight the following changes:

  • Improved user interface with light and dark themes and new icons
  • Fix GTK library issue affecting some Linux installations
  • Support Cocoa TouchBar on MacOS
  • Factor REPL version banner includes build information.
  • Bindings for ForestDB
  • New graphical demos including Minesweeper, Game of Life, Bubble Chamber, etc.
  • Better handling of "out of memory" errors
  • Improved VM and compiler documentation, test fixtures, and bug fixes
  • Much faster Heaps and Heapsort
  • Support for Adobe Brackets, CotEditor, and Microsoft Visual Studio Code editors
  • On Mac OS X, allow use of symlinks to factor binary
  • Lots of improvements to FUEL (Factor's emacs mode)

Some possible backwards compatibility issues:

  • Flattened unicode namespace (either USE: ascii or USE: unicode).
  • Unified CONSTRUCTOR: syntax to include generated word name
  • Returning a struct by value with two register-sized values on 64bit now works correctly
  • Since shutdown hooks run first, calling exit will now unconditionally exit even if there is an error
  • On Windows, don't call cd to change directory when launching processes; there is another mechanism for that
  • In libc, renamed (io-error) to throw-errno
  • In match, renamed ?1-tail to ?rest
  • In sequences, renamed iota to <iota>
  • In sequences, renamed start/start* to subseq-start/subseq-start-from
  • In syntax, renamed GENERIC# to GENERIC#:
  • Improve command-line argument parsing of "executable"
  • Make buffered-port not have a length, because of problem with Linux virtual files and TCP sockets
  • Fix broken optimization that made floats work for integer keys in case statements
  • Growable sequences expand by factor of 2 (instead of 3) when growing
  • Removed support for "triple-quote" strings

What is Factor

Factor is a concatenative, stack-based programming language with high-level features including dynamic types, extensible syntax, macros, and garbage collection. On a practical side, Factor has a full-featured library, supports many different platforms, and has been extensively documented.

The implementation is fully compiled for performance, while still supporting interactive development. Factor applications are portable between all common platforms. Factor can deploy stand-alone applications on all platforms. Full source code for the Factor project is available under a BSD license.


New Libraries:


Improved Libraries:

Monday, February 12, 2018

Minesweeper

Minesweeper is a fun game that was probably made most popular by its inclusion in various Microsoft Windows versions since the early 1990's.

I thought it would be fun to build a simple Minesweeper clone using Factor.

You can run this by updating to the latest code and running:

IN: scratchpad "minesweeper" run

Game Engine

We are going to represent our game grid as a two-dimensional array of "cells".

Each cell contains the number of mines contained in the (up to eight) adjacent cells, whether the cell contains a mine, and a "state" flag showing whether the cell was +clicked+, +flagged+, or marked with a +question+ mark.

SYMBOLS: +clicked+ +flagged+ +question+ ;

TUPLE: cell #adjacent mined? state ;

Making a (rows, cols) grid of cells:

: make-cells ( rows cols -- cells )
    '[ _ [ cell new ] replicate ] replicate ;

We can lookup a particular cell using its (row, col) index:

:: cell-at ( cells row col -- cell/f )
    row cells ?nth [ col swap ?nth ] [ f ] if* ;

Placing a number of mines into cells, just looks for a certain number of unmined cells at random, and then marks them as mined:

: unmined-cell ( cells -- cell )
    f [ dup mined?>> ] [ drop dup random random ] do while nip ;

: place-mines ( cells n -- cells )
    [ dup unmined-cell t >>mined? drop ] times ;

We can count the number of adjacent mines for each cell, by looking at its neighbors:

CONSTANT: neighbors {
    { -1 -1 } { -1  0 } { -1  1 }
    {  0 -1 }           {  0  1 }
    {  1 -1 } {  1  0 } {  1  1 }
}

: adjacent-mines ( cells row col -- #mines )
    neighbors [
        first2 [ + ] bi-curry@ bi* cell-at
        [ mined?>> ] [ f ] if*
    ] with with with count ;

The each-cell word looks at all the cells, helping us update the "adjacent mines" counts:

:: each-cell ( ... cells quot: ( ... row col cell -- ... ) -- ... )
    cells [| row |
        [| cell col | row col cell quot call ] each-index
    ] each-index ; inline

:: update-counts ( cells -- cells )
    cells [| row col cell |
        cells row col adjacent-mines cell #adjacent<<
    ] each-cell cells ;

Since we aren't storing the number of rows and columns, we can get it from the array of cells:

: cells-dim ( cells -- rows cols )
    [ length ] [ first length ] bi ;

We can get the number of mines contained in the grid by counting them up:

: #mines ( cells -- n )
    [ [ mined?>> ] count ] map-sum ;

We can reset the game by making new cells and then placing the same number of mines in them:

: reset-cells ( cells -- cells )
    [ cells-dim make-cells ] [ #mines place-mines ] bi update-counts ;

The player wins if they click on all cells that aren't mines:

: won? ( cells -- ? )
    [ [ { [ state>> +clicked+ = ] [ mined?>> ] } 1|| ] all? ] all? ;

The player loses if they click on any cell that's a mine:

: lost? ( cells -- ? )
    [ [ { [ state>> +clicked+ = ] [ mined?>> ] } 1&& ] any? ] any? ;

And then the game is over if the player either wins or loses:

: game-over? ( cells -- ? )
    { [ lost? ] [ won? ] } 1|| ;

We can tell this is a new game if no cells are clicked on:

: new-game? ( cells -- ? )
    [ [ state>> +clicked+ = ] any? ] any? not ;

When we click on a cell, if it is not adjacent to any mines, we click on all the "clickable" (non-mined) cells around it:

DEFER: click-cell-at

:: click-cells-around ( cells row col -- )
    neighbors [
        first2 [ row + ] [ col + ] bi* :> ( row' col' )
        cells row' col' cell-at [
            mined?>> [
                cells row' col' click-cell-at
            ] unless
        ] when*
    ] each ;

Handle clicking a cell. If it's the first click and the cell is mined, we move it to another random cell, then continue with the click. The click is ignored if the cell was already clicked or flagged. Continue clicking around any cells that have no adjacent mines and are not themselves mined.

:: click-cell-at ( cells row col -- )
    cells row col cell-at [
        cells new-game? [
            ! first click shouldn't be a mine
            dup mined?>> [
                cells unmined-cell t >>mined? drop f >>mined?
                cells update-counts drop
            ] when
        ] when
        dup state>> { +clicked+ +flagged+ } member? [ drop ] [
            +clicked+ >>state
            { [ mined?>> not ] [ #adjacent>> 0 = ] } 1&& [
                cells row col click-cells-around
            ] when
        ] if
    ] when* ;

Handle marking a cell. First by flagging it as a likely mine, or marking with a question mark to come back to later. If the cell is not clicked, we just cycle through flagging, question, or not clicked.

:: mark-cell-at ( cells row col -- )
    cells row col cell-at [
        dup state>> {
            { +clicked+ [ +clicked+ ] }
            { +flagged+ [ +question+ ] }
            { +question+ [ f ] }
            { f [ +flagged+ ] }
        } case >>state drop
    ] when* ;

Graphical Interface

Our graphical interface is going to consist of a gadget with an array of cells and a cache of OpenGL texture objects that can be easily drawn on the screen.

TUPLE: grid-gadget < gadget cells textures ;

When you make a new grid-gadget, it initializes the game to a specified number of rows, columns, and number of mines:

:: <grid-gadget> ( rows cols mines -- gadget )
    grid-gadget new
        rows cols make-cells
        mines place-mines update-counts >>cells
        H{ } clone >>textures
        COLOR: gray <solid> >>interior ;

When ungraft* is called to indicate the gadget is no longer visible on the screen, we clean up the cached textures:

M: grid-gadget ungraft*
    dup find-gl-context
    [ values dispose-each H{ } clone ] change-textures
    call-next-method ;

Our images are going to be 32 x 32 squares, so the preferred dimension is number of rows and columns times 32 pixels for each square.

M: grid-gadget pref-dim*
    cells>> cells-dim [ 32 * ] bi@ swap 2array ;

Some slightly complex logic to decide which image to display for each cell, taking into account whether the game is over so we can show the positions of all the mines and whether the player was correct in flagging a cell as mined, etc:

:: cell-image-path ( cell game-over? -- image-path )
    game-over? cell mined?>> and [
        cell state>> +clicked+ = "mineclicked.gif" "mine.gif" ?
    ] [
        cell state>>
        {
            { +question+ [ "question.gif" ] }
            { +flagged+ [ game-over? "misflagged.gif" "flagged.gif" ? ] }
            { +clicked+ [
                cell mined?>> [
                    "mine.gif"
                ] [
                    cell #adjacent>> 0 or number>string
                    "open" ".gif" surround
                ] if ] }
            { f [ "blank.gif" ] }
        } case
    ] if "vocab:minesweeper/_resources/" prepend ;

Drawing a cached texture is a matter of looking up the image in our texture cache and then rendering to the screen:

: draw-cached-texture ( path gadget -- )
    textures>> [ load-image { 0 0 } <texture> ] cache
    [ dim>> ] [ draw-scaled-texture ] bi ;

Drawing our gadget, is basically drawing all of the cells at their proper locations on the screen:

M:: grid-gadget draw-gadget* ( gadget -- )
    gadget cells>> game-over? :> game-over?
    gadget cells>> [| row col cell |
        col row [ 32 * ] bi@ 2array [
            cell game-over? cell-image-path
            gadget draw-cached-texture
        ] with-translation
    ] each-cell ;

Basic handling for the gadget being left-clicked on:

:: on-click ( gadget -- )
    gadget hand-rel first2 :> ( w h )
    h w [ 32 /i ] bi@ :> ( row col )
    gadget cells>> :> cells
    cells game-over? [
        cells row col click-cell-at
    ] unless gadget relayout-1 ;

Basic handling for the gadget being right-clicked on:

:: on-mark ( gadget -- )
    gadget hand-rel first2 :> ( w h )
    h w [ 32 /i ] bi@ :> ( row col )
    gadget cells>> :> cells
    cells game-over? [
        cells row col mark-cell-at
    ] unless gadget relayout-1 ;

Logic for creating new games of varying difficulties: easy, medium, and hard:

: new-game ( gadget rows cols mines -- )
    [ make-cells ] dip place-mines update-counts >>cells
    relayout-window ;

: com-easy ( gadget -- ) 7 7 10 new-game ;

: com-medium ( gadget -- ) 15 15 40 new-game ;

: com-hard ( gadget -- ) 15 30 99 new-game ;

We set our gesture handlers for keyboard and mouse inputs:

grid-gadget {
    { T{ key-down { sym "1" } } [ com-easy ] }
    { T{ key-down { sym "2" } } [ com-medium ] }
    { T{ key-down { sym "3" } } [ com-hard ] }
    { T{ button-up { # 1 } } [ on-click ] }
    { T{ button-up { # 3 } } [ on-mark ] }
    { T{ key-down { sym " " } } [ on-mark ] }
} set-gestures

And a main word that creates an easy game in our grid-gadget and opens it in a new window:

MAIN-WINDOW: run-minesweeper {
        { title "Minesweeper" }
        { window-controls
            { normal-title-bar close-button minimize-button } }
    } 7 7 10 <grid-gadget> >>gadgets ;

The implementation above is about 200 lines of code and contains the full game logic. The final version is just under 300 lines of code, and adds:

  • support for a toolbar to easily start new games
  • the traditional counter of the number of mines remaining
  • display of the number of seconds elapsed
  • a smiley face showing a funny "uh-oh!" face when you are about to click as well as winning and losing smileys
  • support for retina displays using 2x images

Saturday, February 11, 2017

$7.11

Today, someone blogged about a fun problem:

“A mathematician purchased four items in a grocery store. He noticed that when he added the prices of the four items, the sum came to $7.11, and when he multiplied the prices of the four items, the product came to $7.11.”

In some ways, this is similar to the SEND + MORE = MONEY problem that I blogged about awhile ago. You can always approach this problem with an direct and iterative solution, but instead we will use the backtrack vocabulary to solve this problem with less code.

We'll be solving this exactly, using integer "numbers of cents", progressively restricting the options, and then calling fail if the solution is not found, so we check the next. The first valid solution will be returned:

:: solve-711 ( -- seq )
    711 <iota> amb-lazy :> w
    711 w - <iota> amb-lazy :> x
    711 w - x - <iota> amb-lazy :> y
    711 w - x - y - :> z

    w x * y * z * 711,000,000 = [ fail ] unless

    { w x y z } ;

Using it, we get our answer:

IN: scratchpad solve-711 .
{ 120 125 150 316 }

And that is: $1.20, $1.25, $1.50, and $3.16.

Sunday, February 5, 2017

Dirty Money: Code Challenge

There's a fun coding challenge to follow the dirty money that I discovered recently.

A shady Internet business has been discovered.

The website has been made public by a whistle blower. We have enough evidence about the dirty deals they did. But to charge them we need to get hands on precise numbers about the transactions that happened on their platform.

Unfortunately no record of the transactions could be seized so far. The only hint we have is this one transaction:

fd0d929f-966f-4d1a-89cd-feee5a1c5347.json

What we need is the total of all transactions in Dollar. Can you trace down all other transactions and get the total?

Be careful to count each transaction only once, even if it is linked multiple times. You can use whatever tool works best.

We need a way to extract the dollar amount from the transaction text. The dollars might be specified with period or a comma to represent the decimal point. We use regular expressions to look for the dollar amount and then convert to a number.

: dollars ( str -- $ )
    R/ \$\d*[,.]\d+/ first-match rest
    "," "." replace string>number ;

We will use a hash-set of visited links, and only if the link has not been visited will we http-get the contents of the URL, parse the JSON, and extract the dollar amount of both the transaction and any links it contains. The set of visited links will tell us how many total transactions we traced.

:: transaction ( url visited -- dollars )
    url visited ?adjoin [
        url http-get nip json> :> data
        data "content" of dollars
        data "links" of [ visited transaction ] map-sum +
    ] [ 0 ] if ;

: transactions ( url -- dollars #transactions )
    HS{ } clone [ transaction ] [ cardinality ] bi ;

That's all we need to solve the problem. We can run this with the initial URL and get the answer:

$9064.79 in 50 transactions.