Sorting posts by season

Probably the most “WordPress-y” approach to such a problem is to store the date of your 'event' custom post-type in the post’s metadata. Enabling users to set meta-data on a post is usually accomplished by using the 'register_meta_box_cb' argument to the register_post_type() function to add an extra “meta box” interface to the pages for creating or editing a post on the dashboard. Alternately, you can enable custom fields support for the post-type, then check the box for “Custom Fields” in the screen options when creating or editing an 'event', after which you can manually enter the key ('event_date' would be a good meta-key) and value in the “Custom Fields” section of the editor.

The actual meta-value itself should detail dates in YYYY-MM-DD format in order to best make use of WordPress’s meta-query functionality.

In order to create permalinks to custom queries, you use a custom rewrite rule to populate a custom query var from the desired URL, then alter the main WP_Query with a 'pre_get_posts' hook to retrieve the content indicated with your query var. All together, it looks something like this:

// Allow WP to pass around an "event_year" variable as part of URL querystrings
function wpse_224309_add_event_year_query_var( $vars )
{
  array_push( $vars, 'event_year' );
  return $vars;
}
add_filter( 'query_vars', 'wpse_224309_add_event_date_query_var' );

// Populate the "event_year" query var when visiting "/seasons/{year}" URLs
function wpse_224309_event_archive_year_rewrite() {
  add_rewrite_rule( 'seasons/([0-9]{4})/?', 'index.php?post_type=event&event_year=$matches[1]', 'top' );
}
add_action( 'init', 'wpse_224309_event_archive_year_rewrite' );

// If the "event_year" query var exists when in an event archive query, set up
// a meta-query to retrieve events with appropriate "event_date" metadata
function wpse_224309_event_archive_query_meta_date( $query ) {
  // Don't modify queries that aren't for primary content
  if( ! $query->is_main_query() )
    return;

  // Don't modify queries for anything but the 'events' post-type archive
  if( ! is_post_type_archive( 'events' ) )
    return;

  $year = $query->get( 'event_year' ); // Get the target year from the qv

  // If the "event_year" query var wasn't set, don't alter the query
  if( ! isset( $year ) )
    return;

  $year = absint( $year ); // Make sure $year is a non-negative integer

  // Look for events with an "event_date" meta-value between the end of the
  // previous year and before the start of the next year.
  $meta_query = array(
    'key'     => 'event_date',
    'type'    => 'DATE',
    'compare' => 'BETWEEN'
    'value'   => array(
      ($year - 1) . '-12-31',
      ($year + 1) . '-01-01'
  );

  // Overwrite the meta-query to retrieve events with an "event_date" in the
  // specified year, ordered by ascending date.
  $query->set( 'meta_query', array( $meta_query ) );
  $query->set( 'orderby', 'meta_value_date' );
  $query->set( 'meta_key', 'event_date' );
  $query->set( 'order', 'ASC' );
);
add_action( 'pre_get_posts', 'wpse_224309_event_archive_query_meta_date' );

If you need to filter events for other meta-data as well, however, you’ll need to compose a more detailed meta-query instead of completely overwriting it as I’ve done (see the Codex entries on WP_Query and WP_Meta_Query for details).

For the front-end, create an archive template for your “event” post type (likely archive-events.php, in order to permit WordPress to load it automatically via template hierarchy conventions). You’ll need to create a list of links to your custom year archive manually. In order to do so, you’ll need to retrieve an array of unique years in for the 'event_date' metadata. There are various ways to do this, but I believe that the most efficient is to directly query the database (WPDB->get_col() example courtesy of WPSE 9394):

function wpse_224309_get_event_years( $order ) {
  global $wpdb;

  if( ! $order )
    $order="DESC";

  return $wpdb->get_col( $wpdb->prepare( "
    SELECT DISTINCT YEAR( pm.meta_value ) FROM {$wpdb->postmeta} pm
    LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
    WHERE pm.meta_key = 'event_date' 
    AND p.post_status="publish" 
    AND p.post_type="events"
    ORDER BY pm.meta_value %s
  ", $order ) );
}

function wpse_224309_event_year_archive_links( $args="" ) {
  $defaults = array(
    'format' => 'html',
    'before' => '',
    'after' => '',
    'echo' => 1,
    'order' => 'DESC'
  );

  $r = wp_parse_args( $args, $defaults );
  $years = wpse_224309_get_event_years( $r['order'] );
  
  if( ! is_array( $years ) || 0 === count( $years ) )
    $return;

  $output="";

  for( $i = 0; $i < count( $years ); $i++ )
    $output .= get_archive_link( get_home_url( null, 'seasons/' . $years[ $i ] ), $years[ $i ], $r['format'], $r['before'], $r['after'] );

  if( $r['echo'] )
    echo $output;

  return $output;
}

Alternate Solutions

A simpler solution might be to simply use just a 'pre_get_posts' hook to modify queries on the front-end for year-based event archives (i.e. ! is_admin() && is_post_type_archive( 'events' ) && is_year()), unsetting all date-related query vars and redirecting the value of the year query var to the meta-query instead. This has the advantage of enabling you to simply use the wp_get_archives() function to print a list of year-based archive links. However, the approach is “hacky” in that it foils expected core behaviors, and may break other functionality as a result.

Similarly, the 'get_archives_link' filter could be used to replace the links produced by wp_get_archives() on the fly.

There are many different approaches to this problem – but it’s worth noting that filtering “events” by a date stored in metadata is something that’s no doubt come up time and time again – I’m confident that implemented solutions are out there if you dig deep enough.